Merge branch 'develop' into acct-parse-url

This commit is contained in:
tamaina 2025-08-26 14:36:40 +09:00
commit c4ed1df173
92 changed files with 519 additions and 347 deletions

View File

@ -54,6 +54,8 @@
- Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正 - Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正
- Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正 - Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正
- Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正 - Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正
- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正
- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正
### Server ### Server
- Feat: サーバー管理コマンド - Feat: サーバー管理コマンド

View File

@ -49,15 +49,12 @@ export class NoteReactionEntityService implements OnModuleInit {
public async pack( public async pack(
src: MiNoteReaction['id'] | MiNoteReaction, src: MiNoteReaction['id'] | MiNoteReaction,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
options?: { options?: object,
withNote: boolean;
},
hints?: { hints?: {
packedUser?: Packed<'UserLite'> packedUser?: Packed<'UserLite'>
}, },
): Promise<Packed<'NoteReaction'>> { ): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({ const opts = Object.assign({
withNote: false,
}, options); }, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src }); 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(), createdAt: this.idService.parse(reaction.id).date.toISOString(),
user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me), user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction), 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( public async packMany(
reactions: MiNoteReaction[], reactions: MiNoteReaction[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
options?: { options?: object,
withNote: boolean;
},
): Promise<Packed<'NoteReaction'>[]> { ): Promise<Packed<'NoteReaction'>[]> {
const opts = Object.assign({ const opts = Object.assign({
withNote: false,
}, options); }, options);
const _users = reactions.map(({ user, userId }) => user ?? userId); const _users = reactions.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me) const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u]))); .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) }))); 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 { packedMutingSchema } from '@/models/json-schema/muting.js';
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'; import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
import { packedBlockingSchema } from '@/models/json-schema/blocking.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 { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js'; import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js'; import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
@ -92,6 +92,7 @@ export const refs = {
Note: packedNoteSchema, Note: packedNoteSchema,
NoteDraft: packedNoteDraftSchema, NoteDraft: packedNoteDraftSchema,
NoteReaction: packedNoteReactionSchema, NoteReaction: packedNoteReactionSchema,
NoteReactionWithNote: packedNoteReactionWithNoteSchema,
NoteFavorite: packedNoteFavoriteSchema, NoteFavorite: packedNoteFavoriteSchema,
Notification: packedNotificationSchema, Notification: packedNotificationSchema,
DriveFile: packedDriveFileSchema, DriveFile: packedDriveFileSchema,

View File

@ -10,7 +10,6 @@ export const packedNoteReactionSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
format: 'id', format: 'id',
example: 'xxxxxxxxxx',
}, },
createdAt: { createdAt: {
type: 'string', type: 'string',
@ -28,3 +27,33 @@ export const packedNoteReactionSchema = {
}, },
}, },
} as const; } 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', type: 'string',
optional: false, nullable: false, 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: { imageUrl: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,

View File

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

View File

@ -28,7 +28,7 @@ export const meta = {
items: { items: {
type: 'object', type: 'object',
optional: false, nullable: false, 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 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', () => { main.on('newChatMessage', () => {
updateCurrentAccountPartial({ hasUnreadChatMessages: true }); updateCurrentAccountPartial({ hasUnreadChatMessages: true });
sound.playMisskeySfx('chatMessage'); 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', [$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"> <img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div> </div>
</div> </div>
@ -71,7 +71,7 @@ const props = withDefaults(defineProps<{
const achievements = ref<Misskey.entities.UsersAchievementsResponse | null>(null); const achievements = ref<Misskey.entities.UsersAchievementsResponse | null>(null);
const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x))); const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x)));
function fetch() { function _fetch_() {
misskeyApi('users/achievements', { userId: props.user.id }).then(res => { misskeyApi('users/achievements', { userId: props.user.id }).then(res => {
achievements.value = []; achievements.value = [];
for (const t of ACHIEVEMENT_TYPES) { for (const t of ACHIEVEMENT_TYPES) {
@ -84,11 +84,11 @@ function fetch() {
function clickHere() { function clickHere() {
claimAchievement('clickedClickHere'); claimAchievement('clickedClickHere');
fetch(); _fetch_();
} }
onMounted(() => { onMounted(() => {
fetch(); _fetch_();
}); });
</script> </script>

View File

@ -265,6 +265,8 @@ onUnmounted(() => {
if (handle) { if (handle) {
window.cancelAnimationFrame(handle); window.cancelAnimationFrame(handle);
} }
// TODO: WebGL
}); });
</script> </script>

View File

@ -589,7 +589,10 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
}; };
const fetchInstanceRequestsChart = 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 { return {
series: [{ series: [{
name: 'In', name: 'In',
@ -611,7 +614,10 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
}; };
const fetchInstanceUsersChart = async (total: boolean): 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 { return {
series: [{ series: [{
name: 'Users', name: 'Users',
@ -626,7 +632,10 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
}; };
const fetchInstanceNotesChart = 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 { return {
series: [{ series: [{
name: 'Notes', name: 'Notes',
@ -641,7 +650,10 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
}; };
const fetchInstanceFfChart = 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 { return {
series: [{ series: [{
name: 'Following', name: 'Following',
@ -664,7 +676,10 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
}; };
const fetchInstanceDriveUsageChart = 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 { return {
bytes: true, bytes: true,
series: [{ series: [{
@ -680,7 +695,10 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
}; };
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => { 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 { return {
series: [{ series: [{
name: 'Drive files', name: 'Drive files',
@ -695,7 +713,10 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
}; };
const fetchPerUserNotesChart = async (): Promise<typeof chartData> => { 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 { return {
series: [...(props.args?.withoutAll ? [] : [{ series: [...(props.args?.withoutAll ? [] : [{
name: 'All', name: 'All',
@ -727,7 +748,10 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserPvChart = 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 { return {
series: [{ series: [{
name: 'Unique PV (user)', name: 'Unique PV (user)',
@ -754,7 +778,10 @@ const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserFollowingChart = 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 { return {
series: [{ series: [{
name: 'Local', name: 'Local',
@ -769,7 +796,10 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserFollowersChart = 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 { return {
series: [{ series: [{
name: 'Local', name: 'Local',
@ -784,7 +814,10 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserDriveChart = 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 { return {
bytes: true, bytes: true,
series: [{ series: [{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -297,76 +297,97 @@ function prepend(note: Misskey.entities.Note & MisskeyEntity) {
} }
} }
let connection: Misskey.IChannelConnection | null = null;
let connection2: Misskey.IChannelConnection | null = null;
const stream = store.s.realtimeMode ? useStream() : null; const stream = store.s.realtimeMode ? useStream() : null;
const connections = {
antenna: null as Misskey.IChannelConnection<Misskey.Channels['antenna']> | null,
homeTimeline: null as Misskey.IChannelConnection<Misskey.Channels['homeTimeline']> | null,
localTimeline: null as Misskey.IChannelConnection<Misskey.Channels['localTimeline']> | null,
hybridTimeline: null as Misskey.IChannelConnection<Misskey.Channels['hybridTimeline']> | null,
globalTimeline: null as Misskey.IChannelConnection<Misskey.Channels['globalTimeline']> | null,
main: null as Misskey.IChannelConnection<Misskey.Channels['main']> | null,
userList: null as Misskey.IChannelConnection<Misskey.Channels['userList']> | null,
channel: null as Misskey.IChannelConnection<Misskey.Channels['channel']> | null,
roleTimeline: null as Misskey.IChannelConnection<Misskey.Channels['roleTimeline']> | null,
};
function connectChannel() { function connectChannel() {
if (stream == null) return; if (stream == null) return;
if (props.src === 'antenna') { if (props.src === 'antenna') {
if (props.antenna == null) return; if (props.antenna == null) return;
connection = stream.useChannel('antenna', { connections.antenna = stream.useChannel('antenna', {
antennaId: props.antenna, antennaId: props.antenna,
}); });
connections.antenna.on('note', prepend);
} else if (props.src === 'home') { } else if (props.src === 'home') {
connection = stream.useChannel('homeTimeline', { connections.homeTimeline = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connection2 = stream.useChannel('main'); connections.main = stream.useChannel('main');
connections.homeTimeline.on('note', prepend);
} else if (props.src === 'local') { } else if (props.src === 'local') {
connection = stream.useChannel('localTimeline', { connections.localTimeline = stream.useChannel('localTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.localTimeline.on('note', prepend);
} else if (props.src === 'social') { } else if (props.src === 'social') {
connection = stream.useChannel('hybridTimeline', { connections.hybridTimeline = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.hybridTimeline.on('note', prepend);
} else if (props.src === 'global') { } else if (props.src === 'global') {
connection = stream.useChannel('globalTimeline', { connections.globalTimeline = stream.useChannel('globalTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.globalTimeline.on('note', prepend);
} else if (props.src === 'mentions') { } else if (props.src === 'mentions') {
connection = stream.useChannel('main'); connections.main = stream.useChannel('main');
connection.on('mention', prepend); connections.main.on('mention', prepend);
} else if (props.src === 'directs') { } else if (props.src === 'directs') {
const onNote = note => { const onNote = note => {
if (note.visibility === 'specified') { if (note.visibility === 'specified') {
prepend(note); prepend(note);
} }
}; };
connection = stream.useChannel('main'); connections.main = stream.useChannel('main');
connection.on('mention', onNote); connections.main.on('mention', onNote);
} else if (props.src === 'list') { } else if (props.src === 'list') {
if (props.list == null) return; if (props.list == null) return;
connection = stream.useChannel('userList', { connections.userList = stream.useChannel('userList', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
listId: props.list, listId: props.list,
}); });
connections.userList.on('note', prepend);
} else if (props.src === 'channel') { } else if (props.src === 'channel') {
if (props.channel == null) return; if (props.channel == null) return;
connection = stream.useChannel('channel', { connections.channel = stream.useChannel('channel', {
channelId: props.channel, channelId: props.channel,
}); });
connections.channel.on('note', prepend);
} else if (props.src === 'role') { } else if (props.src === 'role') {
if (props.role == null) return; if (props.role == null) return;
connection = stream.useChannel('roleTimeline', { connections.roleTimeline = stream.useChannel('roleTimeline', {
roleId: props.role, roleId: props.role,
}); });
connections.roleTimeline.on('note', prepend);
} }
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
} }
function disconnectChannel() { function disconnectChannel() {
if (connection) connection.dispose(); for (const key in connections) {
if (connection2) connection2.dispose(); const conn = connections[key as keyof typeof connections];
if (conn != null) {
conn.dispose();
connections[key as keyof typeof connections] = null;
}
}
} }
if (store.s.realtimeMode) { if (store.s.realtimeMode) {

View File

@ -52,7 +52,7 @@ import { prefer } from '@/preferences.js';
type Ad = (typeof instance)['ads'][number]; type Ad = (typeof instance)['ads'][number];
const props = defineProps<{ const props = defineProps<{
preferForms: string[]; preferForms?: string[];
specify?: Ad; specify?: Ad;
}>(); }>();
@ -71,7 +71,7 @@ const choseAd = (): Ad | null => {
ratio: 0, ratio: 0,
} : ad); } : ad);
let ads = allAds.filter(ad => props.preferForms.includes(ad.place)); let ads = props.preferForms ? allAds.filter(ad => props.preferForms!.includes(ad.place)) : allAds;
if (ads.length === 0) { if (ads.length === 0) {
ads = allAds.filter(ad => ad.place === 'square'); ads = allAds.filter(ad => ad.place === 'square');

View File

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

View File

@ -1,14 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<slot></slot>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" module>
</style>

View File

@ -9,7 +9,7 @@ import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { Component, Ref } from 'vue'; import type { Component, Ref } from 'vue';
import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js'; import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js'; import type { PostFormProps } from '@/types/post-form.js';
@ -157,28 +157,9 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
return zIndexes[priority]; return zIndexes[priority];
} }
// InstanceType<typeof Component>['$emit'] だとインターセクション型が返ってきて
// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
type ComponentEmit<T> = T extends new () => { $props: infer Props }
? [keyof Pick<T, Extract<keyof T, `on${string}`>>] extends [never]
? Record<string, unknown> // *.ts ファイルから型がうまく取れないとき用(これがないと {} になって型エラーがうるさい)
: EmitsExtractor<Props>
: T extends (...args: any) => any
? ReturnType<T> extends { [x: string]: any; __ctx?: { [x: string]: any; props: infer Props } }
? [keyof Pick<T, Extract<keyof T, `on${string}`>>] extends [never]
? Record<string, unknown>
: EmitsExtractor<Props>
: never
: never;
// props に ref を許可するようにする // props に ref を許可するようにする
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> }; type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> };
type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
};
export function popup<T extends Component>( export function popup<T extends Component>(
component: T, component: T,
props: ComponentProps<T>, props: ComponentProps<T>,

View File

@ -111,13 +111,13 @@ const props = defineProps<{
fileId: string, fileId: string,
}>(); }>();
async function fetch() { async function _fetch_() {
file.value = await misskeyApi('drive/files/show', { fileId: props.fileId }); file.value = await misskeyApi('drive/files/show', { fileId: props.fileId });
info.value = await misskeyApi('admin/drive/show-file', { fileId: props.fileId }); info.value = await misskeyApi('admin/drive/show-file', { fileId: props.fileId });
isSensitive.value = file.value.isSensitive; isSensitive.value = file.value.isSensitive;
} }
fetch(); _fetch_();
async function del() { async function del() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({

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> <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
</div> </div>
<div v-if="expandedRoleIds.includes(role.id)" :class="$style.roleItemSub"> <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>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-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 v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div> </div>
</div> </div>

View File

@ -140,15 +140,15 @@ function toggleDayOfWeek(ad, index) {
function add() { function add() {
ads.value.unshift({ ads.value.unshift({
id: null, id: '',
memo: '', memo: '',
place: 'square', place: 'square',
priority: 'middle', priority: 'middle',
ratio: 1, ratio: 1,
url: '', url: '',
imageUrl: null, imageUrl: '',
expiresAt: null, expiresAt: new Date().toISOString(),
startsAt: null, startsAt: new Date().toISOString(),
dayOfWeek: 0, dayOfWeek: 0,
}); });
} }
@ -160,7 +160,7 @@ function remove(ad) {
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) return; if (canceled) return;
ads.value = ads.value.filter(x => x !== ad); ads.value = ads.value.filter(x => x !== ad);
if (ad.id == null) return; if (ad.id === '') return;
os.apiWithDialog('admin/ad/delete', { os.apiWithDialog('admin/ad/delete', {
id: ad.id, id: ad.id,
}).then(() => { }).then(() => {
@ -170,7 +170,7 @@ function remove(ad) {
} }
function save(ad) { function save(ad) {
if (ad.id == null) { if (ad.id === '') {
misskeyApi('admin/ad/create', { misskeyApi('admin/ad/create', {
...ad, ...ad,
expiresAt: new Date(ad.expiresAt).getTime(), expiresAt: new Date(ad.expiresAt).getTime(),
@ -207,7 +207,7 @@ function save(ad) {
} }
function more() { function more() {
misskeyApi('admin/ad/list', { untilId: ads.value.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => { misskeyApi('admin/ad/list', { untilId: ads.value.reduce((acc, ad) => ad.id !== '' ? ad : acc).id, publishing: publishing }).then(adsResponse => {
if (adsResponse == null) return; if (adsResponse == null) return;
ads.value = ads.value.concat(adsResponse.map(r => { ads.value = ads.value.concat(adsResponse.map(r => {
const exdate = new Date(r.expiresAt); const exdate = new Date(r.expiresAt);

View File

@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> <MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
</div> </div>
</div> </div>
<MkError v-else-if="error" @retry="fetch()"/> <MkError v-else-if="error" @retry="_fetch_()"/>
<MkLoading v-else/> <MkLoading v-else/>
</Transition> </Transition>
</div> </div>
@ -66,7 +66,7 @@ const announcement = ref<Misskey.entities.Announcement | null>(null);
const error = ref<any>(null); const error = ref<any>(null);
const path = computed(() => props.announcementId); const path = computed(() => props.announcementId);
function fetch() { function _fetch_() {
announcement.value = null; announcement.value = null;
misskeyApi('announcements/show', { misskeyApi('announcements/show', {
announcementId: props.announcementId, announcementId: props.announcementId,
@ -96,7 +96,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
} }
} }
watch(() => path.value, fetch, { immediate: true }); watch(() => path.value, _fetch_, { immediate: true });
const headerActions = computed(() => []); const headerActions = computed(() => []);

View File

@ -75,14 +75,15 @@ onMounted(async () => {
if (!$i) return; if (!$i) return;
try { try {
session.value = await misskeyApi('auth/session/show', { const result = await misskeyApi('auth/session/show', {
token: props.token, token: props.token,
}); });
session.value = result;
// //
if (session.value.app.isAuthorized) { if (result.app.isAuthorized) {
await misskeyApi('auth/accept', { await misskeyApi('auth/accept', {
token: session.value.token, token: result.token,
}); });
accepted(); accepted();
} else { } else {

View File

@ -92,7 +92,7 @@ const props = defineProps<{
}>(); }>();
const channel = ref<Misskey.entities.Channel | null>(null); const channel = ref<Misskey.entities.Channel | null>(null);
const name = ref<string | null>(null); const name = ref<string>('');
const description = ref<string | null>(null); const description = ref<string | null>(null);
const bannerUrl = ref<string | null>(null); const bannerUrl = ref<string | null>(null);
const bannerId = ref<string | null>(null); const bannerId = ref<string | null>(null);
@ -114,20 +114,22 @@ watch(() => bannerId.value, async () => {
async function fetchChannel() { async function fetchChannel() {
if (props.channelId == null) return; if (props.channelId == null) return;
channel.value = await misskeyApi('channels/show', { const result = await misskeyApi('channels/show', {
channelId: props.channelId, channelId: props.channelId,
}); });
name.value = channel.value.name; name.value = result.name;
description.value = channel.value.description; description.value = result.description;
bannerId.value = channel.value.bannerId; bannerId.value = result.bannerId;
bannerUrl.value = channel.value.bannerUrl; bannerUrl.value = result.bannerUrl;
isSensitive.value = channel.value.isSensitive; isSensitive.value = result.isSensitive;
pinnedNotes.value = channel.value.pinnedNoteIds.map(id => ({ pinnedNotes.value = result.pinnedNoteIds.map(id => ({
id, id,
})); }));
color.value = channel.value.color; color.value = result.color;
allowRenoteToExternal.value = channel.value.allowRenoteToExternal; allowRenoteToExternal.value = result.allowRenoteToExternal;
channel.value = result;
} }
fetchChannel(); fetchChannel();
@ -154,15 +156,17 @@ function save() {
name: name.value, name: name.value,
description: description.value, description: description.value,
bannerId: bannerId.value, bannerId: bannerId.value,
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
color: color.value, color: color.value,
isSensitive: isSensitive.value, isSensitive: isSensitive.value,
allowRenoteToExternal: allowRenoteToExternal.value, allowRenoteToExternal: allowRenoteToExternal.value,
}; } satisfies Misskey.entities.ChannelsCreateRequest;
if (props.channelId) { if (props.channelId != null) {
params.channelId = props.channelId; os.apiWithDialog('channels/update', {
os.apiWithDialog('channels/update', params); ...params,
channelId: props.channelId,
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
});
} else { } else {
os.apiWithDialog('channels/create', params).then(created => { os.apiWithDialog('channels/create', params).then(created => {
router.push('/channels/:channelId', { router.push('/channels/:channelId', {
@ -175,12 +179,13 @@ function save() {
} }
async function archive() { async function archive() {
if (props.channelId == null) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }), title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }),
text: i18n.ts.channelArchiveConfirmDescription, text: i18n.ts.channelArchiveConfirmDescription,
}); });
if (canceled) return; if (canceled) return;
misskeyApi('channels/update', { misskeyApi('channels/update', {

View File

@ -312,6 +312,7 @@ const headerActions = computed(() => [{
handler: add, handler: add,
}, { }, {
icon: 'ti ti-dots', icon: 'ti ti-dots',
text: i18n.ts.more,
handler: menu, handler: menu,
}]); }]);

View File

@ -105,7 +105,7 @@ const folderHierarchy = computed(() => {
}); });
const isImage = computed(() => file.value?.type.startsWith('image/')); const isImage = computed(() => file.value?.type.startsWith('image/'));
async function fetch() { async function _fetch_() {
fetching.value = true; fetching.value = true;
file.value = await misskeyApi('drive/files/show', { file.value = await misskeyApi('drive/files/show', {
@ -119,7 +119,7 @@ async function fetch() {
} }
function postThis() { function postThis() {
if (!file.value) return; if (file.value == null) return;
os.post({ os.post({
initialFiles: [file.value], initialFiles: [file.value],
@ -127,26 +127,28 @@ function postThis() {
} }
function move() { function move() {
if (!file.value) return; if (file.value == null) return;
const f = file.value;
selectDriveFolder(null).then(folder => { selectDriveFolder(null).then(folder => {
misskeyApi('drive/files/update', { misskeyApi('drive/files/update', {
fileId: file.value.id, fileId: f.id,
folderId: folder[0] ? folder[0].id : null, folderId: folder[0] ? folder[0].id : null,
}).then(async () => { }).then(async () => {
await fetch(); await _fetch_();
}); });
}); });
} }
function toggleSensitive() { function toggleSensitive() {
if (!file.value) return; if (file.value == null) return;
os.apiWithDialog('drive/files/update', { os.apiWithDialog('drive/files/update', {
fileId: file.value.id, fileId: file.value.id,
isSensitive: !file.value.isSensitive, isSensitive: !file.value.isSensitive,
}).then(async () => { }).then(async () => {
await fetch(); await _fetch_();
}).catch(err => { }).catch(err => {
os.alert({ os.alert({
type: 'error', type: 'error',
@ -157,7 +159,9 @@ function toggleSensitive() {
} }
function rename() { function rename() {
if (!file.value) return; if (file.value == null) return;
const f = file.value;
os.inputText({ os.inputText({
title: i18n.ts.renameFile, title: i18n.ts.renameFile,
@ -166,16 +170,18 @@ function rename() {
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
os.apiWithDialog('drive/files/update', { os.apiWithDialog('drive/files/update', {
fileId: file.value.id, fileId: f.id,
name: name, name: name,
}).then(async () => { }).then(async () => {
await fetch(); await _fetch_();
}); });
}); });
} }
async function describe() { 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), { const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkFileCaptionEditWindow.vue').then(x => x.default), {
default: file.value.comment ?? '', default: file.value.comment ?? '',
@ -183,10 +189,10 @@ async function describe() {
}, { }, {
done: caption => { done: caption => {
os.apiWithDialog('drive/files/update', { os.apiWithDialog('drive/files/update', {
fileId: file.value.id, fileId: f.id,
comment: caption.length === 0 ? null : caption, comment: caption.length === 0 ? null : caption,
}).then(async () => { }).then(async () => {
await fetch(); await _fetch_();
}); });
}, },
closed: () => dispose(), closed: () => dispose(),
@ -194,7 +200,7 @@ async function describe() {
} }
async function deleteFile() { async function deleteFile() {
if (!file.value) return; if (file.value == null) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
@ -212,7 +218,7 @@ async function deleteFile() {
} }
onMounted(async () => { onMounted(async () => {
await fetch(); await _fetch_();
}); });
</script> </script>

View File

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

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkTextarea> </MkTextarea>
<div class="_gaps_s"> <div class="_gaps_s">
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }"> <div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : '' }">
<div class="name">{{ file.name }}</div> <div class="name">{{ file.name }}</div>
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button> <button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
</div> </div>
@ -88,7 +88,7 @@ async function save() {
router.push('/gallery/:postId', { router.push('/gallery/:postId', {
params: { params: {
postId: props.postId, postId: props.postId,
} },
}); });
} else { } else {
const created = await os.apiWithDialog('gallery/posts/create', { const created = await os.apiWithDialog('gallery/posts/create', {
@ -100,7 +100,7 @@ async function save() {
router.push('/gallery/:postId', { router.push('/gallery/:postId', {
params: { params: {
postId: created.id, postId: created.id,
} },
}); });
} }
} }

View File

@ -80,7 +80,7 @@ function close_(): void {
} }
} }
async function fetch() { async function _fetch_() {
if (!url.value || !hash.value) { if (!url.value || !hash.value) {
errorKV.value = { errorKV.value = {
title: i18n.ts._externalResourceInstaller._errors._invalidParams.title, title: i18n.ts._externalResourceInstaller._errors._invalidParams.title,
@ -161,7 +161,7 @@ async function fetch() {
}, },
raw: res.data, raw: res.data,
}; };
} catch (err) { } catch (err: any) {
switch (err.message.toLowerCase()) { switch (err.message.toLowerCase()) {
case 'this theme is already installed': case 'this theme is already installed':
errorKV.value = { errorKV.value = {
@ -229,7 +229,7 @@ async function install() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
url.value = urlParams.get('url'); url.value = urlParams.get('url');
hash.value = urlParams.get('hash'); hash.value = urlParams.get('hash');
fetch(); _fetch_();
definePage(() => ({ definePage(() => ({
title: i18n.ts._externalResourceInstaller.title, title: i18n.ts._externalResourceInstaller.title,

View File

@ -198,7 +198,7 @@ if (iAmModerator) {
}); });
} }
async function fetch(): Promise<void> { async function _fetch_(): Promise<void> {
if (iAmAdmin) { if (iAmAdmin) {
meta.value = await misskeyApi('admin/meta'); meta.value = await misskeyApi('admin/meta');
} }
@ -276,7 +276,7 @@ function refreshMetadata(): void {
}); });
} }
fetch(); _fetch_();
const headerActions = computed(() => [{ const headerActions = computed(() => [{
text: `https://${props.host}`, text: `https://${props.host}`,

View File

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

View File

@ -29,7 +29,7 @@ import MkButton from '@/components/MkButton.vue';
const state = ref<'fetching' | 'done'>('fetching'); const state = ref<'fetching' | 'done'>('fetching');
function fetch() { function _fetch_() {
const params = new URL(window.location.href).searchParams; const params = new URL(window.location.href).searchParams;
// acctdeprecated // acctdeprecated
@ -44,20 +44,18 @@ function fetch() {
if (uri.startsWith('https://')) { if (uri.startsWith('https://')) {
promise = misskeyApi('ap/show', { promise = misskeyApi('ap/show', {
uri, uri,
}); }).then(res => {
promise.then(res => {
if (res.type === 'User') { if (res.type === 'User') {
mainRouter.replace('/@:acct/:page?', { mainRouter.replace('/@:acct/:page?', {
params: { params: {
acct: res.host != null ? `${res.object.username}@${res.object.host}` : res.object.username, acct: res.host != null ? `${res.object.username}@${res.object.host}` : res.object.username,
} },
}); });
} else if (res.type === 'Note') { } else if (res.type === 'Note') {
mainRouter.replace('/notes/:noteId/:initialTab?', { mainRouter.replace('/notes/:noteId/:initialTab?', {
params: { params: {
noteId: res.object.id, noteId: res.object.id,
} },
}); });
} else { } else {
os.alert({ os.alert({
@ -70,12 +68,11 @@ function fetch() {
if (uri.startsWith('acct:')) { if (uri.startsWith('acct:')) {
uri = uri.slice(5); uri = uri.slice(5);
} }
promise = misskeyApi('users/show', Misskey.acct.parse(uri)); promise = misskeyApi('users/show', Misskey.acct.parse(uri)).then(user => {
promise.then(user => {
mainRouter.replace('/@:acct/:page?', { mainRouter.replace('/@:acct/:page?', {
params: { params: {
acct: user.host != null ? `${user.username}@${user.host}` : user.username, acct: user.host != null ? `${user.username}@${user.host}` : user.username,
} },
}); });
}); });
} }
@ -96,7 +93,7 @@ function goToMisskey(): void {
window.location.href = '/'; window.location.href = '/';
} }
fetch(); _fetch_();
const headerActions = computed(() => []); const headerActions = computed(() => []);

View File

@ -30,11 +30,11 @@ import { antennasCache } from '@/cache.js';
const antennas = computed(() => antennasCache.value.value ?? []); const antennas = computed(() => antennasCache.value.value ?? []);
function fetch() { function _fetch_() {
antennasCache.fetch(); antennasCache.fetch();
} }
fetch(); _fetch_();
const headerActions = computed(() => [{ const headerActions = computed(() => [{
asFullButton: true, asFullButton: true,
@ -42,7 +42,7 @@ const headerActions = computed(() => [{
text: i18n.ts.reload, text: i18n.ts.reload,
handler: () => { handler: () => {
antennasCache.delete(); antennasCache.delete();
fetch(); _fetch_();
}, },
}]); }]);

View File

@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="items.length > 0" class="_gaps"> <div v-if="items.length > 0" class="_gaps">
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div> <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds!.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
<MkAvatars :userIds="list.userIds" :limit="10"/> <MkAvatars :userIds="list.userIds!" :limit="10"/>
</MkA> </MkA>
</div> </div>
</div> </div>
@ -40,20 +40,20 @@ const $i = ensureSignin();
const items = computed(() => userListsCache.value.value ?? []); const items = computed(() => userListsCache.value.value ?? []);
function fetch() { function _fetch_() {
userListsCache.fetch(); userListsCache.fetch();
} }
fetch(); _fetch_();
async function create() { async function create() {
const { canceled, result: name } = await os.inputText({ const { canceled, result: name } = await os.inputText({
title: i18n.ts.enterListName, title: i18n.ts.enterListName,
}); });
if (canceled) return; if (canceled || name == null) return;
await os.apiWithDialog('users/lists/create', { name: name }); await os.apiWithDialog('users/lists/create', { name: name });
userListsCache.delete(); userListsCache.delete();
fetch(); _fetch_();
} }
const headerActions = computed(() => [{ const headerActions = computed(() => [{
@ -62,7 +62,7 @@ const headerActions = computed(() => [{
text: i18n.ts.reload, text: i18n.ts.reload,
handler: () => { handler: () => {
userListsCache.delete(); userListsCache.delete();
fetch(); _fetch_();
}, },
}]); }]);
@ -74,7 +74,7 @@ definePage(() => ({
})); }));
onActivated(() => { onActivated(() => {
fetch(); _fetch_();
}); });
</script> </script>

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder defaultOpen> <MkFolder defaultOpen>
<template #label>{{ i18n.ts.members }}</template> <template #label>{{ i18n.ts.members }}</template>
<template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template> <template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds!.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton> <MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>

View File

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

View File

@ -31,7 +31,7 @@ import { Paginator } from '@/utility/paginator.js';
const tab = ref('all'); const tab = ref('all');
const includeTypes = ref<string[] | null>(null); const includeTypes = ref<string[] | null>(null);
const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value.includes(t)) : null); const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value!.includes(t)) : null);
const mentionsPaginator = markRaw(new Paginator('notes/mentions', { const mentionsPaginator = markRaw(new Paginator('notes/mentions', {
limit: 10, limit: 10,
@ -71,7 +71,7 @@ const headerActions = computed(() => [tab.value === 'all' ? {
text: i18n.ts.markAllAsRead, text: i18n.ts.markAllAsRead,
icon: 'ti ti-check', icon: 'ti ti-check',
handler: () => { handler: () => {
os.apiWithDialog('notifications/mark-all-as-read'); os.apiWithDialog('notifications/mark-all-as-read', {});
}, },
} : undefined].filter(x => x !== undefined)); } : undefined].filter(x => x !== undefined));

View File

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

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton> <MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
<div v-if="scopesWithDomain" class="_gaps_m"> <div v-if="scopesWithDomain" class="_gaps_m">
<FormSection v-for="domain in scopesWithDomain" :key="domain.domain"> <FormSection v-for="domain in scopesWithDomain" :key="domain.domain ?? 'system'">
<template #label>{{ domain.domain ? domain.domain.toUpperCase() : i18n.ts.system }}</template> <template #label>{{ domain.domain ? domain.domain.toUpperCase() : i18n.ts.system }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<FormLink v-for="scope in domain.scopes" :to="`/registry/keys/${domain.domain ?? '@'}/${scope.join('/')}`" class="_monospace">{{ scope.length === 0 ? '(root)' : scope.join('/') }}</FormLink> <FormLink v-for="scope in domain.scopes" :to="`/registry/keys/${domain.domain ?? '@'}/${scope.join('/')}`" class="_monospace">{{ scope.length === 0 ? '(root)' : scope.join('/') }}</FormLink>

View File

@ -34,6 +34,7 @@ const props = defineProps<{
const password = ref(''); const password = ref('');
async function save() { async function save() {
if (props.token == null) return;
await os.apiWithDialog('reset-password', { await os.apiWithDialog('reset-password', {
token: props.token, token: props.token,
password: password.value, password: password.value,

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="$i.twoFactorEnabled" class="_gaps_s"> <div v-if="$i.twoFactorEnabled" class="_gaps_s">
<div v-text="i18n.ts._2fa.alreadyRegistered"/> <div v-text="i18n.ts._2fa.alreadyRegistered"/>
<template v-if="$i.securityKeysList.length > 0"> <template v-if="$i.securityKeysList!.length > 0">
<MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton> <MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
<MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo> <MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
</template> </template>
@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else> <template v-else>
<MkButton primary @click="addSecurityKey">{{ i18n.ts._2fa.registerSecurityKey }}</MkButton> <MkButton primary @click="addSecurityKey">{{ i18n.ts._2fa.registerSecurityKey }}</MkButton>
<MkFolder v-for="key in $i.securityKeysList" :key="key.id"> <MkFolder v-for="key in $i.securityKeysList!" :key="key.id">
<template #label>{{ key.name }}</template> <template #label>{{ key.name }}</template>
<template #suffix><I18n :src="i18n.ts.lastUsedAt"><template #t><MkTime :time="key.lastUsed"/></template></I18n></template> <template #suffix><I18n :src="i18n.ts.lastUsedAt"><template #t><MkTime :time="key.lastUsed"/></template></I18n></template>
<div class="_buttons"> <div class="_buttons">
@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker> </SearchMarker>
<SearchMarker :keywords="['password', 'less', 'key', 'passkey', 'login', 'signin']"> <SearchMarker :keywords="['password', 'less', 'key', 'passkey', 'login', 'signin']">
<MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)"> <MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList!.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
<template #label><SearchLabel>{{ i18n.ts.passwordLessLogin }}</SearchLabel></template> <template #label><SearchLabel>{{ i18n.ts.passwordLessLogin }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.passwordLessLoginDescription }}</SearchText></template> <template #caption><SearchText>{{ i18n.ts.passwordLessLoginDescription }}</SearchText></template>
</MkSwitch> </MkSwitch>

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!--<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i></MkButton>--> <!--<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i></MkButton>-->
</div> </div>
<MkUserCardMini v-for="x in accounts" :key="x[0] + x[1].id" :user="x[1]" :class="$style.user" @click.prevent="menu(x[0], x[1], $event)"/> <MkUserCardMini v-for="x in accounts" :key="x[0] + x[1].id" :user="x[1]" :class="$style.user" @click.prevent="showMenu(x[0], x[1], $event)"/>
</div> </div>
</SearchMarker> </SearchMarker>
</template> </template>
@ -36,7 +36,7 @@ function refreshAllAccounts() {
// TODO // TODO
} }
function menu(host: string, account: Misskey.entities.UserDetailed, ev: MouseEvent) { function showMenu(host: string, account: Misskey.entities.UserDetailed, ev: MouseEvent) {
let menu: MenuItem[]; let menu: MenuItem[];
menu = [{ menu = [{

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<template #label>{{ token.name }}</template> <template #label>{{ token.name }}</template>
<template #caption>{{ token.description }}</template> <template #caption>{{ token.description }}</template>
<template #suffix><MkTime :time="token.lastUsedAt"/></template> <template v-if="token.lastUsedAt != null" #suffix><MkTime :time="token.lastUsedAt"/></template>
<template #footer> <template #footer>
<MkButton danger @click="revoke(token)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> <MkButton danger @click="revoke(token)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</template> </template>
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>{{ i18n.ts.installedDate }}</template> <template #key>{{ i18n.ts.installedDate }}</template>
<template #value><MkTime :time="token.createdAt" :mode="'detail'"/></template> <template #value><MkTime :time="token.createdAt" :mode="'detail'"/></template>
</MkKeyValue> </MkKeyValue>
<MkKeyValue oneline> <MkKeyValue v-if="token.lastUsedAt != null" oneline>
<template #key>{{ i18n.ts.lastUsedDate }}</template> <template #key>{{ i18n.ts.lastUsedDate }}</template>
<template #value><MkTime :time="token.lastUsedAt" :mode="'detail'"/></template> <template #value><MkTime :time="token.lastUsedAt" :mode="'detail'"/></template>
</MkKeyValue> </MkKeyValue>

View File

@ -17,13 +17,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.decorations"> <div :class="$style.decorations">
<XDecoration <XDecoration
v-for="(avatarDecoration, i) in $i.avatarDecorations" v-for="(avatarDecoration, i) in $i.avatarDecorations"
:decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)" :decoration="avatarDecorations.find(d => d.id === avatarDecoration.id) ?? { id: '', url: '', name: '?', roleIdsThatCanBeUsedThisDecoration: [] }"
:angle="avatarDecoration.angle" :angle="avatarDecoration.angle"
:flipH="avatarDecoration.flipH" :flipH="avatarDecoration.flipH"
:offsetX="avatarDecoration.offsetX" :offsetX="avatarDecoration.offsetX"
:offsetY="avatarDecoration.offsetY" :offsetY="avatarDecoration.offsetY"
:active="true" :active="true"
@click="openDecoration(avatarDecoration, i)" @click="openAttachedDecoration(i)"
/> />
</div> </div>
@ -50,6 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, defineAsyncComponent, computed } from 'vue'; import { ref, defineAsyncComponent, computed } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XDecoration from './avatar-decoration.decoration.vue'; import XDecoration from './avatar-decoration.decoration.vue';
import XDialog from './avatar-decoration.dialog.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
@ -68,14 +69,24 @@ misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
loading.value = false; loading.value = false;
}); });
async function openDecoration(avatarDecoration, index?: number) { function openAttachedDecoration(index: number) {
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration.dialog.vue').then(x => x.default), { openDecoration(avatarDecorations.value.find(d => d.id === $i.avatarDecorations[index].id) ?? { id: '', url: '', name: '?', roleIdsThatCanBeUsedThisDecoration: [] }, index);
}
async function openDecoration(avatarDecoration: {
id: string;
url: string;
name: string;
roleIdsThatCanBeUsedThisDecoration: string[];
}, index?: number) {
const { dispose } = os.popup(XDialog, {
decoration: avatarDecoration, decoration: avatarDecoration,
usingIndex: index, usingIndex: index ?? null,
}, { }, {
'attach': async (payload) => { 'attach': async (payload) => {
const decoration = { const decoration = {
id: avatarDecoration.id, id: avatarDecoration.id,
url: avatarDecoration.url,
angle: payload.angle, angle: payload.angle,
flipH: payload.flipH, flipH: payload.flipH,
offsetX: payload.offsetX, offsetX: payload.offsetX,
@ -90,13 +101,14 @@ async function openDecoration(avatarDecoration, index?: number) {
'update': async (payload) => { 'update': async (payload) => {
const decoration = { const decoration = {
id: avatarDecoration.id, id: avatarDecoration.id,
url: avatarDecoration.url,
angle: payload.angle, angle: payload.angle,
flipH: payload.flipH, flipH: payload.flipH,
offsetX: payload.offsetX, offsetX: payload.offsetX,
offsetY: payload.offsetY, offsetY: payload.offsetY,
}; };
const update = [...$i.avatarDecorations]; const update = [...$i.avatarDecorations];
update[index] = decoration; update[index!] = decoration;
await os.apiWithDialog('i/update', { await os.apiWithDialog('i/update', {
avatarDecorations: update, avatarDecorations: update,
}); });
@ -104,7 +116,7 @@ async function openDecoration(avatarDecoration, index?: number) {
}, },
'detach': async () => { 'detach': async () => {
const update = [...$i.avatarDecorations]; const update = [...$i.avatarDecorations];
update.splice(index, 1); update.splice(index!, 1);
await os.apiWithDialog('i/update', { await os.apiWithDialog('i/update', {
avatarDecorations: update, avatarDecorations: update,
}); });

View File

@ -43,7 +43,7 @@ async function edit() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkWatermarkEditorDialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkWatermarkEditorDialog.vue')), {
preset: deepClone(props.preset), preset: deepClone(props.preset),
}, { }, {
ok: (preset: WatermarkPreset) => { ok: (preset) => {
emit('updatePreset', preset); emit('updatePreset', preset);
}, },
closed: () => dispose(), closed: () => dispose(),

View File

@ -74,7 +74,7 @@ import { instance } from '@/instance.js';
const $i = ensureSignin(); const $i = ensureSignin();
const emailAddress = ref($i.email); const emailAddress = ref($i.email ?? '');
const onChangeReceiveAnnouncementEmail = (v) => { const onChangeReceiveAnnouncementEmail = (v) => {
misskeyApi('i/update', { misskeyApi('i/update', {

View File

@ -188,6 +188,8 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
}]); }]);
onMounted(() => { onMounted(() => {
if (el.value == null) return; // TS
ro.observe(el.value); ro.observe(el.value);
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
@ -198,6 +200,8 @@ onMounted(() => {
}); });
onActivated(() => { onActivated(() => {
if (el.value == null) return; // TS
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
if (!narrow.value && currentPage.value?.route.name == null) { if (!narrow.value && currentPage.value?.route.name == null) {
@ -215,7 +219,7 @@ watch(router.currentRef, (to) => {
} }
}); });
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); const emailNotConfigured = computed(() => $i && instance.enableEmail && ($i.email == null || !$i.emailVerified));
provideMetadataReceiver((metadataGetter) => { provideMetadataReceiver((metadataGetter) => {
const info = metadataGetter(); const info = metadataGetter();

View File

@ -159,8 +159,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub"> <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub">
<div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div> <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div>
<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -160,10 +160,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label><SearchLabel>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</SearchLabel></template> <template #label><SearchLabel>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</SearchLabel></template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null"> <MkSelect
<option :value="null">{{ i18n.ts.none }}</option> :items="[{
<option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option> value: null,
<option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option> label: i18n.ts.none
}, {
value: 'relative',
label: i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod
}, {
value: 'absolute',
label: i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime
}] as const" :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null"
>
</MkSelect> </MkSelect>
<MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore_selection"> <MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore_selection">
@ -262,7 +270,7 @@ const makeNotesFollowersOnlyBefore_presets = [
const makeNotesFollowersOnlyBefore_isCustomMode = ref( const makeNotesFollowersOnlyBefore_isCustomMode = ref(
makeNotesFollowersOnlyBefore.value != null && makeNotesFollowersOnlyBefore.value != null &&
makeNotesFollowersOnlyBefore.value < 0 && makeNotesFollowersOnlyBefore.value < 0 &&
!makeNotesFollowersOnlyBefore_presets.some((preset) => preset.value === makeNotesFollowersOnlyBefore.value) !makeNotesFollowersOnlyBefore_presets.some((preset) => preset.value === makeNotesFollowersOnlyBefore.value),
); );
const makeNotesFollowersOnlyBefore_selection = computed({ const makeNotesFollowersOnlyBefore_selection = computed({
@ -270,14 +278,14 @@ const makeNotesFollowersOnlyBefore_selection = computed({
set(value) { set(value) {
makeNotesFollowersOnlyBefore_isCustomMode.value = value === 'custom'; makeNotesFollowersOnlyBefore_isCustomMode.value = value === 'custom';
if (value !== 'custom') makeNotesFollowersOnlyBefore.value = value; if (value !== 'custom') makeNotesFollowersOnlyBefore.value = value;
} },
}); });
const makeNotesFollowersOnlyBefore_customMonths = computed({ const makeNotesFollowersOnlyBefore_customMonths = computed({
get: () => makeNotesFollowersOnlyBefore.value ? Math.abs(makeNotesFollowersOnlyBefore.value) / (30 * 24 * 60 * 60) : null, get: () => makeNotesFollowersOnlyBefore.value ? Math.abs(makeNotesFollowersOnlyBefore.value) / (30 * 24 * 60 * 60) : null,
set(value) { set(value) {
if (value != null && value > 0) makeNotesFollowersOnlyBefore.value = -Math.abs(Math.floor(Number(value))) * 30 * 24 * 60 * 60; if (value != null && value > 0) makeNotesFollowersOnlyBefore.value = -Math.abs(Math.floor(Number(value))) * 30 * 24 * 60 * 60;
} },
}); });
const makeNotesHiddenBefore_type = computed(() => { const makeNotesHiddenBefore_type = computed(() => {
@ -303,7 +311,7 @@ const makeNotesHiddenBefore_presets = [
const makeNotesHiddenBefore_isCustomMode = ref( const makeNotesHiddenBefore_isCustomMode = ref(
makeNotesHiddenBefore.value != null && makeNotesHiddenBefore.value != null &&
makeNotesHiddenBefore.value < 0 && makeNotesHiddenBefore.value < 0 &&
!makeNotesHiddenBefore_presets.some((preset) => preset.value === makeNotesHiddenBefore.value) !makeNotesHiddenBefore_presets.some((preset) => preset.value === makeNotesHiddenBefore.value),
); );
const makeNotesHiddenBefore_selection = computed({ const makeNotesHiddenBefore_selection = computed({
@ -311,14 +319,14 @@ const makeNotesHiddenBefore_selection = computed({
set(value) { set(value) {
makeNotesHiddenBefore_isCustomMode.value = value === 'custom'; makeNotesHiddenBefore_isCustomMode.value = value === 'custom';
if (value !== 'custom') makeNotesHiddenBefore.value = value; if (value !== 'custom') makeNotesHiddenBefore.value = value;
} },
}); });
const makeNotesHiddenBefore_customMonths = computed({ const makeNotesHiddenBefore_customMonths = computed({
get: () => makeNotesHiddenBefore.value ? Math.abs(makeNotesHiddenBefore.value) / (30 * 24 * 60 * 60) : null, get: () => makeNotesHiddenBefore.value ? Math.abs(makeNotesHiddenBefore.value) / (30 * 24 * 60 * 60) : null,
set(value) { set(value) {
if (value != null && value > 0) makeNotesHiddenBefore.value = -Math.abs(Math.floor(Number(value))) * 30 * 24 * 60 * 60; if (value != null && value > 0) makeNotesHiddenBefore.value = -Math.abs(Math.floor(Number(value))) * 30 * 24 * 60 * 60;
} },
}); });
watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => { watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => {

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/profile" :label="i18n.ts.profile" :keywords="['profile']" icon="ti ti-user"> <SearchMarker path="/settings/profile" :label="i18n.ts.profile" :keywords="['profile']" icon="ti ti-user">
<div class="_gaps_m"> <div class="_gaps_m">
<div class="_panel"> <div class="_panel">
<div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> <div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : '' }">
<div :class="$style.bannerEdit"> <div :class="$style.bannerEdit">
<SearchMarker :keywords="['banner', 'change']"> <SearchMarker :keywords="['banner', 'change']">
<MkButton primary rounded @click="changeBanner"><SearchLabel>{{ i18n.ts._profile.changeBanner }}</SearchLabel></MkButton> <MkButton primary rounded @click="changeBanner"><SearchLabel>{{ i18n.ts._profile.changeBanner }}</SearchLabel></MkButton>

View File

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

View File

@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkCodeEditor> </MkCodeEditor>
<div class="_buttons"> <div class="_buttons">
<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> <MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" inline @click="() => previewTheme(installThemeCode!)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> <MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" primary inline @click="() => install(installThemeCode!)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
</div> </div>
</div> </div>
</template> </template>
@ -39,7 +39,7 @@ async function install(code: string): Promise<void> {
}); });
installThemeCode.value = null; installThemeCode.value = null;
router.push('/settings/theme'); router.push('/settings/theme');
} catch (err) { } catch (err: any) {
switch (err.message.toLowerCase()) { switch (err.message.toLowerCase()) {
case 'this theme is already installed': case 'this theme is already installed':
os.alert({ os.alert({

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._theme.code }}</template> <template #label>{{ i18n.ts._theme.code }}</template>
<template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template> <template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template>
</MkTextarea> </MkTextarea>
<MkButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" danger @click="uninstall()"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton> <MkButton v-if="!builtinThemes.some(t => t.id == selectedTheme!.id)" danger @click="uninstall()"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
</template> </template>
</div> </div>
</template> </template>

View File

@ -205,8 +205,8 @@ import { computed, ref, watch } from 'vue';
import JSON5 from 'json5'; import JSON5 from 'json5';
import defaultLightTheme from '@@/themes/l-light.json5'; import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5'; import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { Theme } from '@/theme.js';
import { isSafeMode } from '@@/js/config.js'; import { isSafeMode } from '@@/js/config.js';
import type { Theme } from '@/theme.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
@ -275,6 +275,7 @@ async function toggleDarkMode() {
const value = !store.r.darkMode.value; const value = !store.r.darkMode.value;
if (syncDeviceDarkMode.value) { if (syncDeviceDarkMode.value) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.switchDarkModeManuallyWhenSyncEnabledConfirm({ x: i18n.ts.syncDeviceDarkMode }), text: i18n.tsx.switchDarkModeManuallyWhenSyncEnabledConfirm({ x: i18n.ts.syncDeviceDarkMode }),
}); });
if (canceled) return; 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(() => []); const headerActions = computed(() => []);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const headerTabs = computed(() => []); const headerTabs = computed(() => []);
definePage(() => ({ definePage(() => ({

View File

@ -61,7 +61,7 @@ const event_reaction = ref(true);
const event_mention = ref(true); const event_mention = ref(true);
async function create(): Promise<void> { async function create(): Promise<void> {
const events = []; const events: string[] = [];
if (event_follow.value) events.push('follow'); if (event_follow.value) events.push('follow');
if (event_followed.value) events.push('followed'); if (event_followed.value) events.push('followed');
if (event_note.value) events.push('note'); if (event_note.value) events.push('note');

View File

@ -52,7 +52,7 @@ async function post() {
const headerActions = computed(() => [{ const headerActions = computed(() => [{
icon: 'ti ti-dots', icon: 'ti ti-dots',
label: i18n.ts.more, text: i18n.ts.more,
handler: (ev: MouseEvent) => { handler: (ev: MouseEvent) => {
os.popupMenu([{ os.popupMenu([{
text: i18n.ts.embed, text: i18n.ts.embed,

View File

@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.backgroundColor }}</template> <template #label>{{ i18n.ts.backgroundColor }}</template>
<div class="cwepdizn-colors"> <div class="cwepdizn-colors">
<div class="row"> <div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)"> <button v-for="color in bgColors.filter(x => x.kind === 'light')" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
<div class="preview" :style="{ background: color.forPreview }"></div> <div class="preview" :style="{ background: color.forPreview }"></div>
</button> </button>
</div> </div>
<div class="row"> <div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)"> <button v-for="color in bgColors.filter(x => x.kind === 'dark')" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
<div class="preview" :style="{ background: color.forPreview }"></div> <div class="preview" :style="{ background: color.forPreview }"></div>
</button> </button>
</div> </div>
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.accentColor }}</template> <template #label>{{ i18n.ts.accentColor }}</template>
<div class="cwepdizn-colors"> <div class="cwepdizn-colors">
<div class="row"> <div class="row">
<button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)"> <button v-for="color in accentColors" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)">
<div class="preview" :style="{ background: color }"></div> <div class="preview" :style="{ background: color }"></div>
</button> </button>
</div> </div>
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.textColor }}</template> <template #label>{{ i18n.ts.textColor }}</template>
<div class="cwepdizn-colors"> <div class="cwepdizn-colors">
<div class="row"> <div class="row">
<button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)"> <button v-for="color in fgColors" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)">
<div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div> <div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
</button> </button>
</div> </div>
@ -75,17 +75,17 @@ SPDX-License-Identifier: AGPL-3.0-only
import { watch, ref, computed } from 'vue'; import { watch, ref, computed } from 'vue';
import { toUnicode } from 'punycode.js'; import { toUnicode } from 'punycode.js';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { genId } from '@/utility/id.js';
import JSON5 from 'json5'; import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5'; import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5'; import darkTheme from '@@/themes/_dark.json5';
import { host } from '@@/js/config.js'; import { host } from '@@/js/config.js';
import type { Theme } from '@/theme.js'; import type { Theme } from '@/theme.js';
import { genId } from '@/utility/id.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import { $i } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { addTheme, applyTheme } from '@/theme.js'; import { addTheme, applyTheme } from '@/theme.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
@ -94,6 +94,8 @@ import { useLeaveGuard } from '@/composables/use-leave-guard.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const $i = ensureSignin();
const bgColors = [ const bgColors = [
{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' }, { color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
@ -123,12 +125,15 @@ const fgColors = [
{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' }, { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
]; ];
const theme = ref<Partial<Theme>>({ const theme = ref<Theme>({
id: genId(),
name: 'untitled',
author: `@${$i.username}@${toUnicode(host)}`,
base: 'light', base: 'light',
props: lightTheme.props, props: lightTheme.props,
}); });
const description = ref<string | null>(null); const description = ref<string | null>(null);
const themeCode = ref<string | null>(null); const themeCode = ref<string>('');
const changed = ref(false); const changed = ref(false);
useLeaveGuard(changed); useLeaveGuard(changed);
@ -194,7 +199,6 @@ async function saveAs() {
theme.value.id = genId(); theme.value.id = genId();
theme.value.name = name; theme.value.name = name;
theme.value.author = `@${$i.username}@${toUnicode(host)}`;
if (description.value) theme.value.desc = description.value; if (description.value) theme.value.desc = description.value;
await addTheme(theme.value); await addTheme(theme.value);
applyTheme(theme.value); applyTheme(theme.value);

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div> <div>
<MkPagination v-slot="{items}" :paginator="type === 'following' ? followingPaginator : followersPaginator" withControl> <MkPagination v-slot="{items}" :paginator="type === 'following' ? followingPaginator : followersPaginator" withControl>
<div :class="$style.users"> <div :class="$style.users">
<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :user="user"/> <MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee! : x.follower!)" :key="user.id" :user="user"/>
</div> </div>
</MkPagination> </MkPagination>
</div> </div>

View File

@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName class="name" :user="user" :nowrap="true"/> <MkUserName class="name" :user="user" :nowrap="true"/>
<div class="bottom"> <div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true"/></span> <span class="username"><MkAcct :user="user" :detail="true"/></span>
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <span v-if="user.isBot"><i class="ti ti-robot"></i></span>
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea"> <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
<i class="ti ti-edit"/> {{ i18n.ts.addMemo }} <i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
</button> </button>
@ -43,8 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :user="user" :nowrap="false" class="name"/> <MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom"> <div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true"/></span> <span class="username"><MkAcct :user="user" :detail="true"/></span>
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <span v-if="user.isBot"><i class="ti ti-robot"></i></span>
</div> </div>
</div> </div>
<div v-if="user.followedMessage != null" class="followedMessage"> <div v-if="user.followedMessage != null" class="followedMessage">
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFukidashi> </MkFukidashi>
</div> </div>
<div v-if="user.roles.length > 0" class="roles"> <div v-if="user.roles.length > 0" class="roles">
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color ?? '' }">
<MkA v-adaptive-bg :to="`/roles/${role.id}`"> <MkA v-adaptive-bg :to="`/roles/${role.id}`">
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/> <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
{{ role.name }} {{ role.name }}
@ -228,7 +228,7 @@ const bannerEl = ref<null | HTMLElement>(null);
const memoTextareaEl = ref<null | HTMLElement>(null); const memoTextareaEl = ref<null | HTMLElement>(null);
const memoDraft = ref(props.user.memo); const memoDraft = ref(props.user.memo);
const isEditingMemo = ref(false); const isEditingMemo = ref(false);
const moderationNote = ref(props.user.moderationNote); const moderationNote = ref(props.user.moderationNote ?? '');
const editModerationNote = ref(false); const editModerationNote = ref(false);
watch(moderationNote, async () => { watch(moderationNote, async () => {
@ -249,7 +249,7 @@ const style = computed(() => {
}); });
const age = computed(() => { const age = computed(() => {
return calcAge(props.user.birthday); return props.user.birthday ? calcAge(props.user.birthday) : NaN;
}); });
function menu(ev: MouseEvent) { function menu(ev: MouseEvent) {

View File

@ -48,7 +48,7 @@ import FormSection from '@/components/form/section.vue';
import MkObjectView from '@/components/MkObjectView.vue'; import MkObjectView from '@/components/MkObjectView.vue';
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.User; user: Misskey.entities.UserDetailed & { isModerator?: boolean; };
}>(); }>();
const moderator = computed(() => props.user.isModerator ?? false); const moderator = computed(() => props.user.isModerator ?? false);

View File

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

View File

@ -23,6 +23,7 @@ export type Theme = {
author: string; author: string;
desc?: string; desc?: string;
base?: 'dark' | 'light'; base?: 'dark' | 'light';
kind?: 'dark' | 'light'; // legacy
props: Record<string, string>; props: Record<string, string>;
codeHighlighter?: { codeHighlighter?: {
base: BundledTheme; base: BundledTheme;

View File

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

View File

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

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.body"> <div :class="$style.body">
<div :class="$style.top"> <div :class="$style.top">
<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu"> <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/> <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/>
</button> </button>
<button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode"> <button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode">
<i class="ti ti-bolt ti-fw"></i> <i class="ti ti-bolt ti-fw"></i>

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
mode="default" mode="default"
> >
<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse"> <MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
<span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : null }"> <span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : '' }">
<img :class="$style.icon" :src="getInstanceIcon(instance)" alt=""/> <img :class="$style.icon" :src="getInstanceIcon(instance)" alt=""/>
<MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace"> <MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace">
{{ instance.host }} {{ instance.host }}
@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import MkMarqueeText from '@/components/MkMarqueeText.vue'; import MkMarqueeText from '@/components/MkMarqueeText.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { useInterval } from '@@/js/use-interval.js';
import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
const props = defineProps<{ const props = defineProps<{
@ -44,7 +44,7 @@ const props = defineProps<{
marqueeDuration?: number; marqueeDuration?: number;
marqueeReverse?: boolean; marqueeReverse?: boolean;
oneByOneInterval?: number; oneByOneInterval?: number;
refreshIntervalSec?: number; refreshIntervalSec: number;
}>(); }>();
const instances = ref<Misskey.entities.FederationInstance[]>([]); const instances = ref<Misskey.entities.FederationInstance[]>([]);

View File

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

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse"> <MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
<span v-for="note in notes" :key="note.id" :class="$style.item"> <span v-for="note in notes" :key="note.id" :class="$style.item">
<img :class="$style.avatar" :src="note.user.avatarUrl" decoding="async"/> <img v-if="note.user.avatarUrl" :class="$style.avatar" :src="note.user.avatarUrl" decoding="async"/>
<MkA :class="$style.text" :to="notePage(note)"> <MkA :class="$style.text" :to="notePage(note)">
<Mfm :text="getNoteSummary(note)" :plain="true" :nowrap="true"/> <Mfm :text="getNoteSummary(note)" :plain="true" :nowrap="true"/>
</MkA> </MkA>
@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import MkMarqueeText from '@/components/MkMarqueeText.vue'; import MkMarqueeText from '@/components/MkMarqueeText.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { useInterval } from '@@/js/use-interval.js';
import { getNoteSummary } from '@/utility/get-note-summary.js'; import { getNoteSummary } from '@/utility/get-note-summary.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
@ -45,7 +45,7 @@ const props = defineProps<{
marqueeDuration?: number; marqueeDuration?: number;
marqueeReverse?: boolean; marqueeReverse?: boolean;
oneByOneInterval?: number; oneByOneInterval?: number;
refreshIntervalSec?: number; refreshIntervalSec: number;
}>(); }>();
const notes = ref<Misskey.entities.Note[]>([]); const notes = ref<Misskey.entities.Note[]>([]);

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.title"> <div :class="$style.title">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
<span :class="$style.instanceTitle">{{ instance.name ?? host }}</span> <span :class="$style.instanceTitle">{{ instance.name ?? host }}</span>
</div> </div>
<div :class="$style.controls"> <div :class="$style.controls">

View File

@ -10,10 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.nonTitlebarArea"> <div :class="$style.nonTitlebarArea">
<XSidebar v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'left'"/> <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'"/> <XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/>
<XReloadSuggest v-if="shouldSuggestReload"/> <XReloadSuggestion v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/> <XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XAnnouncements v-if="$i"/> <XAnnouncements v-if="$i"/>
<XStatusBars/> <XStatusBars/>
@ -90,7 +90,7 @@ import XNavbarH from '@/ui/_common_/navbar-h.vue';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue'; import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue'; import XTitlebar from '@/ui/_common_/titlebar.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue'; import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggest from '@/ui/_common_/ReloadSuggest.vue'; import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.contents, !isMobile && prefer.r.showTitlebar.value ? $style.withSidebarAndTitlebar : null]" @contextmenu.stop="onContextmenu"> <div :class="[$style.contents, !isMobile && prefer.r.showTitlebar.value ? $style.withSidebarAndTitlebar : null]" @contextmenu.stop="onContextmenu">
<div> <div>
<XReloadSuggest v-if="shouldSuggestReload"/> <XReloadSuggestion v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/> <XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XAnnouncements v-if="$i"/> <XAnnouncements v-if="$i"/>
<XStatusBars :class="$style.statusbars"/> <XStatusBars :class="$style.statusbars"/>
@ -39,7 +39,7 @@ import XCommon from './_common_/common.vue';
import type { PageMetadata } from '@/page.js'; import type { PageMetadata } from '@/page.js';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue'; import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue'; import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggest from '@/ui/_common_/ReloadSuggest.vue'; import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue'; import XTitlebar from '@/ui/_common_/titlebar.vue';
import XSidebar from '@/ui/_common_/navbar.vue'; import XSidebar from '@/ui/_common_/navbar.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';

View File

@ -12,7 +12,7 @@ export async function lookupUser() {
const { canceled, result } = await os.inputText({ const { canceled, result } = await os.inputText({
title: i18n.ts.usernameOrUserId, title: i18n.ts.usernameOrUserId,
}); });
if (canceled) return; if (canceled || result == null) return;
const show = (user) => { const show = (user) => {
os.pageWindow(`/admin/user/${user.id}`); os.pageWindow(`/admin/user/${user.id}`);
@ -46,13 +46,13 @@ export async function lookupUserByEmail() {
title: i18n.ts.emailAddress, title: i18n.ts.emailAddress,
type: 'email', type: 'email',
}); });
if (canceled) return; if (canceled || result == null) return;
try { try {
const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result }); const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result });
os.pageWindow(`/admin/user/${user.id}`); os.pageWindow(`/admin/user/${user.id}`);
} catch (err) { } catch (err: any) {
if (err.code === 'USER_NOT_FOUND') { if (err.code === 'USER_NOT_FOUND') {
os.alert({ os.alert({
type: 'error', type: 'error',

View File

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

View File

@ -8,9 +8,10 @@ import type { Plugin } from 'chart.js';
export const chartVLine = (vLineColor: string) => ({ export const chartVLine = (vLineColor: string) => ({
id: 'vLine', id: 'vLine',
beforeDraw(chart, args, options) { beforeDraw(chart, args, options) {
if (chart.tooltip?._active?.length) { const tooltip = chart.tooltip as any;
if (tooltip?._active?.length) {
const ctx = chart.ctx; const ctx = chart.ctx;
const xs = chart.tooltip._active.map(a => a.element.x); const xs = tooltip._active.map(a => a.element.x);
const x = xs.reduce((a, b) => a + b, 0) / xs.length; const x = xs.reduce((a, b) => a + b, 0) / xs.length;
const topY = chart.scales.y.top; const topY = chart.scales.y.top;
const bottomY = chart.scales.y.bottom; const bottomY = chart.scales.y.bottom;

View File

@ -27,7 +27,7 @@ export async function load() {
scope: ['clickerGame'], scope: ['clickerGame'],
key: 'saveData', key: 'saveData',
}); });
} catch (err) { } catch (err: any) {
if (err.code === 'NO_SUCH_KEY') { if (err.code === 'NO_SUCH_KEY') {
saveData.value = { saveData.value = {
gameVersion: 2, gameVersion: 2,
@ -43,20 +43,6 @@ export async function load() {
} }
throw err; 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() { 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 clips = await clipsCache.fetch();
const menu: MenuItem[] = [...clips.map(clip => ({ const menu: MenuItem[] = [...clips.map(clip => ({
@ -179,7 +179,7 @@ export function getNoteMenu(props: {
translating: Ref<boolean>; translating: Ref<boolean>;
currentClip?: Misskey.entities.Clip; currentClip?: Misskey.entities.Clip;
}) { }) {
const appearNote = getAppearNote(props.note); const appearNote = getAppearNote(props.note) ?? props.note;
const link = appearNote.url ?? appearNote.uri; const link = appearNote.url ?? appearNote.uri;
const cleanups = [] as (() => void)[]; const cleanups = [] as (() => void)[];
@ -554,7 +554,7 @@ export function getRenoteMenu(props: {
renoteButton: ShallowRef<HTMLElement | null | undefined>; renoteButton: ShallowRef<HTMLElement | null | undefined>;
mock?: boolean; mock?: boolean;
}) { }) {
const appearNote = getAppearNote(props.note); const appearNote = getAppearNote(props.note) ?? props.note;
const channelRenoteItems: MenuItem[] = []; const channelRenoteItems: MenuItem[] = [];
const normalRenoteItems: MenuItem[] = []; const normalRenoteItems: MenuItem[] = [];

View File

@ -8,6 +8,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { mainRouter } from '@/router.js'; import { mainRouter } from '@/router.js';
import { acct } from '@/filters/user';
export async function lookup(router?: Router) { export async function lookup(router?: Router) {
const _router = router ?? mainRouter; const _router = router ?? mainRouter;
@ -38,7 +39,7 @@ export async function lookup(router?: Router) {
if (res.type === 'User') { if (res.type === 'User') {
_router.push('/@:acct/:page?', { _router.push('/@:acct/:page?', {
params: { params: {
acct: `${res.object.username}@${res.object.host}`, acct: acct(res.object),
}, },
}); });
} else if (res.type === 'Note') { } else if (res.type === 'Note') {

View File

@ -20,8 +20,8 @@ export function popout(path: string, w?: HTMLElement) {
} else { } else {
const width = 400; const width = 400;
const height = 500; const height = 500;
const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2); const x = window.top == null ? 0 : window.top.outerHeight / 2 + window.top.screenY - (height / 2);
const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2); const y = window.top == null ? 0 : window.top.outerWidth / 2 + window.top.screenX - (width / 2);
window.open(url, url, window.open(url, url,
`width=${width}, height=${height}, top=${x}, left=${y}`); `width=${width}, height=${height}, top=${x}, left=${y}`);
} }

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/*
export class StickySidebar { export class StickySidebar {
private lastScrollTop = 0; private lastScrollTop = 0;
private container: HTMLElement; private container: HTMLElement;
@ -53,3 +54,4 @@ export class StickySidebar {
this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop; this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
} }
} }
*/

View File

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

View File

@ -316,7 +316,6 @@ describe('AiScript UI API', () => {
describe('textInput', () => { describe('textInput', () => {
test.concurrent('all options', async () => { test.concurrent('all options', async () => {
// https://github.com/aiscript-dev/aiscript/pull/948
const { root, get, outputs } = await exe(` const { root, get, outputs } = await exe(`
let text_input = Ui:C:textInput({ let text_input = Ui:C:textInput({
onInput: print onInput: print
@ -357,7 +356,6 @@ describe('AiScript UI API', () => {
describe('textarea', () => { describe('textarea', () => {
test.concurrent('all options', async () => { test.concurrent('all options', async () => {
// https://github.com/aiscript-dev/aiscript/pull/948
const { root, get, outputs } = await exe(` const { root, get, outputs } = await exe(`
let textarea = Ui:C:textarea({ let textarea = Ui:C:textarea({
onInput: print onInput: print
@ -398,7 +396,6 @@ describe('AiScript UI API', () => {
describe('numberInput', () => { describe('numberInput', () => {
test.concurrent('all options', async () => { test.concurrent('all options', async () => {
// https://github.com/aiscript-dev/aiscript/pull/948
const { root, get, outputs } = await exe(` const { root, get, outputs } = await exe(`
let number_input = Ui:C:numberInput({ let number_input = Ui:C:numberInput({
onInput: print onInput: print
@ -560,7 +557,6 @@ describe('AiScript UI API', () => {
describe('switch', () => { describe('switch', () => {
test.concurrent('all options', async () => { test.concurrent('all options', async () => {
// https://github.com/aiscript-dev/aiscript/pull/948
const { root, get, outputs } = await exe(` const { root, get, outputs } = await exe(`
let switch = Ui:C:switch({ let switch = Ui:C:switch({
onChange: print onChange: print
@ -601,7 +597,6 @@ describe('AiScript UI API', () => {
describe('select', () => { describe('select', () => {
test.concurrent('all options', async () => { test.concurrent('all options', async () => {
// https://github.com/aiscript-dev/aiscript/pull/948
const { root, get, outputs } = await exe(` const { root, get, outputs } = await exe(`
let select = Ui:C:select({ let select = Ui:C:select({
items: [ items: [