diff --git a/package.json b/package.json index 60c6a09787..811a60814a 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@types/koa-send": "4.1.1", "@types/koa-views": "2.0.3", "@types/koa__cors": "2.2.3", - "@types/minio": "6.0.2", + "@types/minio": "7.0.0", "@types/mkdirp": "0.5.2", "@types/mocha": "5.2.3", "@types/mongodb": "3.1.4", @@ -80,7 +80,7 @@ "@types/webpack": "4.4.11", "@types/webpack-stream": "3.2.10", "@types/websocket": "0.0.40", - "@types/ws": "6.0.0", + "@types/ws": "6.0.1", "animejs": "2.2.0", "autosize": "4.0.2", "autwh": "0.1.0", diff --git a/src/client/app/boot.js b/src/client/app/boot.js index dd2cf93a89..f14cebe7d5 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -140,7 +140,7 @@ // Random localStorage.setItem('salt', Math.random().toString()); - // Clear cache (serive worker) + // Clear cache (service worker) try { navigator.serviceWorker.controller.postMessage('clear'); diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts index 4445eefc39..91b165b45d 100644 --- a/src/client/app/common/scripts/check-for-update.ts +++ b/src/client/app/common/scripts/check-for-update.ts @@ -9,7 +9,7 @@ export default async function(mios: MiOS, force = false, silent = false) { localStorage.setItem('should-refresh', 'true'); localStorage.setItem('v', newer); - // Clear cache (serive worker) + // Clear cache (service worker) try { if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage('clear'); diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts index 568b8b0372..8dd06f67d3 100644 --- a/src/client/app/common/scripts/streaming/stream-manager.ts +++ b/src/client/app/common/scripts/streaming/stream-manager.ts @@ -1,6 +1,7 @@ import { EventEmitter } from 'eventemitter3'; import * as uuid from 'uuid'; import Connection from './stream'; +import { erase } from '../../../../../prelude/array'; /** * ストリーム接続を管理するクラス @@ -89,7 +90,7 @@ export default abstract class StreamManager<T extends Connection> extends EventE * @param userId use で発行したユーザーID */ public dispose(userId) { - this.users = this.users.filter(id => id != userId); + this.users = erase(userId, this.users); this._connection.user = `Managed (${ this.users.length })`; diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue index 115c934c8b..30d9799fec 100644 --- a/src/client/app/common/views/components/poll-editor.vue +++ b/src/client/app/common/views/components/poll-editor.vue @@ -20,6 +20,7 @@ <script lang="ts"> import Vue from 'vue'; +import { erase } from '../../../../../prelude/array'; export default Vue.extend({ data() { return { @@ -53,7 +54,7 @@ export default Vue.extend({ get() { return { - choices: this.choices.filter(choice => choice != '') + choices: erase('', this.choices) } }, diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index f6f52c8f1f..65dc9eb9c2 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -62,6 +62,7 @@ import getFace from '../../../common/scripts/get-face'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; +import { erase } from '../../../../../prelude/array'; export default Vue.extend({ components: { @@ -346,7 +347,7 @@ export default Vue.extend({ }, removeVisibleUser(user) { - this.visibleUsers = this.visibleUsers.filter(u => u != user); + this.visibleUsers = erase(user, this.visibleUsers); }, post() { diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index d55029fb50..1bcb0ecb20 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -85,6 +85,7 @@ <script lang="ts"> import Vue from 'vue'; import { host, copyright } from '../../../config'; +import { concat } from '../../../../../prelude/array'; export default Vue.extend({ data() { @@ -119,8 +120,8 @@ export default Vue.extend({ (this as any).api('notes/local-timeline', { fileType: image, limit: 6 - }).then(notes => { - const files = [].concat(...notes.map(n => n.files)); + }).then((notes: any[]) => { + const files = concat(notes.map((n: any): any[] => n.files)); this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); }); }, diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts index c2ec7f1750..0f72cd2f34 100644 --- a/src/client/app/mios.ts +++ b/src/client/app/mios.ts @@ -17,6 +17,7 @@ import Err from './common/views/components/connect-failed.vue'; import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline'; import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline'; import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline'; +import { erase } from '../../prelude/array'; //#region api requests let spinner = null; @@ -537,7 +538,7 @@ export default class MiOS extends EventEmitter { } public unregisterStreamConnection(connection: Connection) { - this.connections = this.connections.filter(c => c != connection); + this.connections = erase(connection, this.connections); } } diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 644e27cce8..8107c1f3a7 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -59,6 +59,7 @@ import MkVisibilityChooser from '../../../common/views/components/visibility-cho import getFace from '../../../common/scripts/get-face'; import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; +import { erase } from '../../../../../prelude/array'; export default Vue.extend({ components: { @@ -262,7 +263,7 @@ export default Vue.extend({ }, removeVisibleUser(user) { - this.visibleUsers = this.visibleUsers.filter(u => u != user); + this.visibleUsers = erase(user, this.visibleUsers); }, clear() { diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue index 1856731d8a..7446cc700f 100644 --- a/src/client/app/mobile/views/pages/welcome.vue +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -40,6 +40,7 @@ <script lang="ts"> import Vue from 'vue'; import { apiUrl, copyright, host } from '../../../config'; +import { concat } from '../../../../../prelude/array'; export default Vue.extend({ data() { @@ -79,8 +80,8 @@ export default Vue.extend({ (this as any).api('notes/local-timeline', { fileType: image, limit: 6 - }).then(notes => { - const files = [].concat(...notes.map(n => n.files)); + }).then((notes: any[]) => { + const files = concat(notes.map((n: any): any[] => n.files)); this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); }); } diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 53f3eefc08..08dd9f9920 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -4,6 +4,7 @@ import * as nestedProperty from 'nested-property'; import MiOS from './mios'; import { hostname } from './config'; +import { erase } from '../../prelude/array'; const defaultSettings = { home: null, @@ -195,7 +196,7 @@ export default (os: MiOS) => new Vuex.Store({ removeDeckColumn(state, id) { state.deck.columns = state.deck.columns.filter(c => c.id != id); - state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id)); + state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); }, @@ -266,7 +267,7 @@ export default (os: MiOS) => new Vuex.Store({ stackLeftDeckColumn(state, id) { const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1); - state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id)); + state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); const left = state.deck.layout[i - 1]; if (left) state.deck.layout[i - 1].push(id); state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); @@ -274,7 +275,7 @@ export default (os: MiOS) => new Vuex.Store({ popRightDeckColumn(state, id) { const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1); - state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id)); + state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); state.deck.layout.splice(i + 1, 0, [id]); state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); }, diff --git a/src/client/app/sw.js b/src/client/app/sw.js index ac7ea20acf..d381bfb7a5 100644 --- a/src/client/app/sw.js +++ b/src/client/app/sw.js @@ -3,6 +3,7 @@ */ import composeNotification from './common/scripts/compose-notification'; +import { erase } from '../../prelude/array'; // キャッシュするリソース const cachee = [ @@ -24,8 +25,7 @@ self.addEventListener('activate', ev => { // Clean up old caches ev.waitUntil( caches.keys().then(keys => Promise.all( - keys - .filter(key => key != _VERSION_) + erase(_VERSION_, keys) .map(key => caches.delete(key)) )) ); diff --git a/src/games/reversi/core.ts b/src/games/reversi/core.ts index 9199efa092..e724917fbf 100644 --- a/src/games/reversi/core.ts +++ b/src/games/reversi/core.ts @@ -1,4 +1,4 @@ -import { count, countIf } from "../../prelude/array"; +import { count, concat } from "../../prelude/array"; // MISSKEY REVERSI ENGINE @@ -110,7 +110,7 @@ export default class Reversi { * 白石の数 */ public get whiteCount() { - return count(BLACK, this.board); + return count(WHITE, this.board); } /** @@ -238,87 +238,55 @@ export default class Reversi { /** * 指定のマスに石を置いた時の、反転させられる石を取得します * @param color 自分の色 - * @param pos 位置 + * @param initPos 位置 */ - public effects(color: Color, pos: number): number[] { + public effects(color: Color, initPos: number): number[] { const enemyColor = !color; - // ひっくり返せる石(の位置)リスト - let stones: number[] = []; + const diffVectors: [number, number][] = [ + [ 0, -1], // 上 + [ +1, -1], // 右上 + [ +1, 0], // 右 + [ +1, +1], // 右下 + [ 0, +1], // 下 + [ -1, +1], // 左下 + [ -1, 0], // 左 + [ -1, -1] // 左上 + ]; - const initPos = pos; - - // 走査 - const iterate = (fn: (i: number) => number[]) => { - let i = 1; - const found = []; + const effectsInLine = ([dx, dy]: [number, number]): number[] => { + const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy]; + const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列 + let [x, y] = this.transformPosToXy(initPos); while (true) { - let [x, y] = fn(i); + [x, y] = nextPos(x, y); // 座標が指し示す位置がボード外に出たとき if (this.opts.loopedBoard) { - if (x < 0 ) x = this.mapWidth - ((-x) % this.mapWidth); - if (y < 0 ) y = this.mapHeight - ((-y) % this.mapHeight); - if (x >= this.mapWidth ) x = x % this.mapWidth; - if (y >= this.mapHeight) y = y % this.mapHeight; + x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth; + y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight; - // for debug - //if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) { - // console.log(x, y); - //} - - // 一周して自分に帰ってきたら if (this.transformXyToPos(x, y) == initPos) { - // ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、 - // そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります) - // このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます - // (あと無効な方がゲームとしておもしろそうだった) - stones = stones.concat(found); - break; + // 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ) + return found; } } else { - if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break; + if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) { + return []; // 挟めないことが確定 (盤面外に到達) + } } const pos = this.transformXyToPos(x, y); - - //#region 「配置不能」マスに当たった場合走査終了 - const pixel = this.mapDataGet(pos); - if (pixel == 'null') break; - //#endregion - - // 石取得 + if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達) const stone = this.board[pos]; - - // 石が置かれていないマスなら走査終了 - if (stone === null) break; - - // 相手の石なら「ひっくり返せるかもリスト」に入れておく - if (stone === enemyColor) found.push(pos); - - // 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了 - if (stone === color) { - stones = stones.concat(found); - break; - } - - i++; + if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達) + if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見) + if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見) } }; - const [x, y] = this.transformPosToXy(pos); - - iterate(i => [x , y - i]); // 上 - iterate(i => [x + i, y - i]); // 右上 - iterate(i => [x + i, y ]); // 右 - iterate(i => [x + i, y + i]); // 右下 - iterate(i => [x , y + i]); // 下 - iterate(i => [x - i, y + i]); // 左下 - iterate(i => [x - i, y ]); // 左 - iterate(i => [x - i, y - i]); // 左上 - - return stones; + return concat(diffVectors.map(effectsInLine)); } /** diff --git a/src/prelude/array.ts b/src/prelude/array.ts index aee17640ed..abef6ca039 100644 --- a/src/prelude/array.ts +++ b/src/prelude/array.ts @@ -6,6 +6,18 @@ export function count<T>(x: T, xs: T[]): number { return countIf(y => x === y, xs); } -export function intersperse<T>(sep: T, xs: T[]): T[] { - return [].concat(...xs.map(x => [sep, x])).slice(1); +export function concat<T>(xss: T[][]): T[] { + return ([] as T[]).concat(...xss); +} + +export function intersperse<T>(sep: T, xs: T[]): T[] { + return concat(xs.map(x => [sep, x])).slice(1); +} + +export function erase<T>(x: T, xs: T[]): T[] { + return xs.filter(y => x !== y); +} + +export function unique<T>(xs: T[]): T[] { + return [...new Set(xs)]; } diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index 01dfccc71c..e7c08ca9f0 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -1,4 +1,5 @@ import Note from '../../../../models/note'; +import { erase } from '../../../../prelude/array'; /* トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 @@ -85,8 +86,7 @@ export default () => new Promise(async (res, rej) => { //#endregion // タグを人気順に並べ替え - let hots = (await Promise.all(hotsPromises)) - .filter(x => x != null) + let hots = erase(null, await Promise.all(hotsPromises)) .sort((a, b) => b.count - a.count) .map(tag => tag.name) .slice(0, max); diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts index 82f11a9775..77082c2600 100644 --- a/src/server/api/endpoints/notes/search_by_tag.ts +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -5,6 +5,7 @@ import Mute from '../../../../models/mute'; import { getFriendIds } from '../../common/get-friends'; import { pack } from '../../../../models/note'; import getParams from '../../get-params'; +import { erase } from '../../../../prelude/array'; export const meta = { desc: { @@ -103,23 +104,23 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => if (psErr) throw psErr; if (ps.includeUserUsernames != null) { - const ids = (await Promise.all(ps.includeUserUsernames.map(async (username) => { + const ids = erase(null, await Promise.all(ps.includeUserUsernames.map(async (username) => { const _user = await User.findOne({ usernameLower: username.toLowerCase() }); return _user ? _user._id : null; - }))).filter(id => id != null); + }))); ids.forEach(id => ps.includeUserIds.push(id)); } if (ps.excludeUserUsernames != null) { - const ids = (await Promise.all(ps.excludeUserUsernames.map(async (username) => { + const ids = erase(null, await Promise.all(ps.excludeUserUsernames.map(async (username) => { const _user = await User.findOne({ usernameLower: username.toLowerCase() }); return _user ? _user._id : null; - }))).filter(id => id != null); + }))); ids.forEach(id => ps.excludeUserIds.push(id)); } diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 11e3755863..c08836c94b 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -24,6 +24,7 @@ import isQuote from '../../misc/is-quote'; import { TextElementMention } from '../../mfm/parse/elements/mention'; import { TextElementHashtag } from '../../mfm/parse/elements/hashtag'; import { updateNoteStats } from '../update-chart'; +import { erase, unique } from '../../prelude/array'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -103,7 +104,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< if (data.viaMobile == null) data.viaMobile = false; if (data.visibleUsers) { - data.visibleUsers = data.visibleUsers.filter(x => x != null); + data.visibleUsers = erase(null, data.visibleUsers); } if (data.reply && data.reply.deletedAt != null) { @@ -384,7 +385,7 @@ function extractHashtags(tokens: ReturnType<typeof parse>): string[] { .map(t => (t as TextElementHashtag).hashtag) .filter(tag => tag.length <= 100); - return [...new Set(hashtags)]; + return unique(hashtags); } function index(note: INote) { @@ -541,20 +542,20 @@ function incNotesCount(user: IUser) { async function extractMentionedUsers(tokens: ReturnType<typeof parse>): Promise<IUser[]> { if (tokens == null) return []; - const mentionTokens = [...new Set( + const mentionTokens = unique( tokens .filter(t => t.type == 'mention') as TextElementMention[] - )]; + ); - const mentionedUsers = [...new Set( - (await Promise.all(mentionTokens.map(async m => { + const mentionedUsers = unique( + erase(null, await Promise.all(mentionTokens.map(async m => { try { return await resolveUser(m.username, m.host); } catch (e) { return null; } - }))).filter(x => x != null) - )]; + }))) + ); return mentionedUsers; } diff --git a/tslint.json b/tslint.json index ae0df46b96..1adc0a2aed 100644 --- a/tslint.json +++ b/tslint.json @@ -17,6 +17,7 @@ "no-empty":false, "ordered-imports": [false], "arrow-parens": false, + "array-type": false, "object-literal-shorthand": false, "object-literal-key-quotes": false, "triple-equals": [false],