This commit is contained in:
syuilo 2025-08-26 13:34:41 +09:00
parent eb9915baf8
commit d6a1046361
41 changed files with 289 additions and 140 deletions

View File

@ -49,15 +49,12 @@ export class NoteReactionEntityService implements OnModuleInit {
public async pack(
src: MiNoteReaction['id'] | MiNoteReaction,
me?: { id: MiUser['id'] } | null | undefined,
options?: {
withNote: boolean;
},
options?: object,
hints?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({
withNote: false,
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
@ -67,9 +64,6 @@ export class NoteReactionEntityService implements OnModuleInit {
createdAt: this.idService.parse(reaction.id).date.toISOString(),
user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction),
...(opts.withNote ? {
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
} : {}),
};
}
@ -77,16 +71,50 @@ export class NoteReactionEntityService implements OnModuleInit {
public async packMany(
reactions: MiNoteReaction[],
me?: { id: MiUser['id'] } | null | undefined,
options?: {
withNote: boolean;
},
options?: object,
): Promise<Packed<'NoteReaction'>[]> {
const opts = Object.assign({
withNote: false,
}, options);
const _users = reactions.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
}
@bindThis
public async packWithNote(
src: MiNoteReaction['id'] | MiNoteReaction,
me?: { id: MiUser['id'] } | null | undefined,
options?: object,
hints?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReactionWithNote'>> {
const opts = Object.assign({
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
return {
id: reaction.id,
createdAt: this.idService.parse(reaction.id).date.toISOString(),
user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction),
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
};
}
@bindThis
public async packManyWithNote(
reactions: MiNoteReaction[],
me?: { id: MiUser['id'] } | null | undefined,
options?: object,
): Promise<Packed<'NoteReactionWithNote'>[]> {
const opts = Object.assign({
}, options);
const _users = reactions.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reactions.map(reaction => this.packWithNote(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
}
}

View File

@ -22,7 +22,7 @@ import { packedFollowingSchema } from '@/models/json-schema/following.js';
import { packedMutingSchema } from '@/models/json-schema/muting.js';
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedNoteReactionSchema, packedNoteReactionWithNoteSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
@ -92,6 +92,7 @@ export const refs = {
Note: packedNoteSchema,
NoteDraft: packedNoteDraftSchema,
NoteReaction: packedNoteReactionSchema,
NoteReactionWithNote: packedNoteReactionWithNoteSchema,
NoteFavorite: packedNoteFavoriteSchema,
Notification: packedNotificationSchema,
DriveFile: packedDriveFileSchema,

View File

@ -10,7 +10,6 @@ export const packedNoteReactionSchema = {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string',
@ -28,3 +27,33 @@ export const packedNoteReactionSchema = {
},
},
} as const;
export const packedNoteReactionWithNoteSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
user: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
type: {
type: 'string',
optional: false, nullable: false,
},
note: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
} as const;

View File

@ -49,6 +49,34 @@ export const meta = {
type: 'string',
optional: false, nullable: false,
},
icon: {
type: 'string',
optional: false, nullable: true,
},
display: {
type: 'string',
optional: false, nullable: false,
},
isActive: {
type: 'boolean',
optional: false, nullable: false,
},
forExistingUsers: {
type: 'boolean',
optional: false, nullable: false,
},
silence: {
type: 'boolean',
optional: false, nullable: false,
},
needConfirmationToRead: {
type: 'boolean',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
imageUrl: {
type: 'string',
optional: false, nullable: true,

View File

@ -23,6 +23,16 @@ export const meta = {
type: 'object',
optional: false, nullable: false,
ref: 'UserList',
properties: {
likedCount: {
type: 'number',
optional: true, nullable: false,
},
isLiked: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
errors: {

View File

@ -28,7 +28,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
ref: 'NoteReaction',
ref: 'NoteReactionWithNote',
},
},
@ -120,7 +120,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return true;
});
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
return await this.noteReactionEntityService.packManyWithNote(reactions, me);
});
}
}

View File

@ -368,11 +368,6 @@ export async function mainBoot() {
});
});
main.on('unreadAntenna', () => {
updateCurrentAccountPartial({ hasUnreadAntenna: true });
sound.playMisskeySfx('antenna');
});
main.on('newChatMessage', () => {
updateCurrentAccountPartial({ hasUnreadChatMessages: true });
sound.playMisskeySfx('chatMessage');

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum',
}]"
>
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg ?? '' }">
<img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div>
</div>

View File

@ -589,7 +589,10 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
};
const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return {
series: [{
name: 'In',
@ -611,7 +614,10 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
};
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Users',
@ -626,7 +632,10 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
};
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Notes',
@ -641,7 +650,10 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
};
const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Following',
@ -664,7 +676,10 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
};
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return {
bytes: true,
series: [{
@ -680,7 +695,10 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
};
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
const host = props.args?.host;
if (host == null) return { series: [] };
const raw = await misskeyApiGet('charts/instance', { host: host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Drive files',
@ -695,7 +713,10 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
};
const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/notes', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/notes', { userId: userId, limit: props.limit, span: props.span });
return {
series: [...(props.args?.withoutAll ? [] : [{
name: 'All',
@ -727,7 +748,10 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
};
const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/pv', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/pv', { userId: userId, limit: props.limit, span: props.span });
return {
series: [{
name: 'Unique PV (user)',
@ -754,7 +778,10 @@ const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
};
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/following', { userId: userId, limit: props.limit, span: props.span });
return {
series: [{
name: 'Local',
@ -769,7 +796,10 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
};
const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/following', { userId: userId, limit: props.limit, span: props.span });
return {
series: [{
name: 'Local',
@ -784,7 +814,10 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
};
const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
const raw = await misskeyApiGet('charts/user/drive', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
const userId = props.args?.user?.id;
if (userId == null) return { series: [] };
const raw = await misskeyApiGet('charts/user/drive', { userId: userId, limit: props.limit, span: props.span });
return {
bytes: true,
series: [{

View File

@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, useTemplateRef, ref } from 'vue';
import { onMounted, useTemplateRef, ref, onUnmounted } from 'vue';
import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
@ -55,17 +55,19 @@ const imgEl = useTemplateRef('imgEl');
let cropper: Cropper | null = null;
const loading = ref(true);
const ok = async () => {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
const croppedImage = await cropper?.getCropperImage();
const croppedSection = await cropper?.getCropperSelection();
async function ok() {
const promise = new Promise<Blob>(async (res) => {
if (cropper == null) throw new Error('Cropper is not initialized');
const croppedImage = await cropper.getCropperImage()!;
const croppedSection = await cropper.getCropperSelection()!;
// ()
const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth;
const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate;
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
croppedCanvas?.toBlob(blob => {
const croppedCanvas = await croppedSection.$toCanvas({ width: widthToRender });
croppedCanvas.toBlob(blob => {
if (!blob) return;
res(blob);
});
@ -74,25 +76,27 @@ const ok = async () => {
const f = await promise;
emit('ok', f);
dialogEl.value!.close();
};
if (dialogEl.value != null) dialogEl.value.close();
}
const cancel = () => {
function cancel() {
emit('cancel');
dialogEl.value!.close();
};
if (dialogEl.value != null) dialogEl.value.close();
}
const onImageLoad = () => {
function onImageLoad() {
loading.value = false;
if (cropper) {
cropper.getCropperImage()!.$center('contain');
cropper.getCropperSelection()!.$center();
}
};
}
onMounted(() => {
cropper = new Cropper(imgEl.value!, {
if (imgEl.value == null) return; // TS
cropper = new Cropper(imgEl.value, {
});
const computedStyle = getComputedStyle(window.document.documentElement);
@ -104,16 +108,22 @@ onMounted(() => {
selection.outlined = true;
window.setTimeout(() => {
cropper!.getCropperImage()!.$center('contain');
if (cropper == null) return;
cropper.getCropperImage()!.$center('contain');
selection.$center();
}, 100);
// 調
window.setTimeout(() => {
cropper!.getCropperImage()!.$center('contain');
if (cropper == null) return;
cropper.getCropperImage()!.$center('contain');
selection.$center();
}, 500);
});
onUnmounted(() => {
URL.revokeObjectURL(imgUrl);
});
</script>
<style lang="scss" scoped>

View File

@ -152,7 +152,7 @@ const props = withDefaults(defineProps<{
asDrawer?: boolean;
asWindow?: boolean;
asReactionPicker?: boolean; // 使使
targetNote?: Misskey.entities.Note;
targetNote?: Misskey.entities.Note | null;
}>(), {
showPinned: true,
});

View File

@ -44,11 +44,11 @@ import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
anchorElement?: HTMLElement;
anchorElement?: HTMLElement | null;
showPinned?: boolean;
pinnedEmojis?: string[],
asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note;
targetNote?: Misskey.entities.Note | null;
choseAndClose?: boolean;
}>(), {
manualShowing: null,

View File

@ -91,7 +91,7 @@ const emit = defineEmits<{
(ev: 'opened'): void;
(ev: 'click'): void;
(ev: 'esc'): void;
(ev: 'close'): void;
(ev: 'close'): void; // TODO: (refactor) closing
(ev: 'closed'): void;
}>();
@ -148,7 +148,6 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
useSendAnime.value = true;
}
// eslint-disable-next-line vue/no-mutating-props
if (props.anchorElement) props.anchorElement.style.pointerEvents = 'auto';
showing.value = false;
emit('close');
@ -319,7 +318,6 @@ const alignObserver = new ResizeObserver((entries, observer) => {
onMounted(() => {
watch(() => props.anchorElement, async () => {
if (props.anchorElement) {
// eslint-disable-next-line vue/no-mutating-props
props.anchorElement.style.pointerEvents = 'none';
}
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.anchorElement) != null);

View File

@ -58,18 +58,22 @@ const emit = defineEmits<{
const buttonEl = useTemplateRef('buttonEl');
const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
const canToggle = computed(() => {
const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction);
// TODO
//return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
return !props.reaction.match(/@\w/) && $i && emoji.value;
//return !props.reaction.match(/@\w/) && $i && emoji && checkReactionPermissions($i, props.note, emoji);
return !props.reaction.match(/@\w/) && $i && emoji;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.');
async function toggleReaction() {
if (!canToggle.value) return;
if ($i == null) return;
const me = $i;
const oldReaction = props.myReaction;
if (oldReaction) {
@ -93,7 +97,7 @@ async function toggleReaction() {
noteId: props.noteId,
}).then(() => {
noteEvents.emit(`unreacted:${props.noteId}`, {
userId: $i!.id,
userId: me.id,
reaction: oldReaction,
});
if (oldReaction !== props.reaction) {
@ -101,10 +105,12 @@ async function toggleReaction() {
noteId: props.noteId,
reaction: props.reaction,
}).then(() => {
const emoji = customEmojisMap.get(emojiName.value);
if (emoji == null) return;
noteEvents.emit(`reacted:${props.noteId}`, {
userId: $i!.id,
userId: me.id,
reaction: props.reaction,
emoji: emoji.value,
emoji: emoji,
});
});
}
@ -131,10 +137,13 @@ async function toggleReaction() {
noteId: props.noteId,
reaction: props.reaction,
}).then(() => {
const emoji = customEmojisMap.get(emojiName.value);
if (emoji == null) return;
noteEvents.emit(`reacted:${props.noteId}`, {
userId: $i!.id,
userId: me.id,
reaction: props.reaction,
emoji: emoji.value,
emoji: emoji,
});
});
// TODO:
@ -217,6 +226,8 @@ onMounted(() => {
if (!mock) {
useTooltip(buttonEl, async (showing) => {
if (buttonEl.value == null) return;
const reactions = await misskeyApiGet('notes/reactions', {
noteId: props.noteId,
type: props.reaction,

View File

@ -59,7 +59,7 @@ import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
tabs?: Tab[];
tab?: string;
rootEl?: HTMLElement;
rootEl?: HTMLElement | null;
}>(), {
tabs: () => ([] as Tab[]),
});

View File

@ -143,8 +143,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
</div>
<div v-if="expandedRoleIds.includes(role.id)" :class="$style.roleItemSub">
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id)!.createdAt" mode="detail"/></div>
<div v-if="info.roleAssigns.find(a => a.roleId === role.id)!.expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id)!.expiresAt!).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div>
</div>

View File

@ -119,7 +119,7 @@ async function _fetch_() {
}
function postThis() {
if (!file.value) return;
if (file.value == null) return;
os.post({
initialFiles: [file.value],
@ -127,11 +127,13 @@ function postThis() {
}
function move() {
if (!file.value) return;
if (file.value == null) return;
const f = file.value;
selectDriveFolder(null).then(folder => {
misskeyApi('drive/files/update', {
fileId: file.value.id,
fileId: f.id,
folderId: folder[0] ? folder[0].id : null,
}).then(async () => {
await _fetch_();
@ -140,7 +142,7 @@ function move() {
}
function toggleSensitive() {
if (!file.value) return;
if (file.value == null) return;
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
@ -157,7 +159,9 @@ function toggleSensitive() {
}
function rename() {
if (!file.value) return;
if (file.value == null) return;
const f = file.value;
os.inputText({
title: i18n.ts.renameFile,
@ -166,7 +170,7 @@ function rename() {
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
fileId: f.id,
name: name,
}).then(async () => {
await _fetch_();
@ -175,7 +179,9 @@ function rename() {
}
async function describe() {
if (!file.value) return;
if (file.value == null) return;
const f = file.value;
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkFileCaptionEditWindow.vue').then(x => x.default), {
default: file.value.comment ?? '',
@ -183,7 +189,7 @@ async function describe() {
}, {
done: caption => {
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
fileId: f.id,
comment: caption.length === 0 ? null : caption,
}).then(async () => {
await _fetch_();
@ -194,7 +200,7 @@ async function describe() {
}
async function deleteFile() {
if (!file.value) return;
if (file.value == null) return;
const { canceled } = await os.confirm({
type: 'warning',

View File

@ -26,18 +26,12 @@ import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
tag?: string;
initialTab?: string;
}>(), {
initialTab: 'featured',
});
const tab = ref(props.initialTab);
const tagsEl = useTemplateRef('tagsEl');
watch(() => props.tag, () => {
if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
});
const headerActions = computed(() => []);

View File

@ -103,6 +103,7 @@ definePage(() => ({
icon: 'ti ti-list',
}));
</script>
<style lang="scss" module>
.userItem {
display: flex;

View File

@ -126,7 +126,7 @@ function fetchNote() {
noteId: props.noteId,
}).then(res => {
note.value = res;
const appearNote = getAppearNote(res);
const appearNote = getAppearNote(res) ?? res;
// 2023-10-01notes/clips
if ((appearNote.clippedCount ?? 0) > 0 || new Date(appearNote.createdAt).getTime() < new Date('2023-10-01').getTime()) {
misskeyApi('notes/clips', {

View File

@ -62,10 +62,10 @@ const props = defineProps<{
}>();
const scope = computed(() => props.path.split('/').slice(0, -1));
const key = computed(() => props.path.split('/').at(-1));
const key = computed(() => props.path.split('/').at(-1)!);
const value = ref<any>(null);
const valueForEditor = ref<string | null>(null);
const valueForEditor = ref<string>('');
function fetchValue() {
misskeyApi('i/registry/get-detail', {

View File

@ -108,7 +108,7 @@ async function openDecoration(avatarDecoration: {
offsetY: payload.offsetY,
};
const update = [...$i.avatarDecorations];
update[index] = decoration;
update[index!] = decoration;
await os.apiWithDialog('i/update', {
avatarDecorations: update,
});
@ -116,7 +116,7 @@ async function openDecoration(avatarDecoration: {
},
'detach': async () => {
const update = [...$i.avatarDecorations];
update.splice(index, 1);
update.splice(index!, 1);
await os.apiWithDialog('i/update', {
avatarDecorations: update,
});

View File

@ -43,9 +43,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSection>
<FormSection>
<div class="_gaps_s">
<FormLink @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
<FormLink @click="testNotification">{{ i18n.ts._notification.sendTestNotification }}</FormLink>
<FormLink @click="flushNotification">{{ i18n.ts._notification.flushNotification }}</FormLink>
<MkButton @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</MkButton>
<MkButton @click="testNotification">{{ i18n.ts._notification.sendTestNotification }}</MkButton>
<MkButton @click="flushNotification">{{ i18n.ts._notification.flushNotification }}</MkButton>
</div>
</FormSection>
<FormSection>
@ -76,6 +76,7 @@ import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { ensureSignin } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -96,7 +97,7 @@ const sendReadMessage = computed(() => pushRegistrationInServer.value?.sendReadM
const userLists = await misskeyApi('users/lists/list');
async function readAllNotifications() {
await os.apiWithDialog('notifications/mark-all-as-read');
await os.apiWithDialog('notifications/mark-all-as-read', {});
}
async function updateReceiveConfig(type: typeof notificationTypes[number], value: NotificationConfig) {
@ -134,7 +135,7 @@ async function flushNotification() {
if (canceled) return;
os.apiWithDialog('notifications/flush');
os.apiWithDialog('notifications/flush', {});
}
const headerActions = computed(() => []);

View File

@ -80,14 +80,14 @@ async function change() {
type: 'password',
autocomplete: 'new-password',
});
if (canceled2) return;
if (canceled2 || newPassword == null) return;
const { canceled: canceled3, result: newPassword2 } = await os.inputText({
title: i18n.ts.newPasswordRetype,
type: 'password',
autocomplete: 'new-password',
});
if (canceled3) return;
if (canceled3 || newPassword2 == null) return;
if (newPassword !== newPassword2) {
os.alert({

View File

@ -205,8 +205,8 @@ import { computed, ref, watch } from 'vue';
import JSON5 from 'json5';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { Theme } from '@/theme.js';
import { isSafeMode } from '@@/js/config.js';
import type { Theme } from '@/theme.js';
import * as os from '@/os.js';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
@ -275,6 +275,7 @@ async function toggleDarkMode() {
const value = !store.r.darkMode.value;
if (syncDeviceDarkMode.value) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.switchDarkModeManuallyWhenSyncEnabledConfirm({ x: i18n.ts.syncDeviceDarkMode }),
});
if (canceled) return;

View File

@ -149,10 +149,8 @@ async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<v
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const headerActions = computed(() => []);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const headerTabs = computed(() => []);
definePage(() => ({

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="tlComponent"
:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
:class="$style.tl"
:src="src.split(':')[0]"
:src="(src.split(':')[0] as (BasicTimelineType | 'list'))"
:list="src.split(':')[1]"
:withRenotes="withRenotes"
:withReplies="withReplies"

View File

@ -43,6 +43,8 @@ const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
async function renderChart() {
if (chartEl.value == null) return;
if (chartInstance) {
chartInstance.destroy();
}

View File

@ -36,13 +36,15 @@ const props = defineProps<{
const chartEl = useTemplateRef('chartEl');
const legendEl = useTemplateRef('legendEl');
const now = new Date();
let chartInstance: Chart = null;
let chartInstance: Chart | null = null;
const chartLimit = 50;
const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
async function renderChart() {
if (chartEl.value == null) return;
if (chartInstance) {
chartInstance.destroy();
}

View File

@ -36,13 +36,15 @@ const props = defineProps<{
const chartEl = useTemplateRef('chartEl');
const legendEl = useTemplateRef('legendEl');
const now = new Date();
let chartInstance: Chart = null;
let chartInstance: Chart | null = null;
const chartLimit = 30;
const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
async function renderChart() {
if (chartEl.value == null) return;
if (chartInstance) {
chartInstance.destroy();
}

View File

@ -32,6 +32,8 @@ export type SoundStore = {
volume: number;
};
type OmitStrict<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
export const PREF_DEF = definePreferences({
@ -385,7 +387,7 @@ export const PREF_DEF = definePreferences({
default: false,
},
plugins: {
default: [] as Plugin[],
default: [] as (OmitStrict<Plugin, 'config'> & { config: Record<string, any> })[],
mergeStrategy: (a, b) => {
const sameIdExists = a.some(x => b.some(y => x.installId === y.installId));
if (sameIdExists) throw new Error();

View File

@ -109,10 +109,11 @@ export function definePreferences<T extends Record<string, unknown>>(x: {
}
export function getInitialPrefValue<K extends keyof PREF>(k: K): ValueOf<K> {
if (typeof PREF_DEF[k].default === 'function') { // factory
return PREF_DEF[k].default();
const _default = PREF_DEF[k as string].default;
if (typeof _default === 'function') { // factory
return _default();
} else {
return PREF_DEF[k].default;
return _default;
}
}

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div v-if="$i" :class="$style.root">
<MkA
v-for="announcement in $i.unreadAnnouncements.filter(x => x.display === 'banner')"
:key="announcement.id"

View File

@ -12,7 +12,7 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
function toolsMenuItems(): MenuItem[] {
return [{
const items: MenuItem[] = [{
type: 'link',
to: '/scratchpad',
text: i18n.ts.scratchpad,
@ -27,17 +27,27 @@ function toolsMenuItems(): MenuItem[] {
to: '/clicker',
text: '🍪👈',
icon: 'ti ti-cookie',
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
type: 'link',
to: '/custom-emojis-manager',
text: i18n.ts.manageCustomEmojis,
icon: 'ti ti-icons',
} : undefined, ($i && ($i.isAdmin || $i.policies.canManageAvatarDecorations)) ? {
type: 'link',
to: '/avatar-decorations',
text: i18n.ts.manageAvatarDecorations,
icon: 'ti ti-sparkles',
} : undefined];
}];
if ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) {
items.push({
type: 'link',
to: '/custom-emojis-manager',
text: i18n.ts.manageCustomEmojis,
icon: 'ti ti-icons',
});
}
if ($i && ($i.isAdmin || $i.policies.canManageAvatarDecorations)) {
items.push({
type: 'link' as const,
to: '/avatar-decorations',
text: i18n.ts.manageAvatarDecorations,
icon: 'ti ti-sparkles',
});
}
return items;
}
export function openInstanceMenu(ev: MouseEvent) {

View File

@ -29,18 +29,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkMarqueeText from '@/components/MkMarqueeText.vue';
import { useInterval } from '@@/js/use-interval.js';
import MkMarqueeText from '@/components/MkMarqueeText.vue';
import { shuffle } from '@/utility/shuffle.js';
const props = defineProps<{
url?: string;
url: string;
shuffle?: boolean;
display?: 'marquee' | 'oneByOne';
marqueeDuration?: number;
marqueeReverse?: boolean;
oneByOneInterval?: number;
refreshIntervalSec?: number;
refreshIntervalSec: number;
}>();
const items = ref<Misskey.entities.FetchRssResponse['items']>([]);

View File

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.nonTitlebarArea">
<XSidebar v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'left'"/>
<div :class="[$style.main, { [$style.withWallpaper]: withWallpaper, [$style.withSidebarAndTitlebar]: !isMobile && prefer.r['deck.navbarPosition'].value === 'left' && prefer.r.showTitlebar.value }]" :style="{ backgroundImage: prefer.s['deck.wallpaper'] != null ? `url(${ prefer.s['deck.wallpaper'] })` : null }">
<div :class="[$style.main, { [$style.withWallpaper]: withWallpaper, [$style.withSidebarAndTitlebar]: !isMobile && prefer.r['deck.navbarPosition'].value === 'left' && prefer.r.showTitlebar.value }]" :style="{ backgroundImage: prefer.s['deck.wallpaper'] != null ? `url(${ prefer.s['deck.wallpaper'] })` : '' }">
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/>
<XReloadSuggestion v-if="shouldSuggestReload"/>

View File

@ -52,7 +52,7 @@ export async function lookupUserByEmail() {
const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result });
os.pageWindow(`/admin/user/${user.id}`);
} catch (err) {
} catch (err: any) {
if (err.code === 'USER_NOT_FOUND') {
os.alert({
type: 'error',

View File

@ -10,7 +10,7 @@ export const chartLegend = (legend: InstanceType<typeof MkChartLegend>) => ({
id: 'htmlLegend',
afterUpdate(chart, args, options) {
// Reuse the built-in legendItems generator
const items = chart.options.plugins.legend.labels.generateLabels(chart);
const items = chart.options.plugins!.legend!.labels!.generateLabels!(chart);
legend.update(chart, items);
},

View File

@ -27,7 +27,7 @@ export async function load() {
scope: ['clickerGame'],
key: 'saveData',
});
} catch (err) {
} catch (err: any) {
if (err.code === 'NO_SUCH_KEY') {
saveData.value = {
gameVersion: 2,
@ -43,20 +43,6 @@ export async function load() {
}
throw err;
}
// migration
if (saveData.value.gameVersion === 1) {
saveData.value = {
gameVersion: 2,
cookies: saveData.value.cookies,
totalCookies: saveData.value.cookies,
totalHandmadeCookies: saveData.value.cookies,
clicked: saveData.value.clicked,
achievements: [],
facilities: [],
};
save();
}
}
export async function save() {

View File

@ -39,7 +39,7 @@ export async function getNoteClipMenu(props: {
}
}
const appearNote = getAppearNote(props.note);
const appearNote = getAppearNote(props.note) ?? props.note;
const clips = await clipsCache.fetch();
const menu: MenuItem[] = [...clips.map(clip => ({

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_panel">
<div :class="$style.container" :style="{ backgroundImage: instance.bannerUrl ? `url(${ instance.bannerUrl })` : undefined }">
<div :class="$style.iconContainer">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/>
<img :src="instance.iconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/>
</div>
<div :class="$style.bodyContainer">
<div :class="$style.body">
@ -20,10 +20,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { host } from '@@/js/config.js';
import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import { host } from '@@/js/config.js';
import { instance } from '@/instance.js';
const name = 'instanceInfo';