Merge branch 'develop' into feature/emoji-grid
This commit is contained in:
commit
1d1d1b0d64
|
@ -38,7 +38,7 @@
|
|||
# Option 3: If neither of the above applies to you.
|
||||
# (In this case, the source code should be published
|
||||
# on the Misskey interface. IT IS NOT ENOUGH TO
|
||||
# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY
|
||||
# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY
|
||||
# E-MAIL OR OTHER MEANS. If you are not satisfied
|
||||
# with this, it is recommended that you read the
|
||||
# license again carefully. Anyway, enabling this
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"Orta.vscode-jest",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"mrmlnc.vscode-json5"
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"Orta.vscode-jest",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"mrmlnc.vscode-json5"
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
* β版として公開のため、旧画面も引き続き利用可能です
|
||||
|
||||
### Client
|
||||
-
|
||||
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
|
||||
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
|
||||
- Enhance: リアクション・いいねの総数を表示するように
|
||||
- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
|
||||
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
|
||||
- Fix: 周年の実績が閏年を考慮しない問題を修正
|
||||
|
||||
### Server
|
||||
-
|
||||
|
@ -22,7 +27,7 @@
|
|||
- Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
|
||||
|
||||
### Server
|
||||
-
|
||||
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
|
||||
|
||||
## 2024.3.0
|
||||
|
||||
|
|
|
@ -316,6 +316,98 @@ export const handlers = [
|
|||
|
||||
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
|
||||
|
||||
## Nest
|
||||
|
||||
### Nest Service Circular dependency / Nestでサービスの循環参照でエラーが起きた場合
|
||||
|
||||
#### forwardRef
|
||||
まずは簡単に`forwardRef`を試してみる
|
||||
|
||||
```typescript
|
||||
export class FooService {
|
||||
constructor(
|
||||
@Inject(forwardRef(() => BarService))
|
||||
private barService: BarService
|
||||
) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### OnModuleInit
|
||||
できなければ`OnModuleInit`を使う
|
||||
|
||||
```typescript
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { BarService } from '@/core/BarService';
|
||||
|
||||
@Injectable()
|
||||
export class FooService implements OnModuleInit {
|
||||
private barService: BarService // constructorから移動してくる
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
) {
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.barService = this.moduleRef.get(BarService.name);
|
||||
}
|
||||
|
||||
public async niceMethod() {
|
||||
return await this.barService.incredibleMethod({ hoge: 'fuga' });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Service Unit Test
|
||||
テストで`onModuleInit`を呼び出す必要がある
|
||||
|
||||
```typescript
|
||||
// import ...
|
||||
|
||||
describe('test', () => {
|
||||
let app: TestingModule;
|
||||
let fooService: FooService; // for test case
|
||||
let barService: BarService; // for test case
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: ...,
|
||||
providers: [
|
||||
FooService,
|
||||
{ // mockする (mockは必須ではないかもしれない)
|
||||
provide: BarService,
|
||||
useFactory: () => ({
|
||||
incredibleMethod: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{ // Provideにする
|
||||
provide: BarService.name,
|
||||
useExisting: BarService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.useMocker(...
|
||||
.compile();
|
||||
|
||||
fooService = app.get<FooService>(FooService);
|
||||
barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
|
||||
|
||||
// onModuleInitを実行する
|
||||
await fooService.onModuleInit();
|
||||
});
|
||||
|
||||
test('nice', () => {
|
||||
await fooService.niceMethod();
|
||||
|
||||
expect(barService.incredibleMethod).toHaveBeenCalled();
|
||||
expect(barService.incredibleMethod.mock.lastCall![0])
|
||||
.toEqual({ hoge: 'fuga' });
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
### Misskeyのドメイン固有の概念は`Mi`をprefixする
|
||||
|
|
|
@ -1996,6 +1996,10 @@ export interface Locale extends ILocale {
|
|||
* ノートのアクションをホバー時のみ表示する
|
||||
*/
|
||||
"showNoteActionsOnlyHover": string;
|
||||
/**
|
||||
* ノートのリアクション数を表示する
|
||||
*/
|
||||
"showReactionsCount": string;
|
||||
/**
|
||||
* 履歴はありません
|
||||
*/
|
||||
|
@ -8913,6 +8917,10 @@ export interface Locale extends ILocale {
|
|||
* {n}人がリアクションしました
|
||||
*/
|
||||
"reactedBySomeUsers": ParameterizedString<"n">;
|
||||
/**
|
||||
* {n}人がいいねしました
|
||||
*/
|
||||
"likedBySomeUsers": ParameterizedString<"n">;
|
||||
/**
|
||||
* {n}人がリノートしました
|
||||
*/
|
||||
|
|
|
@ -495,6 +495,7 @@ emojiStyle: "絵文字のスタイル"
|
|||
native: "ネイティブ"
|
||||
disableDrawer: "メニューをドロワーで表示しない"
|
||||
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
|
||||
showReactionsCount: "ノートのリアクション数を表示する"
|
||||
noHistory: "履歴はありません"
|
||||
signinHistory: "ログイン履歴"
|
||||
enableAdvancedMfm: "高度なMFMを有効にする"
|
||||
|
@ -2356,6 +2357,7 @@ _notification:
|
|||
sendTestNotification: "テスト通知を送信する"
|
||||
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
|
||||
reactedBySomeUsers: "{n}人がリアクションしました"
|
||||
likedBySomeUsers: "{n}人がいいねしました"
|
||||
renotedBySomeUsers: "{n}人がリノートしました"
|
||||
followedBySomeUsers: "{n}人にフォローされました"
|
||||
flushNotification: "通知の履歴をリセットする"
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"watch": "node watch.mjs",
|
||||
"restart": "pnpm build && pnpm start",
|
||||
"dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
||||
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
|
||||
|
|
|
@ -129,7 +129,7 @@ export class ApNoteService {
|
|||
value,
|
||||
object,
|
||||
});
|
||||
throw new Error('invalid note');
|
||||
throw err;
|
||||
}
|
||||
|
||||
const note = object as IPost;
|
||||
|
|
|
@ -248,7 +248,7 @@ export class DriveFileEntityService {
|
|||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||
detail: true,
|
||||
}) : null,
|
||||
userId: opts.withUser ? file.userId : null,
|
||||
userId: file.userId,
|
||||
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -333,6 +333,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
|
||||
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
||||
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
||||
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
|
||||
|
|
|
@ -223,6 +223,10 @@ export const packedNoteSchema = {
|
|||
}],
|
||||
},
|
||||
},
|
||||
reactionCount: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
renoteCount: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -67,7 +67,7 @@ export const paramDef = {
|
|||
withFile: { type: 'boolean' },
|
||||
notify: { type: 'boolean' },
|
||||
},
|
||||
required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
|
||||
required: ['antennaId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
@ -83,8 +83,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
|
||||
throw new Error('either keywords or excludeKeywords is required.');
|
||||
if (ps.keywords && ps.excludeKeywords) {
|
||||
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
|
||||
throw new Error('either keywords or excludeKeywords is required.');
|
||||
}
|
||||
}
|
||||
// Fetch the antenna
|
||||
const antenna = await this.antennasRepository.findOneBy({
|
||||
|
@ -98,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
let userList;
|
||||
|
||||
if (ps.src === 'list' && ps.userListId) {
|
||||
if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
|
||||
userList = await this.userListsRepository.findOneBy({
|
||||
id: ps.userListId,
|
||||
userId: me.id,
|
||||
|
@ -112,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
await this.antennasRepository.update(antenna.id, {
|
||||
name: ps.name,
|
||||
src: ps.src,
|
||||
userListId: userList ? userList.id : null,
|
||||
userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
|
||||
keywords: ps.keywords,
|
||||
excludeKeywords: ps.excludeKeywords,
|
||||
users: ps.users,
|
||||
|
|
|
@ -86,8 +86,8 @@
|
|||
//#endregion
|
||||
|
||||
//#region Script
|
||||
function importAppScript() {
|
||||
import(`/vite/${CLIENT_ENTRY}`)
|
||||
async function importAppScript() {
|
||||
await import(`/vite/${CLIENT_ENTRY}`)
|
||||
.catch(async e => {
|
||||
console.error(e);
|
||||
renderError('APP_IMPORT', e);
|
||||
|
|
|
@ -472,7 +472,7 @@ describe('Note', () => {
|
|||
priority: 0,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -784,7 +784,7 @@ describe('Note', () => {
|
|||
priority: 1,
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -838,7 +838,7 @@ describe('Note', () => {
|
|||
priority: 1,
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -894,7 +894,7 @@ describe('Note', () => {
|
|||
priority: 1,
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
|
|
@ -890,17 +890,35 @@ describe('Timelines', () => {
|
|||
|
||||
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||
await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
|
||||
await sleep(1000);
|
||||
const aliceNote = await post(alice, { text: 'hi' });
|
||||
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id, withReplies: false }, alice);
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||
|
||||
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||
await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
|
||||
await sleep(1000);
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
|
||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type FIXME = any;
|
|
@ -4,10 +4,10 @@
|
|||
*/
|
||||
|
||||
import Ajv from 'ajv';
|
||||
import { Schema } from '@/misc/schema';
|
||||
import { Schema } from '@/misc/json-schema.js';
|
||||
|
||||
export const getValidator = (paramDef: Schema) => {
|
||||
const ajv = new Ajv({
|
||||
const ajv = new Ajv.default({
|
||||
useDefaults: true,
|
||||
});
|
||||
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedLocals": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
|
@ -18,6 +18,7 @@
|
|||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
|
|
|
@ -90,7 +90,8 @@ describe('RelayService', () => {
|
|||
|
||||
expect(queueService.deliver).toHaveBeenCalled();
|
||||
expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo');
|
||||
expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow');
|
||||
expect(typeof queueService.deliver.mock.lastCall![1]?.object).toBe('object');
|
||||
expect((queueService.deliver.mock.lastCall![1]?.object as any).type).toBe('Follow');
|
||||
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
|
||||
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
|
||||
|
||||
|
|
|
@ -187,14 +187,26 @@ export async function mainBoot() {
|
|||
if ($i.followersCount >= 500) claimAchievement('followers500');
|
||||
if ($i.followersCount >= 1000) claimAchievement('followers1000');
|
||||
|
||||
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
|
||||
claimAchievement('passedSinceAccountCreated1');
|
||||
}
|
||||
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
|
||||
claimAchievement('passedSinceAccountCreated2');
|
||||
}
|
||||
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
|
||||
const createdAt = new Date($i.createdAt);
|
||||
const createdAtThreeYearsLater = new Date($i.createdAt);
|
||||
createdAtThreeYearsLater.setFullYear(createdAtThreeYearsLater.getFullYear() + 3);
|
||||
if (now >= createdAtThreeYearsLater) {
|
||||
claimAchievement('passedSinceAccountCreated3');
|
||||
claimAchievement('passedSinceAccountCreated2');
|
||||
claimAchievement('passedSinceAccountCreated1');
|
||||
} else {
|
||||
const createdAtTwoYearsLater = new Date($i.createdAt);
|
||||
createdAtTwoYearsLater.setFullYear(createdAtTwoYearsLater.getFullYear() + 2);
|
||||
if (now >= createdAtTwoYearsLater) {
|
||||
claimAchievement('passedSinceAccountCreated2');
|
||||
claimAchievement('passedSinceAccountCreated1');
|
||||
} else {
|
||||
const createdAtOneYearLater = new Date($i.createdAt);
|
||||
createdAtOneYearLater.setFullYear(createdAtOneYearLater.getFullYear() + 1);
|
||||
if (now >= createdAtOneYearLater) {
|
||||
claimAchievement('passedSinceAccountCreated1');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (claimedAchievements.length >= 30) {
|
||||
|
@ -229,7 +241,7 @@ export async function mainBoot() {
|
|||
|
||||
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
|
||||
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
|
||||
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ import * as os from '@/os.js';
|
|||
import bytes from '@/filters/bytes.js';
|
||||
import { hms } from '@/filters/hms.js';
|
||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||
import { iAmModerator } from '@/account.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
|
||||
const props = defineProps<{
|
||||
audio: Misskey.entities.DriveFile;
|
||||
|
@ -96,8 +96,6 @@ function showMenu(ev: MouseEvent) {
|
|||
|
||||
if (iAmModerator) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
||||
icon: props.audio.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
|
||||
danger: true,
|
||||
|
@ -105,6 +103,17 @@ function showMenu(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
if ($i?.id === props.audio.userId) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.audio.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
menuShowing.value = true;
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
|
||||
align: 'right',
|
||||
|
|
|
@ -59,7 +59,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { iAmModerator } from '@/account.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
image: Misskey.entities.DriveFile;
|
||||
|
@ -114,6 +114,13 @@ function showMenu(ev: MouseEvent) {
|
|||
action: () => {
|
||||
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
|
||||
},
|
||||
}] : []), ...($i?.id === props.image.userId ? [{
|
||||
type: 'divider' as const,
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.image.id}`,
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ import * as os from '@/os.js';
|
|||
import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
|
||||
import hasAudio from '@/scripts/media-has-audio.js';
|
||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||
import { iAmModerator } from '@/account.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
|
||||
const props = defineProps<{
|
||||
video: Misskey.entities.DriveFile;
|
||||
|
@ -122,8 +122,6 @@ function showMenu(ev: MouseEvent) {
|
|||
|
||||
if (iAmModerator) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
||||
icon: props.video.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
|
||||
danger: true,
|
||||
|
@ -131,6 +129,17 @@ function showMenu(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
if ($i?.id === props.video.userId) {
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.video.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
menuShowing.value = true;
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
|
||||
align: 'right',
|
||||
|
|
|
@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
||||
<template #more>
|
||||
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
|
||||
</template>
|
||||
|
@ -101,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<footer :class="$style.footer">
|
||||
<button :class="$style.footerButton" class="_button" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
||||
</button>
|
||||
<button
|
||||
v-if="canRenote"
|
||||
|
@ -111,17 +111,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ appearNote.renoteCount }}</p>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
</button>
|
||||
<button v-else :class="$style.footerButton" class="_button" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
|
||||
<i class="ti ti-minus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
|
@ -175,6 +175,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
|||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import number from '@/filters/number.js';
|
||||
import * as os from '@/os.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
@ -420,6 +421,14 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
|||
});
|
||||
}
|
||||
|
||||
function toggleReact() {
|
||||
if (appearNote.value.myReaction == null) {
|
||||
react();
|
||||
} else {
|
||||
undoReact(appearNote.value);
|
||||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
if (props.mock) {
|
||||
return;
|
||||
|
|
|
@ -106,10 +106,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
|
||||
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
||||
</button>
|
||||
<button
|
||||
v-if="canRenote"
|
||||
|
@ -119,17 +119,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
</button>
|
||||
<button v-else class="_button" :class="$style.noteFooterButton" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
|
||||
<i class="ti ti-minus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
|
@ -209,6 +209,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
|||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import number from '@/filters/number.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
|
@ -401,14 +402,22 @@ function react(viaKeyboard = false): void {
|
|||
}
|
||||
}
|
||||
|
||||
function undoReact(note): void {
|
||||
const oldReaction = note.myReaction;
|
||||
function undoReact(targetNote: Misskey.entities.Note): void {
|
||||
const oldReaction = targetNote.myReaction;
|
||||
if (!oldReaction) return;
|
||||
misskeyApi('notes/reactions/delete', {
|
||||
noteId: note.id,
|
||||
noteId: targetNote.id,
|
||||
});
|
||||
}
|
||||
|
||||
function toggleReact() {
|
||||
if (appearNote.value.myReaction == null) {
|
||||
react();
|
||||
} else {
|
||||
undoReact(appearNote.value);
|
||||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement): boolean => {
|
||||
if (el.tagName === 'A') return true;
|
||||
|
|
|
@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.head">
|
||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
||||
|
@ -57,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
|
||||
|
@ -201,6 +203,7 @@ const rejectFollowRequest = () => {
|
|||
}
|
||||
|
||||
.icon_reactionGroup,
|
||||
.icon_reactionGroupHeart,
|
||||
.icon_renoteGroup {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
@ -213,11 +216,15 @@ const rejectFollowRequest = () => {
|
|||
}
|
||||
|
||||
.icon_reactionGroup {
|
||||
background: #e99a0b;
|
||||
background: var(--eventReaction);
|
||||
}
|
||||
|
||||
.icon_reactionGroupHeart {
|
||||
background: var(--eventReactionHeart);
|
||||
}
|
||||
|
||||
.icon_renoteGroup {
|
||||
background: #36d298;
|
||||
background: var(--eventRenote);
|
||||
}
|
||||
|
||||
.icon_app {
|
||||
|
@ -246,49 +253,49 @@ const rejectFollowRequest = () => {
|
|||
|
||||
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest {
|
||||
padding: 3px;
|
||||
background: #36aed2;
|
||||
background: var(--eventFollow);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_renote {
|
||||
padding: 3px;
|
||||
background: #36d298;
|
||||
background: var(--eventRenote);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_quote {
|
||||
padding: 3px;
|
||||
background: #36d298;
|
||||
background: var(--eventRenote);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_reply {
|
||||
padding: 3px;
|
||||
background: #007aff;
|
||||
background: var(--eventReply);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_mention {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_pollEnded {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_achievementEarned {
|
||||
padding: 3px;
|
||||
background: #cb9a11;
|
||||
background: var(--eventAchievement);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_roleAssigned {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||
reactionAcceptance: null,
|
||||
renoteCount: 0,
|
||||
repliesCount: 1,
|
||||
reactionCount: 0,
|
||||
reactions: {},
|
||||
reactionEmojis: {},
|
||||
fileIds: [],
|
||||
|
|
|
@ -68,6 +68,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
|||
reactionAcceptance: null,
|
||||
renoteCount: 0,
|
||||
repliesCount: 1,
|
||||
reactionCount: 0,
|
||||
reactions: {},
|
||||
reactionEmojis: {},
|
||||
fileIds: [],
|
||||
|
|
|
@ -58,6 +58,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||
reactionAcceptance: null,
|
||||
renoteCount: 0,
|
||||
repliesCount: 1,
|
||||
reactionCount: 0,
|
||||
reactions: {},
|
||||
reactionEmojis: {},
|
||||
fileIds: ['0000000002'],
|
||||
|
|
|
@ -14,10 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
[$style.form_vertical]: chosen.place === 'vertical',
|
||||
}]"
|
||||
>
|
||||
<a :href="chosen.url" target="_blank" :class="$style.link">
|
||||
<component
|
||||
:is="self ? 'MkA' : 'a'"
|
||||
:class="$style.link"
|
||||
v-bind="self ? {
|
||||
to: chosen.url.substring(local.length),
|
||||
} : {
|
||||
href: chosen.url,
|
||||
rel: 'nofollow noopener',
|
||||
target: '_blank',
|
||||
}"
|
||||
>
|
||||
<img :src="chosen.imageUrl" :class="$style.img">
|
||||
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
|
||||
</a>
|
||||
</component>
|
||||
</div>
|
||||
<div v-else :class="$style.menu">
|
||||
<div :class="$style.menuContainer">
|
||||
|
@ -32,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@/config.js';
|
||||
import { url as local, host } from '@/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -96,6 +106,9 @@ const choseAd = (): Ad | null => {
|
|||
};
|
||||
|
||||
const chosen = ref(choseAd());
|
||||
|
||||
const self = computed(() => chosen.value?.url.startsWith(local));
|
||||
|
||||
const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
|
||||
|
||||
function reduceFrequency(): void {
|
||||
|
|
|
@ -373,7 +373,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||
this.currentRoute.value = res.route;
|
||||
this.currentKey = res.route.globalCacheKey ?? key ?? path;
|
||||
|
||||
if (emitChange) {
|
||||
if (emitChange && res.route.path !== '/:(*)') {
|
||||
this.emit('change', {
|
||||
beforePath,
|
||||
path,
|
||||
|
@ -408,13 +408,17 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||
if (cancel) return;
|
||||
}
|
||||
const res = this.navigate(path, null);
|
||||
this.emit('push', {
|
||||
beforePath,
|
||||
path: res._parsedRoute.fullPath,
|
||||
route: res.route,
|
||||
props: res.props,
|
||||
key: this.currentKey,
|
||||
});
|
||||
if (res.route.path === '/:(*)') {
|
||||
location.href = path;
|
||||
} else {
|
||||
this.emit('push', {
|
||||
beforePath,
|
||||
path: res._parsedRoute.fullPath,
|
||||
route: res.route,
|
||||
props: res.props,
|
||||
key: this.currentKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public replace(path: string, key?: string | null) {
|
||||
|
|
|
@ -324,6 +324,7 @@ const patrons = [
|
|||
'てば',
|
||||
'たっくん',
|
||||
'SHO SEKIGUCHI',
|
||||
'塩キャベツ',
|
||||
];
|
||||
|
||||
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
|
||||
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
|
||||
<MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch>
|
||||
<MkSwitch v-model="showReactionsCount">{{ i18n.ts.showReactionsCount }}</MkSwitch>
|
||||
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
|
||||
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
|
||||
<MkRadios v-model="reactionsDisplaySize">
|
||||
|
@ -281,6 +282,7 @@ const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
|
|||
const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
|
||||
const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
|
||||
const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
|
||||
const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
|
||||
const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
|
||||
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
|
||||
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
|
||||
|
|
|
@ -70,6 +70,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
|||
'animation',
|
||||
'animatedMfm',
|
||||
'advancedMfm',
|
||||
'showReactionsCount',
|
||||
'loadRawImages',
|
||||
'imageNewTab',
|
||||
'dataSaver',
|
||||
|
|
|
@ -35,6 +35,7 @@ export function useNoteCapture(props: {
|
|||
const currentCount = (note.value.reactions || {})[reaction] || 0;
|
||||
|
||||
note.value.reactions[reaction] = currentCount + 1;
|
||||
note.value.reactionCount += 1;
|
||||
|
||||
if ($i && (body.userId === $i.id)) {
|
||||
note.value.myReaction = reaction;
|
||||
|
@ -49,6 +50,7 @@ export function useNoteCapture(props: {
|
|||
const currentCount = (note.value.reactions || {})[reaction] || 0;
|
||||
|
||||
note.value.reactions[reaction] = Math.max(0, currentCount - 1);
|
||||
note.value.reactionCount = Math.max(0, note.value.reactionCount - 1);
|
||||
if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction];
|
||||
|
||||
if ($i && (body.userId === $i.id)) {
|
||||
|
|
|
@ -227,6 +227,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
showReactionsCount: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
enableQuickAddMfmFunction: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
|
@ -431,10 +435,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
sfxVolume: 1,
|
||||
},
|
||||
},
|
||||
hemisphere: {
|
||||
hemisphere: {
|
||||
where: 'device',
|
||||
default: hemisphere as 'N' | 'S',
|
||||
},
|
||||
},
|
||||
enableHorizontalSwipe: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
|
|
|
@ -22,6 +22,13 @@
|
|||
}
|
||||
|
||||
//--ad: rgb(255 169 0 / 10%);
|
||||
--eventFollow: #36aed2;
|
||||
--eventRenote: #36d298;
|
||||
--eventReply: #007aff;
|
||||
--eventReactionHeart: #dd2e44;
|
||||
--eventReaction: #e99a0b;
|
||||
--eventAchievement: #cb9a11;
|
||||
--eventOther: #88a6b7;
|
||||
}
|
||||
|
||||
::selection {
|
||||
|
|
|
@ -48,6 +48,9 @@ const devConfig = {
|
|||
},
|
||||
'/url': httpUrl,
|
||||
'/proxy': httpUrl,
|
||||
'/_info_card_': httpUrl,
|
||||
'/bios': httpUrl,
|
||||
'/cli': httpUrl,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
|
|
@ -3996,6 +3996,7 @@ export type components = {
|
|||
reactions: {
|
||||
[key: string]: number;
|
||||
};
|
||||
reactionCount: number;
|
||||
renoteCount: number;
|
||||
repliesCount: number;
|
||||
uri?: string;
|
||||
|
@ -10053,19 +10054,19 @@ export type operations = {
|
|||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
antennaId: string;
|
||||
name: string;
|
||||
name?: string;
|
||||
/** @enum {string} */
|
||||
src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
|
||||
src?: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
|
||||
/** Format: misskey:id */
|
||||
userListId?: string | null;
|
||||
keywords: string[][];
|
||||
excludeKeywords: string[][];
|
||||
users: string[];
|
||||
caseSensitive: boolean;
|
||||
keywords?: string[][];
|
||||
excludeKeywords?: string[][];
|
||||
users?: string[];
|
||||
caseSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
withReplies: boolean;
|
||||
withFile: boolean;
|
||||
notify: boolean;
|
||||
withReplies?: boolean;
|
||||
withFile?: boolean;
|
||||
notify?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue