Merge branch 'develop' into deps-update-openapits
This commit is contained in:
@@ -14,6 +14,8 @@
|
|||||||
- デフォルト値は「ローカルのコンテンツだけ公開」になっています
|
- デフォルト値は「ローカルのコンテンツだけ公開」になっています
|
||||||
- Feat: ロールでアップロード可能なファイル種別を設定可能になりました
|
- Feat: ロールでアップロード可能なファイル種別を設定可能になりました
|
||||||
- デフォルトは**テキスト、JSON、画像、動画、音声ファイル**になっています。zipなど、その他の種別のファイルは含まれていないため、必要に応じて設定を変更してください。
|
- デフォルトは**テキスト、JSON、画像、動画、音声ファイル**になっています。zipなど、その他の種別のファイルは含まれていないため、必要に応じて設定を変更してください。
|
||||||
|
- 場合によってはファイル種別を正しく検出できないことがあります(特にテキストフォーマット)。その場合、ファイル種別は application/octet-stream と見做されます。
|
||||||
|
- したがって、それらの種別不明ファイルを許可したい場合は application/octet-stream を指定に追加してください。
|
||||||
- Enhance: UIのアイコンデータの読み込みを軽量化
|
- Enhance: UIのアイコンデータの読み込みを軽量化
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
|||||||
Vendored
+4
@@ -7753,6 +7753,10 @@ export interface Locale extends ILocale {
|
|||||||
* MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)
|
* MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)
|
||||||
*/
|
*/
|
||||||
"uploadableFileTypes_caption": string;
|
"uploadableFileTypes_caption": string;
|
||||||
|
/**
|
||||||
|
* ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。
|
||||||
|
*/
|
||||||
|
"uploadableFileTypes_caption2": ParameterizedString<"x">;
|
||||||
};
|
};
|
||||||
"_condition": {
|
"_condition": {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2007,6 +2007,7 @@ _role:
|
|||||||
chatAvailability: "チャットを許可"
|
chatAvailability: "チャットを許可"
|
||||||
uploadableFileTypes: "アップロード可能なファイル種別"
|
uploadableFileTypes: "アップロード可能なファイル種別"
|
||||||
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
|
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
|
||||||
|
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
|
||||||
_condition:
|
_condition:
|
||||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||||
isLocal: "ローカルユーザー"
|
isLocal: "ローカルユーザー"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.5.1-beta.0",
|
"version": "2025.5.1-beta.2",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -469,13 +469,14 @@ export class DriveService {
|
|||||||
if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
|
if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
|
||||||
|
|
||||||
const info = await this.fileInfoService.getFileInfo(path, {
|
const info = await this.fileInfoService.getFileInfo(path, {
|
||||||
|
fileName: name,
|
||||||
skipSensitiveDetection: skipNsfwCheck,
|
skipSensitiveDetection: skipNsfwCheck,
|
||||||
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
|
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
|
||||||
this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
||||||
this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
||||||
this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
||||||
this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
||||||
0.5,
|
0.5,
|
||||||
sensitiveThresholdForPorn: 0.75,
|
sensitiveThresholdForPorn: 0.75,
|
||||||
enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
|
enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export class FileInfoService {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getFileInfo(path: string, opts: {
|
public async getFileInfo(path: string, opts: {
|
||||||
|
fileName?: string | null;
|
||||||
skipSensitiveDetection: boolean;
|
skipSensitiveDetection: boolean;
|
||||||
sensitiveThreshold?: number;
|
sensitiveThreshold?: number;
|
||||||
sensitiveThresholdForPorn?: number;
|
sensitiveThresholdForPorn?: number;
|
||||||
@@ -76,6 +77,26 @@ export class FileInfoService {
|
|||||||
|
|
||||||
let type = await this.detectType(path);
|
let type = await this.detectType(path);
|
||||||
|
|
||||||
|
if (type.mime === TYPE_OCTET_STREAM.mime && opts.fileName != null) {
|
||||||
|
const ext = opts.fileName.split('.').pop();
|
||||||
|
if (ext === 'txt') {
|
||||||
|
type = {
|
||||||
|
mime: 'text/plain',
|
||||||
|
ext: 'txt',
|
||||||
|
};
|
||||||
|
} else if (ext === 'csv') {
|
||||||
|
type = {
|
||||||
|
mime: 'text/csv',
|
||||||
|
ext: 'csv',
|
||||||
|
};
|
||||||
|
} else if (ext === 'json') {
|
||||||
|
type = {
|
||||||
|
mime: 'application/json',
|
||||||
|
ext: 'json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// image dimensions
|
// image dimensions
|
||||||
let width: number | undefined;
|
let width: number | undefined;
|
||||||
let height: number | undefined;
|
let height: number | undefined;
|
||||||
@@ -438,12 +459,12 @@ export class FileInfoService {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async detectImageSize(path: string): Promise<{
|
private async detectImageSize(path: string): Promise<{
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
wUnits: string;
|
wUnits: string;
|
||||||
hUnits: string;
|
hUnits: string;
|
||||||
orientation?: number;
|
orientation?: number;
|
||||||
}> {
|
}> {
|
||||||
const readable = fs.createReadStream(path);
|
const readable = fs.createReadStream(path);
|
||||||
const imageSize = await probeImageSize(readable);
|
const imageSize = await probeImageSize(readable);
|
||||||
readable.destroy();
|
readable.destroy();
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ export const meta = {
|
|||||||
id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a',
|
id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a',
|
||||||
httpStatusCode: 413,
|
httpStatusCode: 413,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
unallowedFileType: {
|
||||||
|
message: 'Cannot upload the file because it is an unallowed file type.',
|
||||||
|
code: 'UNALLOWED_FILE_TYPE',
|
||||||
|
id: '4becd248-7f2c-48c4-a9f0-75edc4f9a1ea',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -123,6 +129,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
|
if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
|
||||||
if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace);
|
if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace);
|
||||||
if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded);
|
if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded);
|
||||||
|
if (err.id === 'bd71c601-f9b0-4808-9137-a330647ced9b') throw new ApiError(meta.errors.unallowedFileType);
|
||||||
}
|
}
|
||||||
throw new ApiError();
|
throw new ApiError();
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -19,7 +19,26 @@ export const meta = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
reactions: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reactionEmojis: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, ref, useTemplateRef, watch, provide, shallowRef, reactive } from 'vue';
|
import { computed, inject, onMounted, ref, useTemplateRef, provide } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
@@ -283,12 +283,10 @@ if (noteViewInterruptors.length > 0) {
|
|||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
const appearNote = getAppearNote(note);
|
const appearNote = getAppearNote(note);
|
||||||
const $appearNote = reactive({
|
const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({
|
||||||
reactions: appearNote.reactions,
|
note: appearNote,
|
||||||
reactionCount: appearNote.reactionCount,
|
parentNote: note,
|
||||||
reactionEmojis: appearNote.reactionEmojis,
|
mock: props.mock,
|
||||||
myReaction: appearNote.myReaction,
|
|
||||||
pollChoices: appearNote.poll?.choices,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
@@ -410,17 +408,6 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let subscribeManuallyToNoteCapture: () => void = () => { };
|
|
||||||
|
|
||||||
if (!props.mock) {
|
|
||||||
const { subscribe } = useNoteCapture({
|
|
||||||
note: appearNote,
|
|
||||||
parentNote: note,
|
|
||||||
$note: $appearNote,
|
|
||||||
});
|
|
||||||
subscribeManuallyToNoteCapture = subscribe;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.mock) {
|
if (!props.mock) {
|
||||||
useTooltip(renoteButton, async (showing) => {
|
useTooltip(renoteButton, async (showing) => {
|
||||||
const renotes = await misskeyApi('notes/renotes', {
|
const renotes = await misskeyApi('notes/renotes', {
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, provide, reactive, ref, useTemplateRef } from 'vue';
|
import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
@@ -304,12 +304,9 @@ if (noteViewInterruptors.length > 0) {
|
|||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
const appearNote = getAppearNote(note);
|
const appearNote = getAppearNote(note);
|
||||||
const $appearNote = reactive({
|
const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({
|
||||||
reactions: appearNote.reactions,
|
note: appearNote,
|
||||||
reactionCount: appearNote.reactionCount,
|
parentNote: note,
|
||||||
reactionEmojis: appearNote.reactionEmojis,
|
|
||||||
myReaction: appearNote.myReaction,
|
|
||||||
pollChoices: appearNote.poll?.choices,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
@@ -397,12 +394,6 @@ const reactionsPagination = computed(() => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({
|
|
||||||
note: appearNote,
|
|
||||||
parentNote: note,
|
|
||||||
$note: $appearNote,
|
|
||||||
});
|
|
||||||
|
|
||||||
useTooltip(renoteButton, async (showing) => {
|
useTooltip(renoteButton, async (showing) => {
|
||||||
const renotes = await misskeyApi('notes/renotes', {
|
const renotes = await misskeyApi('notes/renotes', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<div>{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}</div>
|
<div>{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}</div>
|
||||||
<div>{{ i18n.ts._uploader.allowedTypes }}: {{ $i.policies.uploadableFileTypes.join(', ') }}</div>
|
|
||||||
|
<!-- クライアントで検出するMIME typeとサーバーで検出するMIME typeが異なる場合があり、混乱の元になるのでとりあえず隠しとく -->
|
||||||
|
<!-- https://github.com/misskey-dev/misskey/issues/16091 -->
|
||||||
|
<!--<div>{{ i18n.ts._uploader.allowedTypes }}: {{ $i.policies.uploadableFileTypes.join(', ') }}</div>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { onUnmounted } from 'vue';
|
import { onUnmounted, reactive } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import type { Reactive, Ref } from 'vue';
|
import type { Reactive } from 'vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
@@ -179,60 +179,83 @@ function realtimeSubscribe(props: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReactiveNoteData = Reactive<{
|
export type ReactiveNoteData = {
|
||||||
reactions: Misskey.entities.Note['reactions'];
|
reactions: Misskey.entities.Note['reactions'];
|
||||||
reactionCount: Misskey.entities.Note['reactionCount'];
|
reactionCount: Misskey.entities.Note['reactionCount'];
|
||||||
reactionEmojis: Misskey.entities.Note['reactionEmojis'];
|
reactionEmojis: Misskey.entities.Note['reactionEmojis'];
|
||||||
myReaction: Misskey.entities.Note['myReaction'];
|
myReaction: Misskey.entities.Note['myReaction'];
|
||||||
pollChoices: NonNullable<Misskey.entities.Note['poll']>['choices'];
|
pollChoices: NonNullable<Misskey.entities.Note['poll']>['choices'];
|
||||||
}>;
|
};
|
||||||
|
|
||||||
|
const noReaction = Symbol();
|
||||||
|
|
||||||
export function useNoteCapture(props: {
|
export function useNoteCapture(props: {
|
||||||
note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>;
|
note: Misskey.entities.Note;
|
||||||
parentNote: Misskey.entities.Note | null;
|
parentNote: Misskey.entities.Note | null;
|
||||||
$note: ReactiveNoteData;
|
mock?: boolean;
|
||||||
}): {
|
}): {
|
||||||
|
$note: Reactive<ReactiveNoteData>;
|
||||||
subscribe: () => void;
|
subscribe: () => void;
|
||||||
} {
|
} {
|
||||||
const { note, parentNote, $note } = props;
|
const { note, parentNote, mock } = props;
|
||||||
|
|
||||||
|
const $note = reactive<ReactiveNoteData>({
|
||||||
|
reactions: Object.entries(note.reactions).reduce((acc, [name, count]) => {
|
||||||
|
// Normalize reactions
|
||||||
|
const normalizedName = name.replace(/^:(\w+):$/, ':$1@.:');
|
||||||
|
if (acc[normalizedName] == null) {
|
||||||
|
acc[normalizedName] = count;
|
||||||
|
} else {
|
||||||
|
acc[normalizedName] += count;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Misskey.entities.Note['reactions']),
|
||||||
|
reactionCount: note.reactionCount,
|
||||||
|
reactionEmojis: note.reactionEmojis,
|
||||||
|
myReaction: note.myReaction,
|
||||||
|
pollChoices: note.poll?.choices ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
noteEvents.on(`reacted:${note.id}`, onReacted);
|
noteEvents.on(`reacted:${note.id}`, onReacted);
|
||||||
noteEvents.on(`unreacted:${note.id}`, onUnreacted);
|
noteEvents.on(`unreacted:${note.id}`, onUnreacted);
|
||||||
noteEvents.on(`pollVoted:${note.id}`, onPollVoted);
|
noteEvents.on(`pollVoted:${note.id}`, onPollVoted);
|
||||||
|
|
||||||
let latestReactedKey: string | null = null;
|
// 操作がダブっていないかどうかを簡易的に記録するためのMap
|
||||||
let latestUnreactedKey: string | null = null;
|
const reactionUserMap = new Map<Misskey.entities.User['id'], string | typeof noReaction>();
|
||||||
let latestPollVotedKey: string | null = null;
|
let latestPollVotedKey: string | null = null;
|
||||||
|
|
||||||
function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
|
function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
|
||||||
const newReactedKey = `${ctx.userId}:${ctx.reaction}`;
|
const normalizedName = ctx.reaction.replace(/^:(\w+):$/, ':$1@.:');
|
||||||
if (newReactedKey === latestReactedKey) return;
|
|
||||||
latestReactedKey = newReactedKey;
|
if (reactionUserMap.has(ctx.userId) && reactionUserMap.get(ctx.userId) === normalizedName) return;
|
||||||
|
reactionUserMap.set(ctx.userId, normalizedName);
|
||||||
|
|
||||||
if (ctx.emoji && !(ctx.emoji.name in $note.reactionEmojis)) {
|
if (ctx.emoji && !(ctx.emoji.name in $note.reactionEmojis)) {
|
||||||
$note.reactionEmojis[ctx.emoji.name] = ctx.emoji.url;
|
$note.reactionEmojis[ctx.emoji.name] = ctx.emoji.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCount = $note.reactions[ctx.reaction] || 0;
|
const currentCount = $note.reactions[normalizedName] || 0;
|
||||||
|
|
||||||
$note.reactions[ctx.reaction] = currentCount + 1;
|
$note.reactions[normalizedName] = currentCount + 1;
|
||||||
$note.reactionCount += 1;
|
$note.reactionCount += 1;
|
||||||
|
|
||||||
if ($i && (ctx.userId === $i.id)) {
|
if ($i && (ctx.userId === $i.id)) {
|
||||||
$note.myReaction = ctx.reaction;
|
$note.myReaction = normalizedName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUnreacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
|
function onUnreacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
|
||||||
const newUnreactedKey = `${ctx.userId}:${ctx.reaction}`;
|
const normalizedName = ctx.reaction.replace(/^:(\w+):$/, ':$1@.:');
|
||||||
if (newUnreactedKey === latestUnreactedKey) return;
|
|
||||||
latestUnreactedKey = newUnreactedKey;
|
|
||||||
|
|
||||||
const currentCount = $note.reactions[ctx.reaction] || 0;
|
// 確実に一度リアクションされて取り消されている場合のみ処理をとめる(APIで初回読み込み→Streamでアップデート等の場合、reactionUserMapに情報がないため)
|
||||||
|
if (reactionUserMap.has(ctx.userId) && reactionUserMap.get(ctx.userId) === noReaction) return;
|
||||||
|
reactionUserMap.set(ctx.userId, noReaction);
|
||||||
|
|
||||||
$note.reactions[ctx.reaction] = Math.max(0, currentCount - 1);
|
const currentCount = $note.reactions[normalizedName] || 0;
|
||||||
|
|
||||||
|
$note.reactions[normalizedName] = Math.max(0, currentCount - 1);
|
||||||
$note.reactionCount = Math.max(0, $note.reactionCount - 1);
|
$note.reactionCount = Math.max(0, $note.reactionCount - 1);
|
||||||
if ($note.reactions[ctx.reaction] === 0) delete $note.reactions[ctx.reaction];
|
if ($note.reactions[normalizedName] === 0) delete $note.reactions[normalizedName];
|
||||||
|
|
||||||
if ($i && (ctx.userId === $i.id)) {
|
if ($i && (ctx.userId === $i.id)) {
|
||||||
$note.myReaction = null;
|
$note.myReaction = null;
|
||||||
@@ -257,10 +280,20 @@ export function useNoteCapture(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function subscribe() {
|
function subscribe() {
|
||||||
|
if (mock) {
|
||||||
|
// モックモードでは購読しない
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($i && store.s.realtimeMode) {
|
if ($i && store.s.realtimeMode) {
|
||||||
realtimeSubscribe(props);
|
realtimeSubscribe({
|
||||||
|
note,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
pollingSubscribe(props);
|
pollingSubscribe({
|
||||||
|
note,
|
||||||
|
$note,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +310,7 @@ export function useNoteCapture(props: {
|
|||||||
if ((Date.now() - new Date(note.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
|
if ((Date.now() - new Date(note.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
|
||||||
// リノートで表示されているノートでもないし、投稿からある程度経過しているので自動で購読しない
|
// リノートで表示されているノートでもないし、投稿からある程度経過しているので自動で購読しない
|
||||||
return {
|
return {
|
||||||
|
$note,
|
||||||
subscribe: () => {
|
subscribe: () => {
|
||||||
subscribe();
|
subscribe();
|
||||||
},
|
},
|
||||||
@@ -286,6 +320,7 @@ export function useNoteCapture(props: {
|
|||||||
if ((Date.now() - new Date(parentNote.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
|
if ((Date.now() - new Date(parentNote.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
|
||||||
// リノートで表示されているノートだが、リノートされてからある程度経過しているので自動で購読しない
|
// リノートで表示されているノートだが、リノートされてからある程度経過しているので自動で購読しない
|
||||||
return {
|
return {
|
||||||
|
$note,
|
||||||
subscribe: () => {
|
subscribe: () => {
|
||||||
subscribe();
|
subscribe();
|
||||||
},
|
},
|
||||||
@@ -296,6 +331,7 @@ export function useNoteCapture(props: {
|
|||||||
subscribe();
|
subscribe();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
$note,
|
||||||
subscribe: () => {
|
subscribe: () => {
|
||||||
// すでに購読しているので何もしない
|
// すでに購読しているので何もしない
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -418,7 +418,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkTextarea :modelValue="role.policies.uploadableFileTypes.value.join('\n')" :disabled="role.policies.uploadableFileTypes.useDefault" :readonly="readonly" @update:modelValue="role.policies.uploadableFileTypes.value = $event.split('\n')">
|
<MkTextarea :modelValue="role.policies.uploadableFileTypes.value.join('\n')" :disabled="role.policies.uploadableFileTypes.useDefault" :readonly="readonly" @update:modelValue="role.policies.uploadableFileTypes.value = $event.split('\n')">
|
||||||
<template #caption>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</template>
|
<template #caption>
|
||||||
|
<div>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</div>
|
||||||
|
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}</div>
|
||||||
|
</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<MkRange v-model="role.policies.uploadableFileTypes.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
<MkRange v-model="role.policies.uploadableFileTypes.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template>
|
<template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template>
|
||||||
<template #suffix>...</template>
|
<template #suffix>...</template>
|
||||||
<MkTextarea :modelValue="policies.uploadableFileTypes.join('\n')">
|
<MkTextarea :modelValue="policies.uploadableFileTypes.join('\n')">
|
||||||
|
<template #caption>
|
||||||
|
<div>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</div>
|
||||||
|
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}</div>
|
||||||
|
</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
|||||||
@@ -101,27 +101,15 @@ html._themeChanging_ {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html::view-transition-new(theme-changing) {
|
html::view-transition-new(theme-changing) {
|
||||||
z-index: 4000001;
|
z-index: 4000000;
|
||||||
animation: themeChangingNew 0.5s ease;
|
|
||||||
animation-fill-mode: forwards;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html::view-transition-old(theme-changing) {
|
html::view-transition-old(theme-changing) {
|
||||||
z-index: 4000000;
|
z-index: 4000001;
|
||||||
animation: themeChangingOld 0.5s ease;
|
animation: themeChangingOld 0.5s ease;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes themeChangingNew {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes themeChangingOld {
|
@keyframes themeChangingOld {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { globalEvents } from '@/events.js';
|
|||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
import { deepEqual } from '@/utility/deep-equal.js';
|
||||||
|
|
||||||
export type Theme = {
|
export type Theme = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -127,6 +128,7 @@ function applyThemeInternal(theme: Theme, persist: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let timeout: number | null = null;
|
let timeout: number | null = null;
|
||||||
|
let currentTheme: Theme | null = null;
|
||||||
|
|
||||||
export function applyTheme(theme: Theme, persist = true) {
|
export function applyTheme(theme: Theme, persist = true) {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
@@ -134,6 +136,9 @@ export function applyTheme(theme: Theme, persist = true) {
|
|||||||
timeout = null;
|
timeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deepEqual(currentTheme, theme)) return;
|
||||||
|
currentTheme = theme;
|
||||||
|
|
||||||
if (window.document.startViewTransition != null && prefer.s.animation) {
|
if (window.document.startViewTransition != null && prefer.s.animation) {
|
||||||
window.document.documentElement.classList.add('_themeChanging_');
|
window.document.documentElement.classList.add('_themeChanging_');
|
||||||
window.document.startViewTransition(async () => {
|
window.document.startViewTransition(async () => {
|
||||||
|
|||||||
@@ -39,20 +39,22 @@ export function uploadFile(file: File | Blob, options: {
|
|||||||
const filePromise = new Promise<Misskey.entities.DriveFile>((resolve, reject) => {
|
const filePromise = new Promise<Misskey.entities.DriveFile>((resolve, reject) => {
|
||||||
if ($i == null) return reject();
|
if ($i == null) return reject();
|
||||||
|
|
||||||
const allowedMimeTypes = $i.policies.uploadableFileTypes;
|
// こっち側で検出するMIME typeとサーバーで検出するMIME typeは異なる場合があるため、こっち側ではやらないことにする
|
||||||
const isAllowedMimeType = allowedMimeTypes.some(mimeType => {
|
// https://github.com/misskey-dev/misskey/issues/16091
|
||||||
if (mimeType === '*' || mimeType === '*/*') return true;
|
//const allowedMimeTypes = $i.policies.uploadableFileTypes;
|
||||||
if (mimeType.endsWith('/*')) return file.type.startsWith(mimeType.slice(0, -1));
|
//const isAllowedMimeType = allowedMimeTypes.some(mimeType => {
|
||||||
return file.type === mimeType;
|
// if (mimeType === '*' || mimeType === '*/*') return true;
|
||||||
});
|
// if (mimeType.endsWith('/*')) return file.type.startsWith(mimeType.slice(0, -1));
|
||||||
if (!isAllowedMimeType) {
|
// return file.type === mimeType;
|
||||||
os.alert({
|
//});
|
||||||
type: 'error',
|
//if (!isAllowedMimeType) {
|
||||||
title: i18n.ts.failedToUpload,
|
// os.alert({
|
||||||
text: i18n.ts.cannotUploadBecauseUnallowedFileType,
|
// type: 'error',
|
||||||
});
|
// title: i18n.ts.failedToUpload,
|
||||||
return reject();
|
// text: i18n.ts.cannotUploadBecauseUnallowedFileType,
|
||||||
}
|
// });
|
||||||
|
// return reject();
|
||||||
|
//}
|
||||||
|
|
||||||
if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
|
if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
|
||||||
os.alert({
|
os.alert({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2025.5.1-beta.0",
|
"version": "2025.5.1-beta.2",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
|||||||
@@ -29772,7 +29772,15 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
'application/json': Record<string, never>[];
|
'application/json': {
|
||||||
|
id: string;
|
||||||
|
reactions: {
|
||||||
|
[key: string]: number;
|
||||||
|
};
|
||||||
|
reactionEmojis: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Client error */
|
/** @description Client error */
|
||||||
|
|||||||
Reference in New Issue
Block a user