mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-28 01:34:38 +02:00
Merge branch 'develop' into frontend-browser-metrics-workflow-2
This commit is contained in:
@@ -128,7 +128,6 @@
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.2",
|
||||
"sanitize-html": "2.17.5",
|
||||
"secure-json-parse": "4.1.0",
|
||||
"semver": "7.8.5",
|
||||
|
||||
@@ -5,20 +5,28 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiHashtag } from '@/models/Hashtag.js';
|
||||
import { MiHashtag } from '@/models/Hashtag.js';
|
||||
import type { HashtagsRepository, MiMeta } from '@/models/_.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('hashtag/create');
|
||||
|
||||
@Injectable()
|
||||
export class HashtagService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@@ -60,19 +68,74 @@ export class HashtagService {
|
||||
// TODO: サンプリング
|
||||
this.updateHashtagsRanking(tag, user.id);
|
||||
|
||||
const index = await this.hashtagsRepository.findOneBy({ name: tag });
|
||||
{
|
||||
const index = await this.hashtagsRepository.findOneBy({ name: tag });
|
||||
|
||||
if (index == null && !inc) return;
|
||||
if (index == null && inc) {
|
||||
try {
|
||||
if (isUserAttached) {
|
||||
await this.hashtagsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
name: tag,
|
||||
mentionedUserIds: [],
|
||||
mentionedUsersCount: 0,
|
||||
mentionedLocalUserIds: [],
|
||||
mentionedLocalUsersCount: 0,
|
||||
mentionedRemoteUserIds: [],
|
||||
mentionedRemoteUsersCount: 0,
|
||||
attachedUserIds: [user.id],
|
||||
attachedUsersCount: 1,
|
||||
attachedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [],
|
||||
attachedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0,
|
||||
attachedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [],
|
||||
attachedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0,
|
||||
} as MiHashtag);
|
||||
} else {
|
||||
await this.hashtagsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
name: tag,
|
||||
mentionedUserIds: [user.id],
|
||||
mentionedUsersCount: 1,
|
||||
mentionedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [],
|
||||
mentionedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0,
|
||||
mentionedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [],
|
||||
mentionedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0,
|
||||
attachedUserIds: [],
|
||||
attachedUsersCount: 0,
|
||||
attachedLocalUserIds: [],
|
||||
attachedLocalUsersCount: 0,
|
||||
attachedRemoteUserIds: [],
|
||||
attachedRemoteUsersCount: 0,
|
||||
} as MiHashtag);
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
if (isDuplicateKeyValueError(err)) {
|
||||
logger.info(`Duplicate insertion detected. Falling back to update. #${tag}`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (index != null) {
|
||||
const q = this.hashtagsRepository.createQueryBuilder('tag').update()
|
||||
.where('name = :name', { name: tag });
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
const transactionalHashtagRepository = transactionalEntityManager
|
||||
.getRepository(MiHashtag);
|
||||
|
||||
const index = await transactionalHashtagRepository
|
||||
.createQueryBuilder()
|
||||
.setLock('pessimistic_write')
|
||||
.where('name = :name', { name: tag })
|
||||
.getOne();
|
||||
|
||||
if (index == null) return;
|
||||
|
||||
const set = {} as any;
|
||||
|
||||
if (isUserAttached) {
|
||||
if (inc) {
|
||||
// 自分が初めてこのタグを使ったなら
|
||||
// 自分が初めてこのタグを使ったなら
|
||||
if (!index.attachedUserIds.some(id => id === user.id)) {
|
||||
set.attachedUserIds = () => `array_append("attachedUserIds", '${user.id}')`;
|
||||
set.attachedUsersCount = () => '"attachedUsersCount" + 1';
|
||||
@@ -117,46 +180,14 @@ export class HashtagService {
|
||||
}
|
||||
|
||||
if (Object.keys(set).length > 0) {
|
||||
q.set(set);
|
||||
q.execute();
|
||||
await transactionalHashtagRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.where('id = :id', { id: index.id })
|
||||
.set(set)
|
||||
.execute();
|
||||
}
|
||||
} else {
|
||||
if (isUserAttached) {
|
||||
this.hashtagsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
name: tag,
|
||||
mentionedUserIds: [],
|
||||
mentionedUsersCount: 0,
|
||||
mentionedLocalUserIds: [],
|
||||
mentionedLocalUsersCount: 0,
|
||||
mentionedRemoteUserIds: [],
|
||||
mentionedRemoteUsersCount: 0,
|
||||
attachedUserIds: [user.id],
|
||||
attachedUsersCount: 1,
|
||||
attachedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [],
|
||||
attachedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0,
|
||||
attachedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [],
|
||||
attachedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0,
|
||||
} as MiHashtag);
|
||||
} else {
|
||||
this.hashtagsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
name: tag,
|
||||
mentionedUserIds: [user.id],
|
||||
mentionedUsersCount: 1,
|
||||
mentionedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [],
|
||||
mentionedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0,
|
||||
mentionedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [],
|
||||
mentionedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0,
|
||||
attachedUserIds: [],
|
||||
attachedUsersCount: 0,
|
||||
attachedLocalUserIds: [],
|
||||
attachedLocalUsersCount: 0,
|
||||
attachedRemoteUserIds: [],
|
||||
attachedRemoteUsersCount: 0,
|
||||
} as MiHashtag);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -7,9 +7,8 @@ import { INestApplicationContext } from '@nestjs/common';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import * as assert from 'assert';
|
||||
import { afterAll, beforeAll, afterEach, describe, test } from 'vitest';
|
||||
import { afterAll, beforeAll, afterEach, describe, test, vi } from 'vitest';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
@@ -17,6 +16,11 @@ import { jobQueue } from '@/boot/common.js';
|
||||
import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
// i/move 呼び出しで大部分が同期的に完了する後続ジョブ (フォロー解除/ブロック・ミュート引き継ぎ/リスト更新等) を待つ
|
||||
const waitForMoveJobOptions = { timeout: 5000, interval: 50 };
|
||||
// AccountMoveService がテスト環境向けに 10000ms に短縮している遅延アンフォロージョブを待つ (安全マージンを載せた上限)
|
||||
const waitForDelayedUnfollowJobOptions = { timeout: 15000, interval: 100 };
|
||||
|
||||
describe('Account Move', () => {
|
||||
let jq: INestApplicationContext;
|
||||
let url: URL;
|
||||
@@ -273,53 +277,63 @@ describe('Account Move', () => {
|
||||
|
||||
assert.strictEqual(move.status, 200);
|
||||
|
||||
await setTimeout(1000 * 3); // wait for jobs to finish
|
||||
|
||||
// Unfollow delayed?
|
||||
const aliceFollowings = await api('users/following', {
|
||||
userId: alice.id,
|
||||
}, alice);
|
||||
assert.strictEqual(aliceFollowings.status, 200);
|
||||
assert.ok(aliceFollowings);
|
||||
assert.strictEqual(aliceFollowings.body.length, 3);
|
||||
await vi.waitFor(async () => {
|
||||
const aliceFollowings = await api('users/following', {
|
||||
userId: alice.id,
|
||||
}, alice);
|
||||
assert.strictEqual(aliceFollowings.status, 200);
|
||||
assert.ok(aliceFollowings);
|
||||
assert.strictEqual(aliceFollowings.body.length, 3);
|
||||
}, waitForMoveJobOptions);
|
||||
|
||||
const carolFollowings = await api('users/following', {
|
||||
userId: carol.id,
|
||||
}, carol);
|
||||
assert.strictEqual(carolFollowings.status, 200);
|
||||
assert.ok(carolFollowings);
|
||||
assert.strictEqual(carolFollowings.body.length, 2);
|
||||
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
|
||||
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
|
||||
await vi.waitFor(async () => {
|
||||
const carolFollowings = await api('users/following', {
|
||||
userId: carol.id,
|
||||
}, carol);
|
||||
assert.strictEqual(carolFollowings.status, 200);
|
||||
assert.ok(carolFollowings);
|
||||
assert.strictEqual(carolFollowings.body.length, 2);
|
||||
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
|
||||
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
|
||||
}, waitForMoveJobOptions);
|
||||
|
||||
const blockings = await api('blocking/list', {}, dave);
|
||||
assert.strictEqual(blockings.status, 200);
|
||||
assert.ok(blockings);
|
||||
assert.strictEqual(blockings.body.length, 2);
|
||||
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
|
||||
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
|
||||
await vi.waitFor(async () => {
|
||||
const blockings = await api('blocking/list', {}, dave);
|
||||
assert.strictEqual(blockings.status, 200);
|
||||
assert.ok(blockings);
|
||||
assert.strictEqual(blockings.body.length, 2);
|
||||
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
|
||||
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
|
||||
}, waitForMoveJobOptions);
|
||||
|
||||
const mutings = await api('mute/list', {}, dave);
|
||||
assert.strictEqual(mutings.status, 200);
|
||||
assert.ok(mutings);
|
||||
assert.strictEqual(mutings.body.length, 2);
|
||||
assert.strictEqual(mutings.body[0].muteeId, bob.id);
|
||||
assert.strictEqual(mutings.body[1].muteeId, alice.id);
|
||||
await vi.waitFor(async () => {
|
||||
const mutings = await api('mute/list', {}, dave);
|
||||
assert.strictEqual(mutings.status, 200);
|
||||
assert.ok(mutings);
|
||||
assert.strictEqual(mutings.body.length, 2);
|
||||
assert.strictEqual(mutings.body[0].muteeId, bob.id);
|
||||
assert.strictEqual(mutings.body[1].muteeId, alice.id);
|
||||
}, waitForMoveJobOptions);
|
||||
|
||||
const rootLists = await api('users/lists/list', {}, root);
|
||||
assert.strictEqual(rootLists.status, 200);
|
||||
assert.ok(rootLists);
|
||||
assert.ok(rootLists.body[0].userIds);
|
||||
assert.strictEqual(rootLists.body[0].userIds.length, 2);
|
||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
|
||||
await vi.waitFor(async () => {
|
||||
const rootLists = await api('users/lists/list', {}, root);
|
||||
assert.strictEqual(rootLists.status, 200);
|
||||
assert.ok(rootLists);
|
||||
assert.ok(rootLists.body[0].userIds);
|
||||
assert.strictEqual(rootLists.body[0].userIds.length, 2);
|
||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
|
||||
}, waitForMoveJobOptions);
|
||||
|
||||
const eveLists = await api('users/lists/list', {}, eve);
|
||||
assert.strictEqual(eveLists.status, 200);
|
||||
assert.ok(eveLists);
|
||||
assert.ok(eveLists.body[0].userIds);
|
||||
assert.strictEqual(eveLists.body[0].userIds.length, 1);
|
||||
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||
await vi.waitFor(async () => {
|
||||
const eveLists = await api('users/lists/list', {}, eve);
|
||||
assert.strictEqual(eveLists.status, 200);
|
||||
assert.ok(eveLists);
|
||||
assert.ok(eveLists.body[0].userIds);
|
||||
assert.strictEqual(eveLists.body[0].userIds.length, 1);
|
||||
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||
}, waitForMoveJobOptions);
|
||||
});
|
||||
|
||||
test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => {
|
||||
@@ -340,14 +354,14 @@ describe('Account Move', () => {
|
||||
});
|
||||
|
||||
test('Unfollowed after 10 sec (24 hours in production).', async () => {
|
||||
await setTimeout(1000 * 8);
|
||||
await vi.waitFor(async () => {
|
||||
const following = await api('users/following', {
|
||||
userId: alice.id,
|
||||
}, alice);
|
||||
|
||||
const following = await api('users/following', {
|
||||
userId: alice.id,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(following.status, 200);
|
||||
assert.strictEqual(following.body.length, 0);
|
||||
assert.strictEqual(following.status, 200);
|
||||
assert.strictEqual(following.body.length, 0);
|
||||
}, waitForDelayedUnfollowJobOptions);
|
||||
});
|
||||
|
||||
test('Unable to move if the destination account has already moved.', async () => {
|
||||
|
||||
@@ -8,9 +8,9 @@ process.env.NODE_ENV = 'test';
|
||||
import * as assert from 'assert';
|
||||
import { describe, beforeAll, test } from 'vitest';
|
||||
import { WebSocket } from 'ws';
|
||||
import { MiFollowing } from '@/models/Following.js';
|
||||
import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js';
|
||||
import { api, connectStream, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { MiFollowing } from '@/models/Following.js';
|
||||
|
||||
describe('Streaming', () => {
|
||||
let Followings: any;
|
||||
@@ -744,164 +744,83 @@ describe('Streaming', () => {
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
// XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac"
|
||||
/*
|
||||
describe('Hashtag Timeline', () => {
|
||||
test('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => {
|
||||
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
|
||||
if (type === 'note') {
|
||||
assert.deepStrictEqual(body.text, '#foo');
|
||||
ws.close();
|
||||
done();
|
||||
}
|
||||
}, {
|
||||
q: [
|
||||
['foo'],
|
||||
],
|
||||
});
|
||||
test('指定したハッシュタグの投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
chitose, 'hashtag',
|
||||
() => api('notes/create', { text: '#foo' }, chitose),
|
||||
msg => msg.type === 'note' && msg.body.text === '#foo',
|
||||
{ q: [['foo']] },
|
||||
);
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo',
|
||||
});
|
||||
}));
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
||||
test('指定したハッシュタグの投稿が流れる (AND)', async () => {
|
||||
const received: string[] = [];
|
||||
const ws = await connectStream(chitose, 'hashtag', (msg) => {
|
||||
if (msg.type === 'note') received.push(msg.body.text);
|
||||
}, { q: [['foo', 'bar']] });
|
||||
|
||||
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
|
||||
if (type === 'note') {
|
||||
if (body.text === '#foo') fooCount++;
|
||||
if (body.text === '#bar') barCount++;
|
||||
if (body.text === '#foo #bar') fooBarCount++;
|
||||
}
|
||||
}, {
|
||||
q: [
|
||||
['foo', 'bar'],
|
||||
],
|
||||
});
|
||||
await Promise.all([
|
||||
await api('notes/create', { text: '#foo' }, chitose),
|
||||
await api('notes/create', { text: '#bar' }, chitose),
|
||||
await api('notes/create', { text: '#foo #bar' }, chitose),
|
||||
]);
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
ws.close();
|
||||
|
||||
post(chitose, {
|
||||
text: '#bar',
|
||||
});
|
||||
assert.strictEqual(received.includes('#foo'), false);
|
||||
assert.strictEqual(received.includes('#bar'), false);
|
||||
assert.strictEqual(received.includes('#foo #bar'), true);
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo #bar',
|
||||
});
|
||||
test('指定したハッシュタグの投稿が流れる (OR)', async () => {
|
||||
const received: string[] = [];
|
||||
const ws = await connectStream(chitose, 'hashtag', (msg) => {
|
||||
if (msg.type === 'note') received.push(msg.body.text);
|
||||
}, { q: [['foo'], ['bar']] });
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fooCount, 0);
|
||||
assert.strictEqual(barCount, 0);
|
||||
assert.strictEqual(fooBarCount, 1);
|
||||
ws.close();
|
||||
done();
|
||||
}, 3000);
|
||||
}));
|
||||
await Promise.all([
|
||||
await api('notes/create', { text: '#foo' }, chitose),
|
||||
await api('notes/create', { text: '#bar' }, chitose),
|
||||
await api('notes/create', { text: '#foo #bar' }, chitose),
|
||||
await api('notes/create', { text: '#piyo' }, chitose),
|
||||
]);
|
||||
|
||||
test('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
||||
let piyoCount = 0;
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
ws.close();
|
||||
|
||||
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
|
||||
if (type === 'note') {
|
||||
if (body.text === '#foo') fooCount++;
|
||||
if (body.text === '#bar') barCount++;
|
||||
if (body.text === '#foo #bar') fooBarCount++;
|
||||
if (body.text === '#piyo') piyoCount++;
|
||||
}
|
||||
}, {
|
||||
q: [
|
||||
['foo'],
|
||||
['bar'],
|
||||
],
|
||||
});
|
||||
assert.strictEqual(received.includes('#foo'), true);
|
||||
assert.strictEqual(received.includes('#bar'), true);
|
||||
assert.strictEqual(received.includes('#foo #bar'), true);
|
||||
assert.strictEqual(received.includes('#piyo'), false);
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo',
|
||||
});
|
||||
test('指定したハッシュタグの投稿が流れる (AND + OR)', async () => {
|
||||
const received: string[] = [];
|
||||
const ws = await connectStream(chitose, 'hashtag', (msg) => {
|
||||
if (msg.type === 'note') received.push(msg.body.text);
|
||||
}, { q: [['foo', 'bar'], ['piyo']] });
|
||||
|
||||
post(chitose, {
|
||||
text: '#bar',
|
||||
});
|
||||
await Promise.all([
|
||||
api('notes/create', { text: '#foo' }, chitose),
|
||||
api('notes/create', { text: '#bar' }, chitose),
|
||||
api('notes/create', { text: '#foo #bar' }, chitose),
|
||||
api('notes/create', { text: '#piyo' }, chitose),
|
||||
api('notes/create', { text: '#waaa' }, chitose),
|
||||
]);
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo #bar',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
ws.close();
|
||||
|
||||
post(chitose, {
|
||||
text: '#piyo',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fooCount, 1);
|
||||
assert.strictEqual(barCount, 1);
|
||||
assert.strictEqual(fooBarCount, 1);
|
||||
assert.strictEqual(piyoCount, 0);
|
||||
ws.close();
|
||||
done();
|
||||
}, 3000);
|
||||
}));
|
||||
|
||||
test('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
||||
let piyoCount = 0;
|
||||
let waaaCount = 0;
|
||||
|
||||
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
|
||||
if (type === 'note') {
|
||||
if (body.text === '#foo') fooCount++;
|
||||
if (body.text === '#bar') barCount++;
|
||||
if (body.text === '#foo #bar') fooBarCount++;
|
||||
if (body.text === '#piyo') piyoCount++;
|
||||
if (body.text === '#waaa') waaaCount++;
|
||||
}
|
||||
}, {
|
||||
q: [
|
||||
['foo', 'bar'],
|
||||
['piyo'],
|
||||
],
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo',
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: '#bar',
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo #bar',
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: '#piyo',
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: '#waaa',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fooCount, 0);
|
||||
assert.strictEqual(barCount, 0);
|
||||
assert.strictEqual(fooBarCount, 1);
|
||||
assert.strictEqual(piyoCount, 1);
|
||||
assert.strictEqual(waaaCount, 0);
|
||||
ws.close();
|
||||
done();
|
||||
}, 3000);
|
||||
}));
|
||||
assert.strictEqual(received.includes('#foo'), false);
|
||||
assert.strictEqual(received.includes('#bar'), false);
|
||||
assert.strictEqual(received.includes('#foo #bar'), true);
|
||||
assert.strictEqual(received.includes('#piyo'), true);
|
||||
assert.strictEqual(received.includes('#waaa'), false);
|
||||
});
|
||||
});
|
||||
*/
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -149,7 +149,7 @@ const isRenote = Misskey.note.isPureRenote(note.value);
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const renoteTime = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => getAppearNote(note.value));
|
||||
const appearNote = computed(() => getAppearNote(note.value) ?? note.value);
|
||||
const showContent = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const isLong = shouldCollapsed(appearNote.value, []);
|
||||
|
||||
@@ -244,7 +244,7 @@ const fetchMore = async (): Promise<void> => {
|
||||
if (i === 10) item._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
const reverseConcat = _res => {
|
||||
const reverseConcat = (_res: MisskeyEntity[]) => {
|
||||
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
|
||||
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ const initialReactions = new Set(Object.keys(props.note.reactions));
|
||||
const reactions = ref<[string, number][]>([]);
|
||||
const hasMoreReactions = ref(false);
|
||||
|
||||
if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
|
||||
reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
|
||||
if (props.note.myReaction != null && !(props.note.myReaction in props.note.reactions)) {
|
||||
reactions.value.push([props.note.myReaction, props.note.reactions[props.note.myReaction]]);
|
||||
}
|
||||
|
||||
function onMockToggleReaction(emoji: string, count: number) {
|
||||
|
||||
@@ -46,6 +46,9 @@ const parsed = computed(() => {
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
|
||||
// slotsの型はTの条件型で決まり文字列インデックスアクセスができないため、
|
||||
// frontend側の同名コンポーネント (packages/frontend/src/components/global/I18n.vue) と同じくanyでキャストする
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : (slots as any)[x.arg]()));
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -45,11 +45,11 @@ export async function fetchCustomEmojis(force = false) {
|
||||
set('lastEmojisFetchedAt', now);
|
||||
}
|
||||
|
||||
let cachedTags;
|
||||
let cachedTags: string[] | null = null;
|
||||
export function getCustomEmojiTags() {
|
||||
if (cachedTags) return cachedTags;
|
||||
|
||||
const tags = new Set();
|
||||
const tags = new Set<string>();
|
||||
for (const emoji of customEmojis.value) {
|
||||
for (const tag of emoji.aliases) {
|
||||
tags.add(tag);
|
||||
|
||||
@@ -85,8 +85,6 @@
|
||||
"@storybook/core-events": "8.6.18",
|
||||
"@storybook/manager-api": "8.6.18",
|
||||
"@storybook/preview-api": "8.6.18",
|
||||
"@storybook/react": "10.4.6",
|
||||
"@storybook/react-vite": "10.4.6",
|
||||
"@storybook/test": "8.6.18",
|
||||
"@storybook/theming": "8.6.18",
|
||||
"@storybook/types": "8.6.18",
|
||||
@@ -110,10 +108,7 @@
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"@vue/compiler-core": "3.5.38",
|
||||
"acorn": "8.17.0",
|
||||
"astring": "1.9.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.17.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.9.2",
|
||||
"happy-dom": "20.10.6",
|
||||
@@ -132,7 +127,6 @@
|
||||
"rollup-plugin-visualizer": "7.0.1",
|
||||
"sass-embedded": "1.100.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "3.0.11",
|
||||
"storybook": "10.4.6",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.22.4",
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<div v-if="show" ref="el" :class="[$style.root]">
|
||||
<div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
|
||||
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" @click="openAccountMenu">
|
||||
<div v-if="!thin_ && (narrow || deviceKind === 'smartphone') && props.displayMyAvatar && $i" class="_button" @click="openAccountMenu">
|
||||
<MkAvatar :class="$style.avatar" :user="$i"/>
|
||||
</div>
|
||||
<div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttons"></div>
|
||||
@@ -62,6 +62,7 @@ import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'v
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import XTabs from './MkPageHeader.tabs.vue';
|
||||
import { getAccountMenu } from '@/accounts.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { DI } from '@/di.js';
|
||||
import * as os from '@/os.js';
|
||||
@@ -160,10 +161,12 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
height: var(--height);
|
||||
|
||||
.tabs:first-child {
|
||||
.tabs:first-child,
|
||||
&:not(.slim) > :not(.titleContainer) ~ .tabs {
|
||||
margin-left: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ function onDrop(ev: DragEvent): void {
|
||||
}
|
||||
|
||||
function onKeydown(ev: KeyboardEvent) {
|
||||
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
||||
if (ev.key === 'Enter') {
|
||||
if (prefer.s['chat.sendOnEnter']) {
|
||||
if (!(ev.ctrlKey || ev.metaKey || ev.shiftKey)) {
|
||||
|
||||
Reference in New Issue
Block a user