Merge branch 'refactor/types' into 'develop'

Refactor/types

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10737
This commit is contained in:
naskya 2024-04-17 20:21:52 +00:00
commit bce88ec199
156 changed files with 2200 additions and 1458 deletions

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": { "organizeImports": {
"enabled": true "enabled": false
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
@ -21,7 +21,8 @@
"useImportType": "warn", "useImportType": "warn",
"useShorthandFunctionType": "warn", "useShorthandFunctionType": "warn",
"useTemplate": "warn", "useTemplate": "warn",
"noNonNullAssertion": "off" "noNonNullAssertion": "off",
"useNodejsImportProtocol": "off"
} }
} }
} }

View File

@ -33,8 +33,10 @@ import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
import { packedEmojiSchema } from "@/models/schema/emoji.js"; import { packedEmojiSchema } from "@/models/schema/emoji.js";
import { packedNoteEdit } from "@/models/schema/note-edit.js"; import { packedNoteEdit } from "@/models/schema/note-edit.js";
import { packedNoteFileSchema } from "@/models/schema/note-file.js"; import { packedNoteFileSchema } from "@/models/schema/note-file.js";
import { packedAbuseUserReportSchema } from "@/models/schema/abuse-user-report.js";
export const refs = { export const refs = {
AbuseUserReport: packedAbuseUserReportSchema,
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema, UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
MeDetailedOnly: packedMeDetailedOnlySchema, MeDetailedOnly: packedMeDetailedOnlySchema,

View File

@ -2,6 +2,7 @@ import { db } from "@/db/postgre.js";
import { Users } from "../index.js"; import { Users } from "../index.js";
import { AbuseUserReport } from "@/models/entities/abuse-user-report.js"; import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
import { awaitAll } from "@/prelude/await-all.js"; import { awaitAll } from "@/prelude/await-all.js";
import type { Packed } from "@/misc/schema.js";
export const AbuseUserReportRepository = db export const AbuseUserReportRepository = db
.getRepository(AbuseUserReport) .getRepository(AbuseUserReport)
@ -10,7 +11,7 @@ export const AbuseUserReportRepository = db
const report = const report =
typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ const packed: Packed<"AbuseUserReport"> = await awaitAll({
id: report.id, id: report.id,
createdAt: report.createdAt.toISOString(), createdAt: report.createdAt.toISOString(),
comment: report.comment, comment: report.comment,
@ -31,9 +32,10 @@ export const AbuseUserReportRepository = db
: null, : null,
forwarded: report.forwarded, forwarded: report.forwarded,
}); });
return packed;
}, },
packMany(reports: any[]) { packMany(reports: (AbuseUserReport["id"] | AbuseUserReport)[]) {
return Promise.all(reports.map((x) => this.pack(x))); return Promise.all(reports.map((x) => this.pack(x)));
}, },
}); });

View File

@ -40,6 +40,7 @@ export const ChannelRepository = db.getRepository(Channel).extend({
name: channel.name, name: channel.name,
description: channel.description, description: channel.description,
userId: channel.userId, userId: channel.userId,
bannerId: channel.bannerId,
bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null, bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
usersCount: channel.usersCount, usersCount: channel.usersCount,
notesCount: channel.notesCount, notesCount: channel.notesCount,

View File

@ -19,7 +19,9 @@ export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
createdAt: post.createdAt.toISOString(), createdAt: post.createdAt.toISOString(),
updatedAt: post.updatedAt.toISOString(), updatedAt: post.updatedAt.toISOString(),
userId: post.userId, userId: post.userId,
user: Users.pack(post.user || post.userId, me), user: Users.pack(post.user || post.userId, me, {
detail: true,
}),
title: post.title, title: post.title,
description: post.description, description: post.description,
fileIds: post.fileIds, fileIds: post.fileIds,

View File

@ -0,0 +1,69 @@
export const packedAbuseUserReportSchema = {
type: "object",
properties: {
id: {
type: "string",
optional: false,
nullable: false,
format: "id",
example: "xxxxxxxxxx",
},
createdAt: {
type: "string",
optional: false,
nullable: false,
format: "date-time",
},
comment: {
type: "string",
optional: false,
nullable: false,
},
resolved: {
type: "boolean",
optional: false,
nullable: false,
},
reporterId: {
type: "string",
optional: false,
nullable: false,
format: "id",
},
targetUserId: {
type: "string",
optional: false,
nullable: false,
format: "id",
},
assigneeId: {
type: "string",
optional: false,
nullable: true,
format: "id",
},
reporter: {
type: "object",
optional: false,
nullable: false,
ref: "UserDetailed",
},
targetUser: {
type: "object",
optional: false,
nullable: false,
ref: "UserDetailed",
},
assignee: {
type: "object",
optional: true,
nullable: true,
ref: "UserDetailed",
},
forwarded: {
type: "boolean",
optional: false,
nullable: false,
},
},
} as const;

View File

@ -36,6 +36,13 @@ export const packedChannelSchema = {
nullable: true, nullable: true,
optional: false, optional: false,
}, },
bannerId: {
type: "string",
optional: false,
nullable: true,
format: "id",
example: "xxxxxxxxxx",
},
notesCount: { notesCount: {
type: "number", type: "number",
nullable: false, nullable: false,
@ -57,5 +64,10 @@ export const packedChannelSchema = {
optional: false, optional: false,
format: "id", format: "id",
}, },
hasUnreadNote: {
type: "boolean",
optional: true,
nullable: false,
},
}, },
} as const; } as const;

View File

@ -38,7 +38,7 @@ export const packedGalleryPostSchema = {
}, },
user: { user: {
type: "object", type: "object",
ref: "UserLite", ref: "UserDetailed",
optional: false, optional: false,
nullable: false, nullable: false,
}, },
@ -79,5 +79,15 @@ export const packedGalleryPostSchema = {
optional: false, optional: false,
nullable: false, nullable: false,
}, },
isLiked: {
type: "boolean",
optional: true,
nullable: false,
},
likedCount: {
type: "number",
optional: false,
nullable: false,
},
}, },
} as const; } as const;

View File

@ -16,68 +16,7 @@ export const meta = {
type: "object", type: "object",
optional: false, optional: false,
nullable: false, nullable: false,
properties: { ref: "AbuseUserReport",
id: {
type: "string",
nullable: false,
optional: false,
format: "id",
example: "xxxxxxxxxx",
},
createdAt: {
type: "string",
nullable: false,
optional: false,
format: "date-time",
},
comment: {
type: "string",
nullable: false,
optional: false,
},
resolved: {
type: "boolean",
nullable: false,
optional: false,
example: false,
},
reporterId: {
type: "string",
nullable: false,
optional: false,
format: "id",
},
targetUserId: {
type: "string",
nullable: false,
optional: false,
format: "id",
},
assigneeId: {
type: "string",
nullable: true,
optional: false,
format: "id",
},
reporter: {
type: "object",
nullable: false,
optional: false,
ref: "User",
},
targetUser: {
type: "object",
nullable: false,
optional: false,
ref: "User",
},
assignee: {
type: "object",
nullable: true,
optional: true,
ref: "User",
},
},
}, },
}, },
} as const; } as const;

View File

@ -83,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => {
await Channels.update(channel.id, { await Channels.update(channel.id, {
...(ps.name !== undefined ? { name: ps.name } : {}), ...(ps.name !== undefined ? { name: ps.name } : {}),
...(ps.description !== undefined ? { description: ps.description } : {}), ...(ps.description !== undefined ? { description: ps.description } : {}),
...(banner ? { bannerId: banner.id } : {}), ...(banner ? { bannerId: banner.id } : { bannerId: null }),
}); });
return await Channels.pack(channel.id, me); return await Channels.pack(channel.id, me);

View File

@ -1,3 +1,4 @@
// biome-ignore lint/suspicious/noExplicitAny:
type FIXME = any; type FIXME = any;
declare const _LANGS_: string[][]; declare const _LANGS_: string[][];

6
packages/client/@types/window.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare global {
interface Window {
__misskey_input_ref__?: HTMLInputElement | null;
}
}
export type {};

View File

@ -72,13 +72,14 @@ import MkSwitch from "@/components/form/switch.vue";
import MkKeyValue from "@/components/MkKeyValue.vue"; import MkKeyValue from "@/components/MkKeyValue.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import type { entities } from "firefish-js";
const props = defineProps<{ const props = defineProps<{
report: any; report: entities.AbuseUserReport;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "resolved", reportId: string): void; resolved: [reportId: string];
}>(); }>();
const forward = ref(props.report.forwarded); const forward = ref(props.report.forwarded);

View File

@ -18,8 +18,8 @@ import { initChart } from "@/scripts/init-chart";
initChart(); initChart();
const rootEl = shallowRef<HTMLDivElement>(); const rootEl = shallowRef<HTMLDivElement | null>(null);
const chartEl = shallowRef<HTMLCanvasElement>(); const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const now = new Date(); const now = new Date();
let chartInstance: Chart | null = null; let chartInstance: Chart | null = null;
const fetching = ref(true); const fetching = ref(true);
@ -33,8 +33,8 @@ async function renderActiveUsersChart() {
chartInstance.destroy(); chartInstance.destroy();
} }
const wide = rootEl.value.offsetWidth > 700; const wide = rootEl.value!.offsetWidth > 700;
const narrow = rootEl.value.offsetWidth < 400; const narrow = rootEl.value!.offsetWidth < 400;
const weeks = wide ? 50 : narrow ? 10 : 25; const weeks = wide ? 50 : narrow ? 10 : 25;
const chartLimit = 7 * weeks; const chartLimit = 7 * weeks;

View File

@ -35,9 +35,10 @@ import MkSparkle from "@/components/MkSparkle.vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import * as os from "@/os"; import * as os from "@/os";
import type { entities } from "firefish-js";
const props = defineProps<{ const props = defineProps<{
announcement: Announcement; announcement: entities.Announcement;
}>(); }>();
const { id, text, title, imageUrl, isGoodNews } = props.announcement; const { id, text, title, imageUrl, isGoodNews } = props.announcement;
@ -45,7 +46,7 @@ const { id, text, title, imageUrl, isGoodNews } = props.announcement;
const modal = shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const gotIt = () => { const gotIt = () => {
modal.value.close(); modal.value!.close();
os.api("i/read-announcement", { announcementId: id }); os.api("i/read-announcement", { announcementId: id });
}; };
</script> </script>

View File

@ -62,7 +62,7 @@
<span v-else class="emoji">{{ emoji.emoji }}</span> <span v-else class="emoji">{{ emoji.emoji }}</span>
<span <span
class="name" class="name"
v-html="emoji.name.replace(q, `<b>${q}</b>`)" v-html="q ? emoji.name.replace(q, `<b>${q}</b>`) : emoji.name"
></span> ></span>
<span v-if="emoji.aliasOf" class="alias" <span v-if="emoji.aliasOf" class="alias"
>({{ emoji.aliasOf }})</span >({{ emoji.aliasOf }})</span
@ -107,7 +107,7 @@ interface EmojiDef {
emoji: string; emoji: string;
name: string; name: string;
aliasOf?: string; aliasOf?: string;
url?: string; url: string;
isCustomEmoji?: boolean; isCustomEmoji?: boolean;
} }

View File

@ -9,12 +9,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import * as os from "@/os"; import * as os from "@/os";
import type { entities } from "firefish-js";
const props = defineProps<{ const props = defineProps<{
userIds: string[]; userIds: string[];
}>(); }>();
const users = ref([]); const users = ref<entities.UserDetailed[]>([]);
onMounted(async () => { onMounted(async () => {
users.value = await os.api("users/show", { users.value = await os.api("users/show", {

View File

@ -16,7 +16,7 @@
v-else v-else
class="bghgjjyj _button" class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full, mini }" :class="{ inline, primary, gradate, danger, rounded, full, mini }"
:to="to" :to="to!"
@mousedown="onMousedown" @mousedown="onMousedown"
> >
<div ref="ripples" class="ripples"></div> <div ref="ripples" class="ripples"></div>
@ -36,6 +36,7 @@ const props = defineProps<{
gradate?: boolean; gradate?: boolean;
rounded?: boolean; rounded?: boolean;
inline?: boolean; inline?: boolean;
// FIXME: if `link`, `to` is necessary
link?: boolean; link?: boolean;
to?: string; to?: string;
autofocus?: boolean; autofocus?: boolean;
@ -47,7 +48,7 @@ const props = defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "click", payload: MouseEvent): void; click: [payload: MouseEvent];
}>(); }>();
const el = ref<HTMLElement | null>(null); const el = ref<HTMLElement | null>(null);
@ -61,11 +62,19 @@ onMounted(() => {
} }
}); });
function distance(p, q): number { function distance(
p: { x: number; y: number },
q: { x: number; y: number },
): number {
return Math.hypot(p.x - q.x, p.y - q.y); return Math.hypot(p.x - q.x, p.y - q.y);
} }
function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY): number { function calcCircleScale(
boxW: number,
boxH: number,
circleCenterX: number,
circleCenterY: number,
): number {
const origin = { x: circleCenterX, y: circleCenterY }; const origin = { x: circleCenterX, y: circleCenterY };
const dist1 = distance({ x: 0, y: 0 }, origin); const dist1 = distance({ x: 0, y: 0 }, origin);
const dist2 = distance({ x: boxW, y: 0 }, origin); const dist2 = distance({ x: boxW, y: 0 }, origin);
@ -79,8 +88,8 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
const ripple = document.createElement("div"); const ripple = document.createElement("div");
ripple.style.top = (evt.clientY - rect.top - 1).toString() + "px"; ripple.style.top = `${(evt.clientY - rect.top - 1).toString()}px`;
ripple.style.left = (evt.clientX - rect.left - 1).toString() + "px"; ripple.style.left = `${(evt.clientX - rect.left - 1).toString()}px`;
ripples.value!.appendChild(ripple); ripples.value!.appendChild(ripple);
@ -97,7 +106,7 @@ function onMousedown(evt: MouseEvent): void {
vibrate(10); vibrate(10);
window.setTimeout(() => { window.setTimeout(() => {
ripple.style.transform = "scale(" + scale / 2 + ")"; ripple.style.transform = `scale(${scale / 2})`;
}, 1); }, 1);
window.setTimeout(() => { window.setTimeout(() => {
ripple.style.transition = "all 1s ease"; ripple.style.transition = "all 1s ease";

View File

@ -50,7 +50,7 @@ const props = defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "update:modelValue", v: string | null): void; "update:modelValue": [v: string | null];
}>(); }>();
const available = ref(false); const available = ref(false);
@ -93,7 +93,9 @@ if (loaded) {
src: src.value, src: src.value,
}), }),
) )
).addEventListener("load", () => (available.value = true)); )
// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
.addEventListener("load", () => (available.value = true));
} }
function reset() { function reset() {

View File

@ -27,10 +27,11 @@ import { ref } from "vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { entities } from "firefish-js";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
channel: Record<string, any>; channel: entities.Channel;
full?: boolean; full?: boolean;
}>(), }>(),
{ {
@ -38,7 +39,7 @@ const props = withDefaults(
}, },
); );
const isFollowing = ref<boolean>(props.channel.isFollowing); const isFollowing = ref<boolean>(props.channel.isFollowing ?? false);
const wait = ref(false); const wait = ref(false);
async function onClick() { async function onClick() {

View File

@ -11,7 +11,7 @@
</div> </div>
</template> </template>
<template #default="{ items }"> <template #default="{ items }: { items: entities.Channel[] }">
<MkChannelPreview <MkChannelPreview
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
@ -29,14 +29,15 @@ import type { PagingOf } from "@/components/MkPagination.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const props = withDefaults( withDefaults(
defineProps<{ defineProps<{
pagination: PagingOf<entities.Channel>; pagination: PagingOf<entities.Channel>;
noGap?: boolean; noGap?: boolean;
extractor?: (item: any) => any; // TODO: this function is not used and may can be removed
extractor?: (item: entities.Channel) => entities.Channel;
}>(), }>(),
{ {
extractor: (item) => item, extractor: (item: entities.Channel) => item,
}, },
); );
</script> </script>

View File

@ -54,9 +54,10 @@
import { computed } from "vue"; import { computed } from "vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { entities } from "firefish-js";
const props = defineProps<{ const props = defineProps<{
channel: Record<string, any>; channel: entities.Channel;
}>(); }>();
const bannerStyle = computed(() => { const bannerStyle = computed(() => {

View File

@ -100,9 +100,9 @@ const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = (arr) => arr.map((x) => -x); const negate = (arr) => arr.map((x) => -x);
const alpha = (hex, a) => { const alpha = (hex, a) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
const r = parseInt(result[1], 16); const r = Number.parseInt(result[1], 16);
const g = parseInt(result[2], 16); const g = Number.parseInt(result[2], 16);
const b = parseInt(result[3], 16); const b = Number.parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${a})`; return `rgba(${r}, ${g}, ${b}, ${a})`;
}; };

View File

@ -28,11 +28,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue"; import type { Ref } from "vue";
import MkTooltip from "./MkTooltip.vue"; import MkTooltip from "./MkTooltip.vue";
const props = defineProps<{ const props = defineProps<{
showing: boolean; showing: Ref<boolean>;
x: number; x: number;
y: number; y: number;
title?: string; title?: string;

View File

@ -4,14 +4,14 @@
:class="{ :class="{
isMe: isMe(message), isMe: isMe(message),
isRead: message.groupId isRead: message.groupId
? message.reads.includes(me?.id) ? message.reads.includes(me!.id)
: message.isRead, : message.isRead,
}" }"
:to=" :to="
message.groupId message.groupId
? `/my/messaging/group/${message.groupId}` ? `/my/messaging/group/${message.groupId}`
: `/my/messaging/${acct.toString( : `/my/messaging/${acct.toString(
isMe(message) ? message.recipient : message.user, isMe(message) ? message.recipient! : message.user,
)}` )}`
" "
> >
@ -22,27 +22,27 @@
message.groupId message.groupId
? message.user ? message.user
: isMe(message) : isMe(message)
? message.recipient ? message.recipient!
: message.user : message.user
" "
:show-indicator="true" :show-indicator="true"
disable-link disable-link
/> />
<header v-if="message.groupId"> <header v-if="message.groupId">
<span class="name">{{ message.group.name }}</span> <span class="name">{{ message.group!.name }}</span>
<MkTime :time="message.createdAt" class="time" /> <MkTime :time="message.createdAt" class="time" />
</header> </header>
<header v-else> <header v-else>
<span class="name" <span class="name"
><MkUserName ><MkUserName
:user=" :user="
isMe(message) ? message.recipient : message.user isMe(message) ? message.recipient! : message.user
" "
/></span> /></span>
<span class="username" <span class="username"
>@{{ >@{{
acct.toString( acct.toString(
isMe(message) ? message.recipient : message.user, isMe(message) ? message.recipient! : message.user,
) )
}}</span }}</span
> >
@ -65,16 +65,16 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { acct } from "firefish-js"; import { acct, type entities } from "firefish-js";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { me } from "@/me"; import { me } from "@/me";
defineProps<{ defineProps<{
message: Record<string, any>; message: entities.MessagingMessage;
}>(); }>();
function isMe(message): boolean { function isMe(message: entities.MessagingMessage): boolean {
return message.userId === me?.id; return message.userId === me!.id;
} }
</script> </script>

View File

@ -29,6 +29,7 @@ if (props.lang != null && !(props.lang in Prism.languages)) {
const { lang } = props; const { lang } = props;
loadLanguage(props.lang).then( loadLanguage(props.lang).then(
// onLoaded // onLoaded
// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
() => (prismLang.value = lang), () => (prismLang.value = lang),
// onError // onError
() => {}, () => {},

View File

@ -9,6 +9,7 @@
scrollable, scrollable,
closed: !showBody, closed: !showBody,
}" }"
ref="el"
> >
<header v-if="showHeader" ref="header"> <header v-if="showHeader" ref="header">
<div class="title"><slot name="header"></slot></div> <div class="title"><slot name="header"></slot></div>
@ -59,123 +60,110 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from "vue"; import { onMounted, ref, watch } from "vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
export default defineComponent({ const props = withDefaults(
props: { defineProps<{
showHeader: { showHeader?: boolean;
type: Boolean, thin?: boolean;
required: false, naked?: boolean;
default: true, foldable?: boolean;
}, expanded?: boolean;
thin: { scrollable?: boolean;
type: Boolean, maxHeight?: number | null;
required: false, }>(),
default: false, {
}, showHeader: true,
naked: { thin: false,
type: Boolean, naked: false,
required: false, foldable: false,
default: false, expanded: true,
}, scrollable: false,
foldable: { maxHeight: null,
type: Boolean,
required: false,
default: false,
},
expanded: {
type: Boolean,
required: false,
default: true,
},
scrollable: {
type: Boolean,
required: false,
default: false,
},
maxHeight: {
type: Number,
required: false,
default: null,
},
}, },
data() { );
return {
showBody: this.expanded,
omitted: null,
ignoreOmit: false,
i18n,
icon,
defaultStore,
};
},
mounted() {
this.$watch(
"showBody",
(showBody) => {
const headerHeight = this.showHeader
? this.$refs.header.offsetHeight
: 0;
this.$el.style.minHeight = `${headerHeight}px`;
if (showBody) {
this.$el.style.flexBasis = "auto";
} else {
this.$el.style.flexBasis = `${headerHeight}px`;
}
},
{
immediate: true,
},
);
this.$el.style.setProperty("--maxHeight", this.maxHeight + "px"); const showBody = ref(props.expanded);
const omitted = ref<boolean | null>(null);
const ignoreOmit = ref(false);
const el = ref<HTMLElement | null>(null);
const header = ref<HTMLElement | null>(null);
const content = ref<HTMLElement | null>(null);
const calcOmit = () => { function toggleContent(show: boolean) {
if ( if (!props.foldable) return;
this.omitted || showBody.value = show;
this.ignoreOmit || }
this.maxHeight == null ||
this.$refs.content == null
)
return;
const height = this.$refs.content.offsetHeight;
this.omitted = height > this.maxHeight;
};
function enter(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0;
el.offsetHeight; // reflow
el.style.height = `${elementHeight}px`;
}
function afterEnter(el) {
el.style.height = null;
}
function leave(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow
el.style.height = 0;
}
function afterLeave(el) {
el.style.height = null;
}
onMounted(() => {
watch(
showBody,
(showBody) => {
const headerHeight = props.showHeader ? header.value!.offsetHeight : 0;
el.value!.style.minHeight = `${headerHeight}px`;
if (showBody) {
el.value!.style.flexBasis = "auto";
} else {
el.value!.style.flexBasis = `${headerHeight}px`;
}
},
{
immediate: true,
},
);
if (props.maxHeight != null) {
el.value!.style.setProperty("--maxHeight", `${props.maxHeight}px`);
}
const calcOmit = () => {
if (
omitted.value ||
ignoreOmit.value ||
props.maxHeight == null ||
content.value == null
)
return;
const height = content.value.offsetHeight;
omitted.value = height > props.maxHeight;
};
calcOmit();
new ResizeObserver((_entries, _observer) => {
calcOmit(); calcOmit();
new ResizeObserver((entries, observer) => { }).observe(content.value!);
calcOmit(); });
}).observe(this.$refs.content);
},
methods: {
toggleContent(show: boolean) {
if (!this.foldable) return;
this.showBody = show;
},
enter(el) { defineExpose({
const elementHeight = el.getBoundingClientRect().height; toggleContent,
el.style.height = 0; enter,
el.offsetHeight; // reflow afterEnter,
el.style.height = elementHeight + "px"; leave,
}, afterLeave,
afterEnter(el) {
el.style.height = null;
},
leave(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + "px";
el.offsetHeight; // reflow
el.style.height = 0;
},
afterLeave(el) {
el.style.height = null;
},
},
}); });
</script> </script>

View File

@ -28,7 +28,7 @@ const emit = defineEmits<{
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
const rootEl = ref<HTMLDivElement>(); const rootEl = ref<HTMLDivElement | null>(null);
const zIndex = ref<number>(os.claimZIndex("high")); const zIndex = ref<number>(os.claimZIndex("high"));
@ -36,8 +36,8 @@ onMounted(() => {
let left = props.ev.pageX + 1; // + 1 let left = props.ev.pageX + 1; // + 1
let top = props.ev.pageY + 1; // + 1 let top = props.ev.pageY + 1; // + 1
const width = rootEl.value.offsetWidth; const width = rootEl.value!.offsetWidth;
const height = rootEl.value.offsetHeight; const height = rootEl.value!.offsetHeight;
if (left + width - window.scrollX > window.innerWidth) { if (left + width - window.scrollX > window.innerWidth) {
left = window.innerWidth - width + window.scrollX; left = window.innerWidth - width + window.scrollX;
@ -55,8 +55,8 @@ onMounted(() => {
left = 0; left = 0;
} }
rootEl.value.style.top = `${top}px`; rootEl.value!.style.top = `${top}px`;
rootEl.value.style.left = `${left}px`; rootEl.value!.style.left = `${left}px`;
document.body.addEventListener("mousedown", onMousedown); document.body.addEventListener("mousedown", onMousedown);
}); });

View File

@ -68,40 +68,48 @@ let cropper: Cropper | null = null;
const loading = ref(true); const loading = ref(true);
const ok = async () => { const ok = async () => {
const promise = new Promise<entities.DriveFile>(async (res) => { async function UploadCroppedImg(): Promise<entities.DriveFile> {
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas(); const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
croppedCanvas.toBlob((blob) => {
const formData = new FormData();
formData.append("file", blob);
if (defaultStore.state.uploadFolder) {
formData.append("folderId", defaultStore.state.uploadFolder);
}
fetch(apiUrl + "/drive/files/create", { const blob = await new Promise<Blob | null>((resolve) =>
method: "POST", croppedCanvas!.toBlob((blob) => resolve(blob)),
body: formData, );
headers: {
authorization: `Bearer ${me.token}`, // MDN says `null` may be passed if the image cannot be created for any reason.
}, // But I don't think this is reachable for normal case.
}) if (blob == null) {
.then((response) => response.json()) throw "Cropping image failed.";
.then((f) => { }
res(f);
}); const formData = new FormData();
formData.append("file", blob);
if (defaultStore.state.uploadFolder) {
formData.append("folderId", defaultStore.state.uploadFolder);
}
const response = await fetch(`${apiUrl}/drive/files/create`, {
method: "POST",
body: formData,
headers: {
authorization: `Bearer ${me!.token}`,
},
}); });
}); return await response.json();
}
const promise = UploadCroppedImg();
os.promiseDialog(promise); os.promiseDialog(promise);
const f = await promise; const f = await promise;
emit("ok", f); emit("ok", f);
dialogEl.value.close(); dialogEl.value!.close();
}; };
const cancel = () => { const cancel = () => {
emit("cancel"); emit("cancel");
dialogEl.value.close(); dialogEl.value!.close();
}; };
const onImageLoad = () => { const onImageLoad = () => {
@ -114,7 +122,7 @@ const onImageLoad = () => {
}; };
onMounted(() => { onMounted(() => {
cropper = new Cropper(imgEl.value, {}); cropper = new Cropper(imgEl.value!, {});
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
@ -127,13 +135,13 @@ onMounted(() => {
selection.outlined = true; selection.outlined = true;
window.setTimeout(() => { window.setTimeout(() => {
cropper.getCropperImage()!.$center("contain"); cropper!.getCropperImage()!.$center("contain");
selection.$center(); selection.$center();
}, 100); }, 100);
// 調 // 調
window.setTimeout(() => { window.setTimeout(() => {
cropper.getCropperImage()!.$center("contain"); cropper!.getCropperImage()!.$center("contain");
selection.$center(); selection.$center();
}, 500); }, 500);
}); });

View File

@ -48,7 +48,7 @@ const toggle = () => {
}; };
function focus() { function focus() {
el.value.focus(); el.value?.focus();
} }
defineExpose({ defineExpose({

View File

@ -104,7 +104,7 @@
</MkInput> </MkInput>
<MkTextarea <MkTextarea
v-if="input && input.type === 'paragraph'" v-if="input && input.type === 'paragraph'"
v-model="inputValue" v-model="(inputValue as string)"
autofocus autofocus
type="paragraph" type="paragraph"
:placeholder="input.placeholder || undefined" :placeholder="input.placeholder || undefined"
@ -204,28 +204,44 @@ import { i18n } from "@/i18n";
import iconify from "@/scripts/icon"; import iconify from "@/scripts/icon";
interface Input { interface Input {
type: HTMLInputElement["type"]; type?:
| "text"
| "number"
| "password"
| "email"
| "url"
| "date"
| "time"
| "search"
| "paragraph";
placeholder?: string | null; placeholder?: string | null;
autocomplete?: string; autocomplete?: string;
default: string | number | null; default?: string | number | null;
minLength?: number; minLength?: number;
maxLength?: number; maxLength?: number;
} }
interface Select { type Select = {
items: { default?: string | null;
value: string; } & (
text: string; | {
}[]; items: {
groupedItems: { value: string;
label: string; text: string;
items: { }[];
value: string; groupedItems?: undefined;
text: string; }
}[]; | {
}[]; items?: undefined;
default: string | null; groupedItems: {
} label: string;
items: {
value: string;
text: string;
}[];
}[];
}
);
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -237,8 +253,8 @@ const props = withDefaults(
| "question" | "question"
| "waiting" | "waiting"
| "search"; | "search";
title: string; title?: string | null;
text?: string; text?: string | null;
isPlaintext?: boolean; isPlaintext?: boolean;
input?: Input; input?: Input;
select?: Select; select?: Select;
@ -246,7 +262,7 @@ const props = withDefaults(
actions?: { actions?: {
text: string; text: string;
primary?: boolean; primary?: boolean;
callback: (...args: any[]) => void; callback: () => void;
}[]; }[];
showOkButton?: boolean; showOkButton?: boolean;
showCancelButton?: boolean; showCancelButton?: boolean;
@ -268,7 +284,10 @@ const props = withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "done", v: { canceled: boolean; result: any }): void; (
ev: "done",
v: { canceled: boolean; result?: string | number | boolean | null },
): void;
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
@ -306,7 +325,7 @@ const okButtonDisabled = computed<boolean>(() => {
const inputEl = ref<typeof MkInput>(); const inputEl = ref<typeof MkInput>();
function done(canceled: boolean, result?) { function done(canceled: boolean, result?: string | number | boolean | null) {
emit("done", { canceled, result }); emit("done", { canceled, result });
modal.value?.close(null); modal.value?.close(null);
} }
@ -342,12 +361,12 @@ function onInputKeydown(evt: KeyboardEvent) {
} }
} }
function formatDateToYYYYMMDD(date) { // function formatDateToYYYYMMDD(date) {
const year = date.getFullYear(); // const year = date.getFullYear();
const month = ("0" + (date.getMonth() + 1)).slice(-2); // const month = ("0" + (date.getMonth() + 1)).slice(-2);
const day = ("0" + (date.getDate() + 1)).slice(-2); // const day = ("0" + (date.getDate() + 1)).slice(-2);
return `${year}-${month}-${day}`; // return `${year}-${month}-${day}`;
} // }
/** /**
* Appends a new search parameter to the value in the input field. * Appends a new search parameter to the value in the input field.
@ -355,18 +374,18 @@ function formatDateToYYYYMMDD(date) {
* begin typing a new criteria. * begin typing a new criteria.
* @param value The value to append. * @param value The value to append.
*/ */
function appendFilter(value: string) { // function appendFilter(value: string) {
return ( // return (
[ // [
typeof inputValue.value === "string" // typeof inputValue.value === "string"
? inputValue.value.trim() // ? inputValue.value.trim()
: inputValue.value, // : inputValue.value,
value, // value,
] // ]
.join(" ") // .join(" ")
.trim() + " " // .trim() + " "
); // );
} // }
onMounted(() => { onMounted(() => {
document.addEventListener("keydown", onKeydown); document.addEventListener("keydown", onKeydown);

View File

@ -26,7 +26,7 @@ const props = withDefaults(
}, },
); );
let intervalId; let intervalId: number;
const hh = ref(""); const hh = ref("");
const mm = ref(""); const mm = ref("");
const ss = ref(""); const ss = ref("");

View File

@ -29,7 +29,7 @@
<MkButton <MkButton
v-if="instance.donationLink" v-if="instance.donationLink"
gradate gradate
@click="openExternal(instance.donationLink)" @click="openExternal(instance.donationLink!)"
>{{ >{{
i18n.t("_aboutFirefish.donateHost", { i18n.t("_aboutFirefish.donateHost", {
host: hostname, host: hostname,
@ -73,7 +73,8 @@ const emit = defineEmits<{
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
const hostname = instance.name?.length < 38 ? instance.name : host; const hostname =
instance.name?.length && instance.name?.length < 38 ? instance.name : host;
const zIndex = os.claimZIndex("low"); const zIndex = os.claimZIndex("low");
@ -97,7 +98,7 @@ function neverShow() {
close(); close();
} }
function openExternal(link) { function openExternal(link: string) {
window.open(link, "_blank"); window.open(link, "_blank");
} }
</script> </script>

View File

@ -47,6 +47,7 @@ import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { me } from "@/me"; import { me } from "@/me";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { MenuItem } from "@/types/menu";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -72,7 +73,7 @@ const title = computed(
() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`, () => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`,
); );
function getMenu() { function getMenu(): MenuItem[] {
return [ return [
{ {
text: i18n.ts.rename, text: i18n.ts.rename,
@ -180,12 +181,15 @@ function describe() {
image: props.file, image: props.file,
}, },
{ {
done: (result) => { done: (result: {
canceled: boolean;
result?: string | null;
}) => {
if (!result || result.canceled) return; if (!result || result.canceled) return;
const comment = result.result; const comment = result.result;
os.api("drive/files/update", { os.api("drive/files/update", {
fileId: props.file.id, fileId: props.file.id,
comment: comment.length === 0 ? null : comment, comment: comment || null,
}); });
}, },
}, },

View File

@ -253,7 +253,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
removeFolder(folderId); removeFolder(folderId);
} }
function onDragover(ev: DragEvent): any { function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;
// //
@ -285,7 +285,7 @@ function onDragleave() {
draghover.value = false; draghover.value = false;
} }
function onDrop(ev: DragEvent): any { function onDrop(ev: DragEvent) {
draghover.value = false; draghover.value = false;
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;
@ -493,14 +493,12 @@ function move(target?: entities.DriveFolder) {
if (!target) { if (!target) {
goRoot(); goRoot();
return; return;
} else if (typeof target === "object") {
target = target.id;
} }
fetching.value = true; fetching.value = true;
os.api("drive/folders/show", { os.api("drive/folders/show", {
folderId: target, folderId: target.id,
}).then((folderToMove) => { }).then((folderToMove) => {
folder.value = folderToMove; folder.value = folderToMove;
hierarchyFolders.value = []; hierarchyFolders.value = [];

View File

@ -14,7 +14,7 @@
class="_button" class="_button"
@click.stop=" @click.stop="
applyUnicodeSkinTone( applyUnicodeSkinTone(
props.skinTones.indexOf(skinTone) + 1, props.skinTones!.indexOf(skinTone) + 1,
) )
" "
> >

View File

@ -180,6 +180,11 @@ import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
// FIXME: This variable doesn't seem to be used at all. I don't know why it was here.
const isActive = ref<boolean>();
type EmojiDef = string | entities.CustomEmoji | UnicodeEmojiDef;
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
showPinned?: boolean; showPinned?: boolean;
@ -193,7 +198,7 @@ const props = withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "chosen", v: string, ev: MouseEvent): void; chosen: [v: string, ev?: MouseEvent];
}>(); }>();
const search = ref<HTMLInputElement>(); const search = ref<HTMLInputElement>();
@ -410,13 +415,17 @@ function reset() {
q.value = ""; q.value = "";
} }
function getKey( function getKey(emoji: EmojiDef): string {
emoji: string | entities.CustomEmoji | UnicodeEmojiDef, if (typeof emoji === "string") {
): string { return emoji;
return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`; }
if ("emoji" in emoji) {
return emoji.emoji;
}
return `:${emoji.name}:`;
} }
function chosen(emoji: any, ev?: MouseEvent) { function chosen(emoji: EmojiDef, ev?: MouseEvent) {
const el = const el =
ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined); ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
if (el) { if (el) {
@ -432,22 +441,33 @@ function chosen(emoji: any, ev?: MouseEvent) {
// 使 // 使
if (!pinned.value.includes(key)) { if (!pinned.value.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis; let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== key); recents = recents.filter((emoji) => emoji !== key);
recents.unshift(key); recents.unshift(key);
defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32)); defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32));
} }
} }
function paste(event: ClipboardEvent) { async function paste(event: ClipboardEvent) {
const paste = (event.clipboardData || window.clipboardData).getData("text"); let pasteStr: string | null = null;
if (done(paste)) { if (event.clipboardData) {
pasteStr = event.clipboardData.getData("text");
} else {
// Use native api
try {
pasteStr = await window.navigator.clipboard.readText();
} catch (_err) {
// Reading the clipboard requires permission, and the user did not give it
}
}
if (done(pasteStr)) {
event.preventDefault(); event.preventDefault();
} }
} }
function done(query?: any): boolean | void { function done(query?: string | null): boolean {
// biome-ignore lint/style/noParameterAssign: assign it intentially
if (query == null) query = q.value; if (query == null) query = q.value;
if (query == null || typeof query !== "string") return; if (query == null || typeof query !== "string") return false;
const q2 = query.replaceAll(":", ""); const q2 = query.replaceAll(":", "");
const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2); const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2);
@ -470,6 +490,7 @@ function done(query?: any): boolean | void {
chosen(searchResultUnicode.value[0]); chosen(searchResultUnicode.value[0]);
return true; return true;
} }
return false;
} }
onMounted(() => { onMounted(() => {

View File

@ -51,7 +51,7 @@ withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "done", v: any): void; (ev: "done", v: string): void;
(ev: "close"): void; (ev: "close"): void;
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
@ -64,7 +64,7 @@ function checkForShift(ev?: MouseEvent) {
modal.value?.close(ev); modal.value?.close(ev);
} }
function chosen(emoji: any, ev: MouseEvent) { function chosen(emoji: string, ev?: MouseEvent) {
emit("done", emoji); emit("done", emoji);
checkForShift(ev); checkForShift(ev);
} }

View File

@ -31,72 +31,76 @@
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from "vue"; import { ref, watch } from "vue";
import { getUniqueId } from "@/os"; import { getUniqueId } from "@/os";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
// import icon from "@/scripts/icon"; // import icon from "@/scripts/icon";
const localStoragePrefix = "ui:folder:"; const localStoragePrefix = "ui:folder:";
export default defineComponent({ const props = withDefaults(
props: { defineProps<{
expanded: { expanded?: boolean;
type: Boolean, persistKey?: string | null;
required: false, }>(),
default: true, {
}, expanded: true,
persistKey: { persistKey: null,
type: String,
required: false,
default: null,
},
}, },
data() { );
return {
bodyId: getUniqueId(),
showBody:
this.persistKey &&
localStorage.getItem(localStoragePrefix + this.persistKey)
? localStorage.getItem(localStoragePrefix + this.persistKey) === "t"
: this.expanded,
animation: defaultStore.state.animation,
};
},
watch: {
showBody() {
if (this.persistKey) {
localStorage.setItem(
localStoragePrefix + this.persistKey,
this.showBody ? "t" : "f",
);
}
},
},
methods: {
toggleContent(show: boolean) {
this.showBody = show;
},
enter(el) { const bodyId = ref(getUniqueId());
const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0; const showBody = ref(
el.offsetHeight; // reflow props.persistKey &&
el.style.height = elementHeight + "px"; localStorage.getItem(localStoragePrefix + props.persistKey)
}, ? localStorage.getItem(localStoragePrefix + props.persistKey) === "t"
afterEnter(el) { : props.expanded,
el.style.height = null; );
},
leave(el) { const animation = defaultStore.state.animation;
const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + "px"; watch(showBody, () => {
el.offsetHeight; // reflow if (props.persistKey) {
el.style.height = 0; localStorage.setItem(
}, localStoragePrefix + props.persistKey,
afterLeave(el) { showBody.value ? "t" : "f",
el.style.height = null; );
}, }
}, });
function toggleContent(show: boolean) {
showBody.value = show;
}
function enter(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0;
el.offsetHeight; // reflow
// biome-ignore lint/style/useTemplate: <explanation>
el.style.height = elementHeight + "px";
}
function afterEnter(el) {
el.style.height = null;
}
function leave(el) {
const elementHeight = el.getBoundingClientRect().height;
// biome-ignore lint/style/useTemplate: <explanation>
el.style.height = elementHeight + "px";
el.offsetHeight; // reflow
el.style.height = 0;
}
function afterLeave(el) {
el.style.height = null;
}
defineExpose({
toggleContent,
enter,
afterEnter,
leave,
afterLeave,
}); });
</script> </script>

View File

@ -8,7 +8,7 @@
<i :class="icon('ph-dots-three-outline')"></i> <i :class="icon('ph-dots-three-outline')"></i>
</button> </button>
<button <button
v-if="!hideFollowButton && isSignedIn && me.id != user.id" v-if="!hideFollowButton && isSignedIn && me!.id != user.id"
v-tooltip="full ? null : `${state} ${user.name || user.username}`" v-tooltip="full ? null : `${state} ${user.name || user.username}`"
class="kpoogebi _button follow-button" class="kpoogebi _button follow-button"
:class="{ :class="{

View File

@ -3,7 +3,7 @@
ref="dialog" ref="dialog"
:width="370" :width="370"
:height="400" :height="400"
@close="dialog.close()" @close="dialog!.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.forgotPassword }}</template> <template #header>{{ i18n.ts.forgotPassword }}</template>
@ -76,7 +76,7 @@ const emit = defineEmits<{
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
const dialog: InstanceType<typeof XModalWindow> = ref(); const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
const username = ref(""); const username = ref("");
const email = ref(""); const email = ref("");
@ -89,7 +89,7 @@ async function onSubmit() {
email: email.value, email: email.value,
}); });
emit("done"); emit("done");
dialog.value.close(); dialog.value!.close();
} }
</script> </script>

View File

@ -8,7 +8,7 @@
@click="cancel()" @click="cancel()"
@ok="ok()" @ok="ok()"
@close="cancel()" @close="cancel()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header> <template #header>
{{ title }} {{ title }}
@ -17,86 +17,107 @@
<MkSpacer :margin-min="20" :margin-max="32"> <MkSpacer :margin-min="20" :margin-max="32">
<div class="_formRoot"> <div class="_formRoot">
<template <template
v-for="item in Object.keys(form).filter( v-for="[formItem, formItemName] in unHiddenForms()"
(item) => !form[item].hidden,
)"
> >
<FormInput <FormInput
v-if="form[item].type === 'number'" v-if="formItem.type === 'number'"
v-model="values[item]" v-model="values[formItemName]"
type="number" type="number"
:step="form[item].step || 1" :step="formItem.step || 1"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormInput> </FormInput>
<FormInput <FormInput
v-else-if=" v-else-if="
form[item].type === 'string' && formItem.type === 'string' &&
!form[item].multiline !formItem.multiline
" "
v-model="values[item]" v-model="values[formItemName]"
type="text" type="text"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template>
</FormInput>
<FormInput
v-else-if="
formItem.type === 'email' ||
formItem.type === 'password' ||
formItem.type === 'url' ||
formItem.type === 'date' ||
formItem.type === 'time' ||
formItem.type === 'search'
"
v-model="values[formItemName]"
:type="formItem.type"
class="_formBlock"
>
<template #label
><span v-text="formItem.label || formItemName"></span
><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span
></template
>
<template v-if="formItem.description" #caption>{{
formItem.description
}}</template> }}</template>
</FormInput> </FormInput>
<FormTextarea <FormTextarea
v-else-if=" v-else-if="
form[item].type === 'string' && form[item].multiline formItem.type === 'string' && formItem.multiline
" "
v-model="values[item]" v-model="values[formItemName]"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormTextarea> </FormTextarea>
<FormSwitch <FormSwitch
v-else-if="form[item].type === 'boolean'" v-else-if="formItem.type === 'boolean'"
v-model="values[item]" v-model="values[formItemName]"
class="_formBlock" class="_formBlock"
> >
<span v-text="form[item].label || item"></span> <span v-text="formItem.label || formItemName"></span>
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormSwitch> </FormSwitch>
<FormSelect <FormSelect
v-else-if="form[item].type === 'enum'" v-else-if="formItem.type === 'enum'"
v-model="values[item]" v-model="values[formItemName]"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label>
><span v-text="form[item].label || item"></span <span v-text="formItem.label || formItemName"></span>
><span v-if="form[item].required === false"> <span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template
> >
</template>
<option <option
v-for="item in form[item].enum" v-for="item in formItem.enum"
:key="item.value" :key="item.value"
:value="item.value" :value="item.value"
> >
@ -104,18 +125,18 @@
</option> </option>
</FormSelect> </FormSelect>
<FormRadios <FormRadios
v-else-if="form[item].type === 'radio'" v-else-if="formItem.type === 'radio'"
v-model="values[item]" v-model="values[formItemName]"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<option <option
v-for="item in form[item].options" v-for="item in formItem.options"
:key="item.value" :key="item.value"
:value="item.value" :value="item.value"
> >
@ -123,30 +144,30 @@
</option> </option>
</FormRadios> </FormRadios>
<FormRange <FormRange
v-else-if="form[item].type === 'range'" v-else-if="formItem.type === 'range'"
v-model="values[item]" v-model="values[formItemName]"
:min="form[item].min" :min="formItem.min"
:max="form[item].max" :max="formItem.max"
:step="form[item].step" :step="formItem.step"
:text-converter="form[item].textConverter" :text-converter="formItem.textConverter"
class="_formBlock" class="_formBlock"
> >
<template #label <template #label
><span v-text="form[item].label || item"></span ><span v-text="formItem.label || formItemName"></span
><span v-if="form[item].required === false"> ><span v-if="formItem.required === false">
({{ i18n.ts.optional }})</span ({{ i18n.ts.optional }})</span
></template ></template
> >
<template v-if="form[item].description" #caption>{{ <template v-if="formItem.description" #caption>{{
form[item].description formItem.description
}}</template> }}</template>
</FormRange> </FormRange>
<MkButton <MkButton
v-else-if="form[item].type === 'button'" v-else-if="formItem.type === 'button'"
class="_formBlock" class="_formBlock"
@click="form[item].action($event, values)" @click="formItem.action($event, values)"
> >
<span v-text="form[item].content || item"></span> <span v-text="formItem.content || formItemName"></span>
</MkButton> </MkButton>
</template> </template>
</div> </div>
@ -154,8 +175,8 @@
</XModalWindow> </XModalWindow>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from "vue"; import { ref } from "vue";
import FormInput from "./form/input.vue"; import FormInput from "./form/input.vue";
import FormTextarea from "./form/textarea.vue"; import FormTextarea from "./form/textarea.vue";
import FormSwitch from "./form/switch.vue"; import FormSwitch from "./form/switch.vue";
@ -165,59 +186,50 @@ import MkButton from "./MkButton.vue";
import FormRadios from "./form/radios.vue"; import FormRadios from "./form/radios.vue";
import XModalWindow from "@/components/MkModalWindow.vue"; import XModalWindow from "@/components/MkModalWindow.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import type { FormItemType } from "@/types/form";
export default defineComponent({ const props = defineProps<{
components: { title: string;
XModalWindow, form: Record<string, FormItemType>;
FormInput, }>();
FormTextarea,
FormSwitch,
FormSelect,
FormRange,
MkButton,
FormRadios,
},
props: { // biome-ignore lint/suspicious/noExplicitAny: To prevent overly complex types we have to use any here
title: { type ValueType = Record<string, any>;
type: String,
required: true, const emit = defineEmits<{
done: [
status: {
result?: Record<string, FormItemType["default"]>;
canceled?: true;
}, },
form: { ];
type: Object, closed: [];
required: true, }>();
},
},
emits: ["done"], const values = ref<ValueType>({});
const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
data() { for (const item in props.form) {
return { values.value[item] = props.form[item].default ?? null;
values: {}, }
i18n,
};
},
created() { function unHiddenForms(): [FormItemType, string][] {
for (const item in this.form) { return Object.keys(props.form)
this.values[item] = this.form[item].default ?? null; .filter((itemName) => !props.form[itemName].hidden)
} .map((itemName) => [props.form[itemName], itemName]);
}, }
methods: { function ok() {
ok() { emit("done", {
this.$emit("done", { result: values.value,
result: this.values, });
}); dialog.value!.close();
this.$refs.dialog.close(); }
},
cancel() { function cancel() {
this.$emit("done", { emit("done", {
canceled: true, canceled: true,
}); });
this.$refs.dialog.close(); dialog.value!.close();
}, }
},
});
</script> </script>

View File

@ -19,11 +19,11 @@ export default defineComponent({
}, },
}, },
computed: { computed: {
compiledFormula(): any { compiledFormula() {
return katex.renderToString(this.formula, { return katex.renderToString(this.formula, {
throwOnError: false, throwOnError: false,
displayMode: this.block, displayMode: this.block,
} as any); });
}, },
}, },
}); });

View File

@ -2,10 +2,24 @@
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel"> <MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel">
<div class="thumbnail"> <div class="thumbnail">
<ImgWithBlurhash <ImgWithBlurhash
v-if="post.files && post.files.length > 0"
class="img" class="img"
:src="post.files[0].thumbnailUrl" :src="post.files[0].thumbnailUrl"
:hash="post.files[0].blurhash" :hash="post.files[0].blurhash"
/> />
<div
v-else
class="_fullinfo"
>
<!-- If there is no picture
This can happen if the user deletes the image in the drive
-->
<img
src="/static-assets/badges/not-found.webp"
class="img"
:alt="i18n.ts.notFound"
/>
</div>
</div> </div>
<article> <article>
<header> <header>
@ -20,9 +34,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue"; import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
import { i18n } from "@/i18n";
import type { entities } from "firefish-js";
const props = defineProps<{ defineProps<{
post: any; post: entities.GalleryPost;
}>(); }>();
</script> </script>

View File

@ -2,16 +2,16 @@
<MkModal <MkModal
ref="modal" ref="modal"
:z-priority="'middle'" :z-priority="'middle'"
@click="modal.close()" @click="modal!.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<div class="xubzgfga"> <div class="xubzgfga">
<header>{{ image.name }}</header> <header>{{ image.name }}</header>
<img <img
:src="image.url" :src="image.url"
:alt="image.comment" :alt="image.comment || undefined"
:title="image.comment" :title="image.comment || undefined"
@click="modal.close()" @click="modal!.close()"
/> />
<footer> <footer>
<span>{{ image.type }}</span> <span>{{ image.type }}</span>
@ -33,7 +33,7 @@ import bytes from "@/filters/bytes";
import number from "@/filters/number"; import number from "@/filters/number";
import MkModal from "@/components/MkModal.vue"; import MkModal from "@/components/MkModal.vue";
const props = withDefaults( withDefaults(
defineProps<{ defineProps<{
image: entities.DriveFile; image: entities.DriveFile;
}>(), }>(),
@ -41,10 +41,10 @@ const props = withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "closed"): void; closed: [];
}>(); }>();
const modal = ref<InstanceType<typeof MkModal>>(); const modal = ref<InstanceType<typeof MkModal> | null>(null);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,20 +4,20 @@
ref="canvas" ref="canvas"
:width="size" :width="size"
:height="size" :height="size"
:title="title" :title="title || undefined"
/> />
<img <img
v-if="src" v-if="src"
:src="src" :src="src"
:title="title" :title="title || undefined"
:type="type" :type="type"
:alt="alt" :alt="alt || undefined"
:class="{ :class="{
cover, cover,
wide: largestDimension === 'width', wide: largestDimension === 'width',
tall: largestDimension === 'height', tall: largestDimension === 'height',
}" }"
:style="{ 'object-fit': cover ? 'cover' : null }" :style="{ 'object-fit': cover ? 'cover' : undefined }"
loading="lazy" loading="lazy"
@load="onLoad" @load="onLoad"
/> />

View File

@ -23,17 +23,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue";
import type { entities } from "firefish-js"; import type { entities } from "firefish-js";
import * as os from "@/os";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
const props = defineProps<{ defineProps<{
instance: entities.Instance; instance: entities.Instance;
}>(); }>();
function getInstanceIcon(instance): string { function getInstanceIcon(instance: entities.Instance): string {
return ( return (
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ?? getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
getProxiedImageUrlNullable(instance.iconUrl, "preview") ?? getProxiedImageUrlNullable(instance.iconUrl, "preview") ??

View File

@ -65,14 +65,14 @@ import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "ok", selected: entities.Instance): void; ok: [selected: entities.Instance];
(ev: "cancel"): void; cancel: [];
(ev: "closed"): void; closed: [];
}>(); }>();
const hostname = ref(""); const hostname = ref("");
const instances = ref<entities.Instance[]>([]); const instances = ref<entities.Instance[]>([]);
const selected = ref<entities.Instance | null>(); const selected = ref<entities.Instance | null>(null);
const dialogEl = ref<InstanceType<typeof XModalWindow>>(); const dialogEl = ref<InstanceType<typeof XModalWindow>>();
let searchOrderLatch = 0; let searchOrderLatch = 0;

View File

@ -52,6 +52,7 @@ import { i18n } from "@/i18n";
import MkActiveUsersHeatmap from "@/components/MkActiveUsersHeatmap.vue"; import MkActiveUsersHeatmap from "@/components/MkActiveUsersHeatmap.vue";
import MkFolder from "@/components/MkFolder.vue"; import MkFolder from "@/components/MkFolder.vue";
import { initChart } from "@/scripts/init-chart"; import { initChart } from "@/scripts/init-chart";
import type { entities } from "firefish-js";
initChart(); initChart();
@ -67,7 +68,18 @@ const { handler: externalTooltipHandler2 } = useChartTooltip({
position: "middle", position: "middle",
}); });
function createDoughnut(chartEl, tooltip, data) { interface ColorData {
name: string;
color: string | undefined;
value: number;
onClick?: () => void;
}
function createDoughnut(
chartEl: HTMLCanvasElement,
tooltip: typeof externalTooltipHandler1,
data: ColorData[],
) {
const chartInstance = new Chart(chartEl, { const chartInstance = new Chart(chartEl, {
type: "doughnut", type: "doughnut",
data: { data: {
@ -96,13 +108,13 @@ function createDoughnut(chartEl, tooltip, data) {
}, },
onClick: (ev) => { onClick: (ev) => {
const hit = chartInstance.getElementsAtEventForMode( const hit = chartInstance.getElementsAtEventForMode(
ev, ev as unknown as Event,
"nearest", "nearest",
{ intersect: true }, { intersect: true },
false, false,
)[0]; )[0];
if (hit && data[hit.index].onClick) { if (hit) {
data[hit.index].onClick(); data[hit.index].onClick?.();
} }
}, },
plugins: { plugins: {
@ -124,48 +136,41 @@ function createDoughnut(chartEl, tooltip, data) {
return chartInstance; return chartInstance;
} }
function instance2ColorData(x: entities.Instance): ColorData {
return {
name: x.host,
color: x.themeColor || undefined,
value: x.followersCount,
onClick: () => {
os.pageWindow(`/instance-info/${x.host}`);
},
};
}
onMounted(() => { onMounted(() => {
os.apiGet("federation/stats", { limit: 30 }).then((fedStats) => { os.apiGet("federation/stats", { limit: 30 }).then((fedStats) => {
createDoughnut( createDoughnut(
subDoughnutEl.value, subDoughnutEl.value!,
externalTooltipHandler1, externalTooltipHandler1,
fedStats.topSubInstances fedStats.topSubInstances.map(instance2ColorData).concat([
.map((x) => ({ {
name: x.host, name: "(other)",
color: x.themeColor, color: "#80808080",
value: x.followersCount, value: fedStats.otherFollowersCount,
onClick: () => { },
os.pageWindow(`/instance-info/${x.host}`); ]),
},
}))
.concat([
{
name: "(other)",
color: "#80808080",
value: fedStats.otherFollowersCount,
},
]),
); );
createDoughnut( createDoughnut(
pubDoughnutEl.value, pubDoughnutEl.value!,
externalTooltipHandler2, externalTooltipHandler2,
fedStats.topPubInstances fedStats.topPubInstances.map(instance2ColorData).concat([
.map((x) => ({ {
name: x.host, name: "(other)",
color: x.themeColor, color: "#80808080",
value: x.followingCount, value: fedStats.otherFollowingCount,
onClick: () => { },
os.pageWindow(`/instance-info/${x.host}`); ]),
},
}))
.concat([
{
name: "(other)",
color: "#80808080",
value: fedStats.otherFollowingCount,
},
]),
); );
}); });
}); });

View File

@ -20,27 +20,22 @@ import { ref } from "vue";
import { instanceName, version } from "@/config"; import { instanceName, version } from "@/config";
import { instance as Instance } from "@/instance"; import { instance as Instance } from "@/instance";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
import type { entities } from "firefish-js";
const props = defineProps<{ const props = defineProps<{
instance?: { instance?: entities.InstanceLite;
faviconUrl?: string;
name: string;
themeColor?: string;
softwareName?: string;
softwareVersion?: string;
};
}>(); }>();
const ticker = ref<HTMLElement | null>(null); const ticker = ref<HTMLElement | null>(null);
// if no instance data is given, this is for the local instance // if no instance data is given, this is for the local instance
const instance = props.instance ?? { const instance = props.instance ?? {
faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico", faviconUrl: Instance.iconUrl || "/favicon.ico",
name: instanceName, name: instanceName,
themeColor: ( themeColor: (
document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement
)?.content, )?.content,
softwareName: Instance.softwareName ?? "Firefish", softwareName: "Firefish",
softwareVersion: version, softwareVersion: version,
}; };
@ -67,7 +62,7 @@ const commonNames = new Map<string, string>([
["wxwclub", "wxwClub"], ["wxwclub", "wxwClub"],
]); ]);
const capitalize = (s: string) => { const capitalize = (s?: string | null) => {
if (s == null) return "Unknown"; if (s == null) return "Unknown";
if (commonNames.has(s)) return commonNames.get(s); if (commonNames.has(s)) return commonNames.get(s);
return s[0].toUpperCase() + s.slice(1); return s[0].toUpperCase() + s.slice(1);

View File

@ -6,7 +6,7 @@
:anchor="anchor" :anchor="anchor"
:transparent-bg="true" :transparent-bg="true"
:src="src" :src="src"
@click="modal.close()" @click="modal!.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<div <div
@ -73,7 +73,10 @@ import { deviceKind } from "@/scripts/device-kind";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
src?: HTMLElement; src?: HTMLElement;
anchor?: { x: string; y: string }; anchor?: {
x: "left" | "center" | "right";
y: "top" | "center" | "bottom";
};
}>(), }>(),
{ {
anchor: () => ({ x: "right", y: "center" }), anchor: () => ({ x: "right", y: "center" }),
@ -109,7 +112,7 @@ const items = Object.keys(navbarItemDef)
})); }));
function close() { function close() {
modal.value.close(); modal.value!.close();
} }
</script> </script>

View File

@ -42,7 +42,7 @@ useTooltip(el, (showing) => {
os.popup( os.popup(
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")), defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
{ {
showing, showing: showing.value,
url: props.url, url: props.url,
source: el.value, source: el.value,
}, },

View File

@ -23,7 +23,7 @@ import { i18n } from "@/i18n";
const modal = shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const checkAnnouncements = () => { const checkAnnouncements = () => {
modal.value.close(); modal.value!.close();
location.href = "/announcements"; location.href = "/announcements";
}; };
</script> </script>

View File

@ -50,7 +50,7 @@
> >
<video <video
:poster="media.thumbnailUrl" :poster="media.thumbnailUrl"
:aria-label="media.comment" :aria-label="media.comment || undefined"
preload="none" preload="none"
controls controls
playsinline playsinline

View File

@ -64,7 +64,7 @@ import "vue-plyr/dist/vue-plyr.css";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
const props = withDefaults( withDefaults(
defineProps<{ defineProps<{
media: entities.DriveFile; media: entities.DriveFile;
}>(), }>(),

View File

@ -1,5 +1,5 @@
<template> <template>
<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')"> <MkModal ref="modal" @click="done(true)" @closed="emit('closed')">
<div class="container"> <div class="container">
<div class="fullwidth top-caption"> <div class="fullwidth top-caption">
<div class="mk-dialog"> <div class="mk-dialog">
@ -48,9 +48,9 @@
<img <img
id="imgtocaption" id="imgtocaption"
:src="image.url" :src="image.url"
:alt="image.comment" :alt="image.comment || undefined"
:title="image.comment" :title="image.comment || undefined"
@click="$refs.modal.close()" @click="modal!.close()"
/> />
<footer> <footer>
<span>{{ image.type }}</span> <span>{{ image.type }}</span>
@ -65,8 +65,8 @@
</MkModal> </MkModal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from "vue"; import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import insertTextAtCursor from "insert-text-at-cursor"; import insertTextAtCursor from "insert-text-at-cursor";
import { length } from "stringz"; import { length } from "stringz";
import * as os from "@/os"; import * as os from "@/os";
@ -76,122 +76,100 @@ import bytes from "@/filters/bytes";
import number from "@/filters/number"; import number from "@/filters/number";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { instance } from "@/instance"; import { instance } from "@/instance";
import type { entities } from "firefish-js";
export default defineComponent({ const props = withDefaults(
components: { defineProps<{
MkModal, image: entities.DriveFile;
MkButton,
},
props: {
image: {
type: Object,
required: true,
},
title: {
type: String,
required: false,
},
input: { input: {
required: true, placeholder: string;
}, default: string;
showOkButton: {
type: Boolean,
default: true,
},
showCaptionButton: {
type: Boolean,
default: true,
},
showCancelButton: {
type: Boolean,
default: true,
},
cancelableByBgClick: {
type: Boolean,
default: true,
},
},
emits: ["done", "closed"],
data() {
return {
inputValue: this.input.default ? this.input.default : null,
i18n,
}; };
title?: string;
showOkButton?: boolean;
showCaptionButton?: boolean;
showCancelButton?: boolean;
cancelableByBgClick?: boolean;
}>(),
{
showOkButton: true,
showCaptionButton: true,
showCancelButton: true,
cancelableByBgClick: true,
}, },
);
computed: { const emit = defineEmits<{
remainingLength(): number { done: [result: { canceled: boolean; result?: string | null }];
const maxCaptionLength = instance.maxCaptionTextLength ?? 512; closed: [];
if (typeof this.inputValue !== "string") return maxCaptionLength; }>();
return maxCaptionLength - length(this.inputValue);
},
},
mounted() { const modal = ref<InstanceType<typeof MkModal> | null>(null);
document.addEventListener("keydown", this.onKeydown);
},
beforeUnmount() { const inputValue = ref(props.input.default ? props.input.default : null);
document.removeEventListener("keydown", this.onKeydown);
},
methods: { const remainingLength = computed(() => {
bytes, const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
number, if (typeof inputValue.value !== "string") return maxCaptionLength;
return maxCaptionLength - length(inputValue.value);
});
done(canceled, result?) { function done(canceled: boolean, result?: string | null) {
this.$emit("done", { canceled, result }); emit("done", { canceled, result });
this.$refs.modal.close(); modal.value!.close();
}, }
async ok() { async function ok() {
if (!this.showOkButton) return; if (!props.showOkButton) return;
const result = this.inputValue; const result = inputValue.value;
this.done(false, result); done(false, result);
}, }
cancel() { function cancel() {
this.done(true); done(true);
}, }
onBgClick() { // function onBgClick() {
if (this.cancelableByBgClick) { // if (props.cancelableByBgClick) {
this.cancel(); // cancel();
} // }
}, // }
onKeydown(evt) { function onKeydown(evt) {
if (evt.which === 27) { if (evt.which === 27) {
// ESC // ESC
this.cancel(); cancel();
} }
}, }
onInputKeydown(evt) { function onInputKeydown(evt) {
if (evt.which === 13) { if (evt.which === 13) {
// Enter // Enter
if (evt.ctrlKey) { if (evt.ctrlKey) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
this.ok(); ok();
} }
} }
}, }
caption() { function caption() {
const img = document.getElementById("imgtocaption") as HTMLImageElement; const img = document.getElementById("imgtocaption") as HTMLImageElement;
const ta = document.getElementById("captioninput") as HTMLTextAreaElement; const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
os.api("drive/files/caption-image", { os.api("drive/files/caption-image", {
url: img.src, url: img.src,
}).then((text) => { }).then((text) => {
insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length)); insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length));
}); });
}, }
},
onMounted(() => {
document.addEventListener("keydown", onKeydown);
});
onBeforeUnmount(() => {
document.removeEventListener("keydown", onKeydown);
}); });
</script> </script>

View File

@ -22,7 +22,7 @@
media.type.startsWith('video') || media.type.startsWith('video') ||
media.type.startsWith('image') media.type.startsWith('image')
" "
:key="media.id" :key="`m-${media.id}`"
:class="{ image: media.type.startsWith('image') }" :class="{ image: media.type.startsWith('image') }"
:data-id="media.id" :data-id="media.id"
:media="media" :media="media"
@ -30,7 +30,7 @@
/> />
<XModPlayer <XModPlayer
v-else-if="isModule(media)" v-else-if="isModule(media)"
:key="media.id" :key="`p-${media.id}`"
:module="media" :module="media"
/> />
</template> </template>
@ -48,7 +48,7 @@ import "photoswipe/style.css";
import XBanner from "@/components/MkMediaBanner.vue"; import XBanner from "@/components/MkMediaBanner.vue";
import XMedia from "@/components/MkMedia.vue"; import XMedia from "@/components/MkMedia.vue";
import XModPlayer from "@/components/MkModPlayer.vue"; import XModPlayer from "@/components/MkModPlayer.vue";
import * as os from "@/os"; // import * as os from "@/os";
import { import {
FILE_EXT_TRACKER_MODULES, FILE_EXT_TRACKER_MODULES,
FILE_TYPE_BROWSERSAFE, FILE_TYPE_BROWSERSAFE,
@ -61,8 +61,8 @@ const props = defineProps<{
inDm?: boolean; inDm?: boolean;
}>(); }>();
const gallery = ref(null); const gallery = ref<HTMLElement | null>(null);
const pswpZIndex = os.claimZIndex("middle"); // const pswpZIndex = os.claimZIndex("middle");
onMounted(() => { onMounted(() => {
const lightbox = new PhotoSwipeLightbox({ const lightbox = new PhotoSwipeLightbox({
@ -79,7 +79,7 @@ onMounted(() => {
src: media.url, src: media.url,
w: media.properties.width, w: media.properties.width,
h: media.properties.height, h: media.properties.height,
alt: media.comment, alt: media.comment || undefined,
}; };
if ( if (
media.properties.orientation != null && media.properties.orientation != null &&
@ -89,7 +89,7 @@ onMounted(() => {
} }
return item; return item;
}), }),
gallery: gallery.value, gallery: gallery.value || undefined,
children: ".image", children: ".image",
thumbSelector: ".image img", thumbSelector: ".image img",
loop: false, loop: false,
@ -119,9 +119,13 @@ onMounted(() => {
// element is children // element is children
const { element } = itemData; const { element } = itemData;
if (element == null) return;
const id = element.dataset.id; const id = element.dataset.id;
const file = props.mediaList.find((media) => media.id === id); const file = props.mediaList.find((media) => media.id === id);
if (file == null) return;
itemData.src = file.url; itemData.src = file.url;
itemData.w = Number(file.properties.width); itemData.w = Number(file.properties.width);
itemData.h = Number(file.properties.height); itemData.h = Number(file.properties.height);
@ -132,12 +136,12 @@ onMounted(() => {
[itemData.w, itemData.h] = [itemData.h, itemData.w]; [itemData.w, itemData.h] = [itemData.h, itemData.w];
} }
itemData.msrc = file.thumbnailUrl; itemData.msrc = file.thumbnailUrl;
itemData.alt = file.comment; itemData.alt = file.comment || undefined;
itemData.thumbCropped = true; itemData.thumbCropped = true;
}); });
lightbox.on("uiRegister", () => { lightbox.on("uiRegister", () => {
lightbox.pswp.ui.registerElement({ lightbox.pswp?.ui?.registerElement({
name: "altText", name: "altText",
className: "pwsp__alt-text-container", className: "pwsp__alt-text-container",
appendTo: "wrapper", appendTo: "wrapper",
@ -146,7 +150,7 @@ onMounted(() => {
textBox.className = "pwsp__alt-text"; textBox.className = "pwsp__alt-text";
el.appendChild(textBox); el.appendChild(textBox);
const preventProp = function (ev: Event): void { const preventProp = (ev: Event): void => {
ev.stopPropagation(); ev.stopPropagation();
}; };
@ -158,7 +162,7 @@ onMounted(() => {
el.onpointermove = preventProp; el.onpointermove = preventProp;
pwsp.on("change", () => { pwsp.on("change", () => {
textBox.textContent = pwsp.currSlide.data.alt?.trim(); textBox.textContent = pwsp.currSlide?.data.alt?.trim() ?? null;
}); });
}, },
}); });
@ -168,7 +172,7 @@ onMounted(() => {
history.pushState(null, "", location.href); history.pushState(null, "", location.href);
addEventListener("popstate", close); addEventListener("popstate", close);
// This is a workaround. Not sure why, but when clicking to open, it doesn't move focus to the photoswipe. Preventing using esc to close. However when using keyboard to open it already focuses the lightbox fine. // This is a workaround. Not sure why, but when clicking to open, it doesn't move focus to the photoswipe. Preventing using esc to close. However when using keyboard to open it already focuses the lightbox fine.
lightbox.pswp.element.focus(); lightbox.pswp?.element?.focus();
}); });
lightbox.on("close", () => { lightbox.on("close", () => {
removeEventListener("popstate", close); removeEventListener("popstate", close);
@ -180,7 +184,7 @@ onMounted(() => {
function close() { function close() {
removeEventListener("popstate", close); removeEventListener("popstate", close);
history.forward(); history.forward();
lightbox.pswp.close(); lightbox.pswp?.close();
} }
}); });
@ -198,7 +202,7 @@ const isModule = (file: entities.DriveFile): boolean => {
return ( return (
FILE_TYPE_TRACKER_MODULES.includes(file.type) || FILE_TYPE_TRACKER_MODULES.includes(file.type) ||
FILE_EXT_TRACKER_MODULES.some((ext) => { FILE_EXT_TRACKER_MODULES.some((ext) => {
return file.name.toLowerCase().endsWith("." + ext); return file.name.toLowerCase().endsWith(`.${ext}`);
}) })
); );
}; };

View File

@ -23,7 +23,6 @@
:href="url" :href="url"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
:style="{ background: bgCss }"
@click.stop @click.stop
> >
<span class="main"> <span class="main">
@ -54,7 +53,7 @@ const url = `/${canonical}`;
const isMe = const isMe =
isSignedIn && isSignedIn &&
`@${props.username}@${toUnicode(props.host)}`.toLowerCase() === `@${props.username}@${toUnicode(props.host)}`.toLowerCase() ===
`@${me.username}@${toUnicode(localHost)}`.toLowerCase(); `@${me!.username}@${toUnicode(localHost)}`.toLowerCase();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -37,8 +37,8 @@ function setPosition() {
const rect = props.targetElement.getBoundingClientRect(); const rect = props.targetElement.getBoundingClientRect();
const left = props.targetElement.offsetWidth; const left = props.targetElement.offsetWidth;
const top = rect.top - rootRect.top - 8; const top = rect.top - rootRect.top - 8;
el.value.style.left = left + "px"; el.value!.style.left = `${left}px`;
el.value.style.top = top + "px"; el.value!.style.top = `${top}px`;
} }
function onChildClosed(actioned?: boolean) { function onChildClosed(actioned?: boolean) {
@ -58,7 +58,7 @@ onMounted(() => {
defineExpose({ defineExpose({
checkHit: (ev: MouseEvent) => { checkHit: (ev: MouseEvent) => {
return ev.target === el.value || el.value.contains(ev.target); return ev.target === el.value || el.value?.contains(ev.target as Node);
}, },
}); });
</script> </script>

View File

@ -89,7 +89,8 @@
></span> ></span>
</a> </a>
<button <button
v-else-if="item.type === 'user' && !items.hidden" v-else-if="item.type === 'user'"
v-show="!item.hidden"
class="_button item" class="_button item"
:class="{ active: item.active }" :class="{ active: item.active }"
:disabled="item.active" :disabled="item.active"
@ -201,6 +202,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
type Ref,
defineAsyncComponent, defineAsyncComponent,
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
@ -213,6 +215,7 @@ import type {
InnerMenuItem, InnerMenuItem,
MenuAction, MenuAction,
MenuItem, MenuItem,
MenuParent,
MenuPending, MenuPending,
} from "@/types/menu"; } from "@/types/menu";
import * as os from "@/os"; import * as os from "@/os";
@ -234,21 +237,29 @@ const props = defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "close", actioned?: boolean): void; close: [actioned?: boolean];
}>(); }>();
const itemsEl = ref<HTMLDivElement>(); const itemsEl = ref<HTMLDivElement>();
const items2: InnerMenuItem[] = ref([]); /**
* Strictly speaking, this type conversion is wrong
* because `ref` will deeply unpack the `ref` in `MenuSwitch`.
* But it performs correctly, so who cares?
*/
const items2 = ref([]) as Ref<InnerMenuItem[]>;
const child = ref<InstanceType<typeof XChild>>(); const child = ref<InstanceType<typeof XChild>>();
const childShowingItem = ref<MenuItem | null>(); const childShowingItem = ref<MenuItem | null>();
// FIXME: this is not used
const isActive = ref();
watch( watch(
() => props.items, () => props.items,
() => { () => {
const items: (MenuItem | MenuPending)[] = [...props.items].filter( const items: (MenuItem | MenuPending)[] = props.items.filter(
(item) => item !== undefined, (item) => item !== undefined,
); );
@ -288,29 +299,29 @@ function onGlobalMousedown(event: MouseEvent) {
if ( if (
childTarget.value && childTarget.value &&
(event.target === childTarget.value || (event.target === childTarget.value ||
childTarget.value.contains(event.target)) childTarget.value.contains(event.target as Node))
) )
return; return;
if (child.value && child.value.checkHit(event)) return; if (child.value?.checkHit(event)) return;
closeChild(); closeChild();
} }
let childCloseTimer: null | number = null; let childCloseTimer: null | number = null;
function onItemMouseEnter(item) { function onItemMouseEnter(_item) {
childCloseTimer = window.setTimeout(() => { childCloseTimer = window.setTimeout(() => {
closeChild(); closeChild();
}, 300); }, 300);
} }
function onItemMouseLeave(item) { function onItemMouseLeave(_item) {
if (childCloseTimer) window.clearTimeout(childCloseTimer); if (childCloseTimer) window.clearTimeout(childCloseTimer);
} }
async function showChildren(item: MenuItem, ev: MouseEvent) { async function showChildren(item: MenuParent, ev: MouseEvent) {
if (props.asDrawer) { if (props.asDrawer) {
os.popupMenu(item.children, ev.currentTarget ?? ev.target); os.popupMenu(item.children, (ev.currentTarget ?? ev.target) as HTMLElement);
close(); close();
} else { } else {
childTarget.value = ev.currentTarget ?? ev.target; childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
childMenu.value = item.children; childMenu.value = item.children;
childShowingItem.value = item; childShowingItem.value = item;
} }

View File

@ -20,7 +20,7 @@
:stroke="color" :stroke="color"
stroke-width="2" stroke-width="2"
/> />
<circle :cx="headX" :cy="headY" r="3" :fill="color" /> <circle :cx="headX ?? undefined" :cy="headY ?? undefined" r="3" :fill="color" />
</svg> </svg>
</template> </template>

View File

@ -140,7 +140,7 @@ const patternShow = ref(false);
const modPattern = ref<HTMLDivElement>(); const modPattern = ref<HTMLDivElement>();
const progress = ref<typeof FormRange>(); const progress = ref<typeof FormRange>();
const position = ref(0); const position = ref(0);
const patData = shallowRef([] as ModRow[][]); const patData = shallowRef<readonly ModRow[][]>([]);
const currentPattern = ref(0); const currentPattern = ref(0);
const nbChannels = ref(0); const nbChannels = ref(0);
const length = ref(1); const length = ref(1);
@ -159,7 +159,7 @@ function load() {
error.value = false; error.value = false;
fetching.value = false; fetching.value = false;
}) })
.catch((e: any) => { .catch((e: unknown) => {
console.error(e); console.error(e);
error.value = true; error.value = true;
fetching.value = false; fetching.value = false;
@ -293,12 +293,13 @@ function isRowActive(i: number) {
} }
return true; return true;
} }
return false;
} }
function indexText(i: number) { function indexText(i: number) {
let rowText = i.toString(16); let rowText = i.toString(16);
if (rowText.length === 1) { if (rowText.length === 1) {
rowText = "0" + rowText; rowText = `0${rowText}`;
} }
return rowText; return rowText;
} }

View File

@ -108,8 +108,11 @@ type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
manualShowing?: boolean | null; manualShowing?: boolean | null;
anchor?: { x: string; y: string }; anchor?: {
src?: HTMLElement; x: "left" | "center" | "right";
y: "top" | "center" | "bottom";
};
src?: HTMLElement | null;
preferType?: ModalTypes | "auto"; preferType?: ModalTypes | "auto";
zPriority?: "low" | "middle" | "high"; zPriority?: "low" | "middle" | "high";
noOverlap?: boolean; noOverlap?: boolean;
@ -118,7 +121,7 @@ const props = withDefaults(
}>(), }>(),
{ {
manualShowing: null, manualShowing: null,
src: undefined, src: null,
anchor: () => ({ x: "center", y: "bottom" }), anchor: () => ({ x: "center", y: "bottom" }),
preferType: "auto", preferType: "auto",
zPriority: "low", zPriority: "low",
@ -139,6 +142,9 @@ const emit = defineEmits<{
provide("modal", true); provide("modal", true);
// FIXME: this may not used
const isActive = ref();
const maxHeight = ref<number>(); const maxHeight = ref<number>();
const fixed = ref(false); const fixed = ref(false);
const transformOrigin = ref("center"); const transformOrigin = ref("center");
@ -189,8 +195,8 @@ const transitionDuration = computed(() =>
let contentClicking = false; let contentClicking = false;
const focusedElement = document.activeElement; const focusedElement = document.activeElement as HTMLElement;
function close(_ev, opts: { useSendAnimation?: boolean } = {}) { function close(_ev?, opts: { useSendAnimation?: boolean } = {}) {
// removeEventListener("popstate", close); // removeEventListener("popstate", close);
// if (props.preferType == "dialog") { // if (props.preferType == "dialog") {
// history.forward(); // history.forward();
@ -204,7 +210,7 @@ function close(_ev, opts: { useSendAnimation?: boolean } = {}) {
showing.value = false; showing.value = false;
emit("close"); emit("close");
if (!props.noReturnFocus) { if (!props.noReturnFocus) {
focusedElement.focus(); focusedElement?.focus();
} }
} }
@ -235,8 +241,8 @@ const align = () => {
const width = content.value!.offsetWidth; const width = content.value!.offsetWidth;
const height = content.value!.offsetHeight; const height = content.value!.offsetHeight;
let left: number; let left = 0;
let top: number; let top = MARGIN;
const x = srcRect.left + (fixed.value ? 0 : window.scrollX); const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
const y = srcRect.top + (fixed.value ? 0 : window.scrollY); const y = srcRect.top + (fixed.value ? 0 : window.scrollY);

View File

@ -29,7 +29,7 @@
<button <button
class="_button" class="_button"
:aria-label="i18n.ts.close" :aria-label="i18n.ts.close"
@click="$refs.modal.close()" @click="modal!.close()"
> >
<i :class="icon('ph-x')"></i> <i :class="icon('ph-x')"></i>
</button> </button>
@ -65,6 +65,7 @@ import type { PageMetadata } from "@/scripts/page-metadata";
import { provideMetadataReceiver } from "@/scripts/page-metadata"; import { provideMetadataReceiver } from "@/scripts/page-metadata";
import { Router } from "@/nirax"; import { Router } from "@/nirax";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { MenuItem } from "@/types/menu";
const props = defineProps<{ const props = defineProps<{
initialPath: string; initialPath: string;
@ -81,11 +82,11 @@ router.addListener("push", (ctx) => {});
const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
const rootEl = ref(); const rootEl = ref();
const modal = ref<InstanceType<typeof MkModal>>(); const modal = ref<InstanceType<typeof MkModal> | null>(null);
const path = ref(props.initialPath); const path = ref(props.initialPath);
const width = ref(860); const width = ref(860);
const height = ref(660); const height = ref(660);
const history = []; const history: string[] = [];
provide("router", router); provide("router", router);
provideMetadataReceiver((info) => { provideMetadataReceiver((info) => {
@ -95,7 +96,7 @@ provide("shouldOmitHeaderTitle", true);
provide("shouldHeaderThin", true); provide("shouldHeaderThin", true);
const pageUrl = computed(() => url + path.value); const pageUrl = computed(() => url + path.value);
const contextmenu = computed(() => { const contextmenu = computed((): MenuItem[] => {
return [ return [
{ {
type: "label", type: "label",
@ -117,7 +118,7 @@ const contextmenu = computed(() => {
text: i18n.ts.openInNewTab, text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(pageUrl.value, "_blank"); window.open(pageUrl.value, "_blank");
modal.value.close(); modal.value!.close();
}, },
}, },
{ {
@ -130,23 +131,26 @@ const contextmenu = computed(() => {
]; ];
}); });
function navigate(path, record = true) { function navigate(path: string, record = true) {
if (record) history.push(router.getCurrentPath()); if (record) history.push(router.getCurrentPath());
router.push(path); router.push(path);
} }
function back() { function back() {
navigate(history.pop(), false); const backTo = history.pop();
if (backTo) {
navigate(backTo, false);
}
} }
function expand() { function expand() {
mainRouter.push(path.value); mainRouter.push(path.value);
modal.value.close(); modal.value!.close();
} }
function popout() { function popout() {
_popout(path.value, rootEl.value); _popout(path.value, rootEl.value);
modal.value.close(); modal.value!.close();
} }
function onContextmenu(ev: MouseEvent) { function onContextmenu(ev: MouseEvent) {

View File

@ -15,7 +15,7 @@
height: scroll height: scroll
? height ? height
? `${props.height}px` ? `${props.height}px`
: null : undefined
: height : height
? `min(${props.height}px, 100%)` ? `min(${props.height}px, 100%)`
: '100%', : '100%',
@ -54,7 +54,10 @@
</button> </button>
</div> </div>
<div class="body"> <div class="body">
<slot></slot> <slot
:width="width"
:height="height"
></slot>
</div> </div>
</div> </div>
</FocusTrap> </FocusTrap>
@ -62,7 +65,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef } from "vue"; import { ref, shallowRef } from "vue";
import { FocusTrap } from "focus-trap-vue"; import { FocusTrap } from "focus-trap-vue";
import MkModal from "./MkModal.vue"; import MkModal from "./MkModal.vue";
@ -93,11 +96,14 @@ const emit = defineEmits<{
(event: "ok"): void; (event: "ok"): void;
}>(); }>();
// FIXME: seems that this is not used
const isActive = ref();
const modal = shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const headerEl = shallowRef<HTMLElement>(); const headerEl = shallowRef<HTMLElement>();
const close = (ev) => { const close = (ev?) => {
modal.value?.close(ev); modal.value?.close(ev);
}; };

View File

@ -9,7 +9,7 @@
v-vibrate="5" v-vibrate="5"
:aria-label="accessibleLabel" :aria-label="accessibleLabel"
class="tkcbzcuz note-container" class="tkcbzcuz note-container"
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : undefined"
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
> >
<MkNoteSub <MkNoteSub
@ -112,9 +112,9 @@
:note="appearNote" :note="appearNote"
:detailed="true" :detailed="true"
:detailed-view="detailedView" :detailed-view="detailedView"
:parent-id="appearNote.parentId" :parent-id="appearNote.id"
@push="(e) => router.push(notePage(e))" @push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()" @focusfooter="footerEl!.focus()"
@expanded="(e) => setPostExpanded(e)" @expanded="(e) => setPostExpanded(e)"
></MkSubNoteContent> ></MkSubNoteContent>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
@ -312,11 +312,17 @@ import { notePage } from "@/filters/note";
import { deepClone } from "@/scripts/clone"; import { deepClone } from "@/scripts/clone";
import { getNoteSummary } from "@/scripts/get-note-summary"; import { getNoteSummary } from "@/scripts/get-note-summary";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { NoteTranslation } from "@/types/note";
const router = useRouter(); const router = useRouter();
type NoteType = entities.Note & {
_featuredId_?: string;
_prId_?: string;
};
const props = defineProps<{ const props = defineProps<{
note: entities.Note; note: NoteType;
pinned?: boolean; pinned?: boolean;
detailedView?: boolean; detailedView?: boolean;
collapsedReply?: boolean; collapsedReply?: boolean;
@ -354,18 +360,18 @@ const isRenote =
note.value.fileIds.length === 0 && note.value.fileIds.length === 0 &&
note.value.poll == null; note.value.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement | null>(null);
const footerEl = ref<HTMLElement>(); const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
const renoteTime = ref<HTMLElement>(); const renoteTime = ref<HTMLElement>();
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement | null>(null);
const appearNote = computed(() => const appearNote = computed(() =>
isRenote ? (note.value.renote as entities.Note) : note.value, isRenote ? (note.value.renote as NoteType) : note.value,
); );
const isMyRenote = isSignedIn && me.id === note.value.userId; const isMyRenote = isSignedIn && me!.id === note.value.userId;
const showContent = ref(false); // const showContent = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref( const muted = ref(
getWordSoftMute( getWordSoftMute(
@ -375,7 +381,7 @@ const muted = ref(
defaultStore.state.mutedLangs, defaultStore.state.mutedLangs,
), ),
); );
const translation = ref(null); const translation = ref<NoteTranslation | null>(null);
const translating = ref(false); const translating = ref(false);
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick; const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
@ -391,7 +397,7 @@ const isForeignLanguage: boolean =
return postLang !== "" && postLang !== targetLang; return postLang !== "" && postLang !== targetLang;
})(); })();
async function translate_(noteId, targetLang: string) { async function translate_(noteId: string, targetLang: string) {
return await os.api("notes/translate", { return await os.api("notes/translate", {
noteId, noteId,
targetLang, targetLang,
@ -421,12 +427,13 @@ async function translate() {
const keymap = { const keymap = {
r: () => reply(true), r: () => reply(true),
"e|a|plus": () => react(true), "e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true), q: () => renoteButton.value!.renote(true),
"up|k": focusBefore, "up|k": focusBefore,
"down|j": focusAfter, "down|j": focusAfter,
esc: blur, esc: blur,
"m|o": () => menu(true), "m|o": () => menu(true),
s: () => showContent.value !== showContent.value, // FIXME: What's this?
// s: () => showContent.value !== showContent.value,
}; };
if (appearNote.value.historyId == null) { if (appearNote.value.historyId == null) {
@ -437,12 +444,12 @@ if (appearNote.value.historyId == null) {
}); });
} }
function reply(viaKeyboard = false): void { function reply(_viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
os.post( os.post(
{ {
reply: appearNote.value, reply: appearNote.value,
animation: !viaKeyboard, // animation: !viaKeyboard,
}, },
() => { () => {
focus(); focus();
@ -450,11 +457,11 @@ function reply(viaKeyboard = false): void {
); );
} }
function react(viaKeyboard = false): void { function react(_viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
blur(); blur();
reactionPicker.show( reactionPicker.show(
reactButton.value, reactButton.value!,
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: appearNote.value.id, noteId: appearNote.value.id,
@ -467,7 +474,7 @@ function react(viaKeyboard = false): void {
); );
} }
function undoReact(note): void { function undoReact(note: NoteType): void {
const oldReaction = note.myReaction; const oldReaction = note.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
os.api("notes/reactions/delete", { os.api("notes/reactions/delete", {
@ -481,16 +488,17 @@ const currentClipPage = inject<Ref<entities.Clip> | null>(
); );
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement): boolean => {
if (el.tagName === "A") return true; if (el.tagName === "A") return true;
// The Audio element's context menu is the browser default, such as for selecting playback speed. // The Audio element's context menu is the browser default, such as for selecting playback speed.
if (el.tagName === "AUDIO") return true; if (el.tagName === "AUDIO") return true;
if (el.parentElement) { if (el.parentElement) {
return isLink(el.parentElement); return isLink(el.parentElement);
} }
return false;
}; };
if (isLink(ev.target)) return; if (isLink(ev.target as HTMLElement)) return;
if (window.getSelection().toString() !== "") return; if (window.getSelection()?.toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault(); ev.preventDefault();
@ -509,7 +517,7 @@ function onContextmenu(ev: MouseEvent): void {
os.pageWindow(notePage(appearNote.value)); os.pageWindow(notePage(appearNote.value));
}, },
}, },
notePage(appearNote.value) != location.pathname notePage(appearNote.value) !== location.pathname
? { ? {
icon: `${icon("ph-arrows-out-simple")}`, icon: `${icon("ph-arrows-out-simple")}`,
text: i18n.ts.showInPage, text: i18n.ts.showInPage,
@ -589,11 +597,11 @@ function showRenoteMenu(viaKeyboard = false): void {
} }
function focus() { function focus() {
el.value.focus(); el.value!.focus();
} }
function blur() { function blur() {
el.value.blur(); el.value!.blur();
} }
function focusBefore() { function focusBefore() {
@ -605,12 +613,12 @@ function focusAfter() {
} }
function scrollIntoView() { function scrollIntoView() {
el.value.scrollIntoView(); el.value!.scrollIntoView();
} }
function noteClick(e) { function noteClick(e) {
if ( if (
document.getSelection().type === "Range" || document.getSelection()?.type === "Range" ||
props.detailedView || props.detailedView ||
!expandOnNoteClick !expandOnNoteClick
) { ) {

View File

@ -6,7 +6,7 @@
v-hotkey="keymap" v-hotkey="keymap"
v-size="{ max: [500, 350, 300] }" v-size="{ max: [500, 350, 300] }"
class="lxwezrsl _block" class="lxwezrsl _block"
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : undefined"
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
> >
<MkNoteSub <MkNoteSub
@ -64,7 +64,7 @@
) )
}} }}
</option> </option>
<option v-if="directQuotes?.length > 0" value="quotes"> <option v-if="directQuotes && directQuotes.length > 0" value="quotes">
<!-- <i :class="icon('ph-quotes')"></i> --> <!-- <i :class="icon('ph-quotes')"></i> -->
{{ {{
wordWithCount( wordWithCount(
@ -102,7 +102,7 @@
:detailed-view="true" :detailed-view="true"
:parent-id="note.id" :parent-id="note.id"
/> />
<MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" /> <MkLoading v-else-if="tab === 'quotes' && directQuotes && directQuotes.length > 0" />
<!-- <MkPagination <!-- <MkPagination
v-if="tab === 'renotes'" v-if="tab === 'renotes'"
@ -225,12 +225,12 @@ if (noteViewInterruptors.length > 0) {
}); });
} }
const el = ref<HTMLElement>(); const el = ref<HTMLElement | null>(null);
const noteEl = ref(); const noteEl = ref();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement>();
const showContent = ref(false); // const showContent = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref( const muted = ref(
getWordSoftMute( getWordSoftMute(
@ -248,7 +248,8 @@ const directReplies = ref<null | entities.Note[]>([]);
const directQuotes = ref<null | entities.Note[]>([]); const directQuotes = ref<null | entities.Note[]>([]);
const clips = ref(); const clips = ref();
const renotes = ref(); const renotes = ref();
let isScrolling; const isRenote = ref(note.value.renoteId != null);
let isScrolling: boolean;
const reactionsCount = Object.values(props.note.reactions).reduce( const reactionsCount = Object.values(props.note.reactions).reduce(
(x, y) => x + y, (x, y) => x + y,
@ -258,10 +259,10 @@ const reactionsCount = Object.values(props.note.reactions).reduce(
const keymap = { const keymap = {
r: () => reply(true), r: () => reply(true),
"e|a|plus": () => react(true), "e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true), q: () => renoteButton.value!.renote(true),
esc: blur, esc: blur,
"m|o": () => menu(true), "m|o": () => menu(true),
s: () => showContent.value !== showContent.value, // s: () => showContent.value !== showContent.value,
}; };
useNoteCapture({ useNoteCapture({
@ -270,21 +271,21 @@ useNoteCapture({
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
function reply(viaKeyboard = false): void { function reply(_viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
os.post({ os.post({
reply: note.value, reply: note.value,
animation: !viaKeyboard, // animation: !viaKeyboard,
}).then(() => { }).then(() => {
focus(); focus();
}); });
} }
function react(viaKeyboard = false): void { function react(_viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
blur(); blur();
reactionPicker.show( reactionPicker.show(
reactButton.value, reactButton.value!,
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: note.value.id, noteId: note.value.id,
@ -297,13 +298,13 @@ function react(viaKeyboard = false): void {
); );
} }
function undoReact(note): void { // function undoReact(note): void {
const oldReaction = note.myReaction; // const oldReaction = note.myReaction;
if (!oldReaction) return; // if (!oldReaction) return;
os.api("notes/reactions/delete", { // os.api("notes/reactions/delete", {
noteId: note.id, // noteId: note.id,
}); // });
} // }
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement) => {
@ -312,8 +313,8 @@ function onContextmenu(ev: MouseEvent): void {
return isLink(el.parentElement); return isLink(el.parentElement);
} }
}; };
if (isLink(ev.target)) return; if (isLink(ev.target as HTMLElement)) return;
if (window.getSelection().toString() !== "") return; if (window.getSelection()?.toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault(); ev.preventDefault();
@ -362,12 +363,17 @@ os.api("notes/children", {
limit: 30, limit: 30,
depth: 12, depth: 12,
}).then((res) => { }).then((res) => {
res = res.reduce((acc, resNote) => { // biome-ignore lint/style/noParameterAssign: assign it intentially
if (resNote.userId == note.value.userId) { res = res
return [...acc, resNote]; .filter((n) => n.userId !== note.value.userId)
} .reverse()
return [resNote, ...acc]; .concat(res.filter((n) => n.userId === note.value.userId));
}, []); // res = res.reduce((acc: entities.Note[], resNote) => {
// if (resNote.userId === note.value.userId) {
// return [...acc, resNote];
// }
// return [resNote, ...acc];
// }, []);
replies.value = res; replies.value = res;
directReplies.value = res directReplies.value = res
.filter((resNote) => resNote.replyId === note.value.id) .filter((resNote) => resNote.replyId === note.value.id)
@ -438,7 +444,7 @@ async function onNoteUpdated(
} }
switch (type) { switch (type) {
case "replied": case "replied": {
const { id: createdId } = body; const { id: createdId } = body;
const replyNote = await os.api("notes/show", { const replyNote = await os.api("notes/show", {
noteId: createdId, noteId: createdId,
@ -446,10 +452,10 @@ async function onNoteUpdated(
replies.value.splice(found, 0, replyNote); replies.value.splice(found, 0, replyNote);
if (found === 0) { if (found === 0) {
directReplies.value.push(replyNote); directReplies.value!.push(replyNote);
} }
break; break;
}
case "deleted": case "deleted":
if (found === 0) { if (found === 0) {
isDeleted.value = true; isDeleted.value = true;

View File

@ -1,9 +1,9 @@
<template> <template>
<div v-size="{ min: [350, 500] }" class="fefdfafb"> <div v-size="{ min: [350, 500] }" class="fefdfafb">
<MkAvatar class="avatar" :user="me" disable-link /> <MkAvatar class="avatar" :user="me!" disable-link />
<div class="main"> <div class="main">
<div class="header"> <div class="header">
<MkUserName :user="me" /> <MkUserName :user="me!" />
</div> </div>
<div class="body"> <div class="body">
<div class="content"> <div class="content">

View File

@ -1,7 +1,7 @@
<template> <template>
<article <article
v-if="!muted.muted || muted.what === 'reply'" v-if="!muted.muted || muted.what === 'reply'"
:id="detailedView ? appearNote.id : null" :id="detailedView ? appearNote.id : undefined"
ref="el" ref="el"
v-size="{ max: [450, 500] }" v-size="{ max: [450, 500] }"
class="wrpstxzv" class="wrpstxzv"
@ -35,10 +35,10 @@
:parent-id="parentId" :parent-id="parentId"
:conversation="conversation" :conversation="conversation"
:detailed-view="detailedView" :detailed-view="detailedView"
@focusfooter="footerEl.focus()" @focusfooter="footerEl!.focus()"
/> />
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating || translation == null" mini />
<div v-else class="translated"> <div v-else class="translated">
<b <b
>{{ >{{
@ -217,6 +217,7 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { deepClone } from "@/scripts/clone"; import { deepClone } from "@/scripts/clone";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { NoteTranslation } from "@/types/note";
const router = useRouter(); const router = useRouter();
@ -256,12 +257,12 @@ const isRenote =
note.value.fileIds.length === 0 && note.value.fileIds.length === 0 &&
note.value.poll == null; note.value.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement | null>(null);
const footerEl = ref<HTMLElement>(); const footerEl = ref<HTMLElement | null>(null);
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton> | null>(null);
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement | null>(null);
const appearNote = computed(() => const appearNote = computed(() =>
isRenote ? (note.value.renote as entities.Note) : note.value, isRenote ? (note.value.renote as entities.Note) : note.value,
); );
@ -274,7 +275,7 @@ const muted = ref(
defaultStore.state.mutedLangs, defaultStore.state.mutedLangs,
), ),
); );
const translation = ref(null); const translation = ref<NoteTranslation | null>(null);
const translating = ref(false); const translating = ref(false);
const replies: entities.Note[] = const replies: entities.Note[] =
props.conversation props.conversation
@ -330,21 +331,21 @@ useNoteCapture({
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
function reply(viaKeyboard = false): void { function reply(_viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
os.post({ os.post({
reply: appearNote.value, reply: appearNote.value,
animation: !viaKeyboard, // animation: !viaKeyboard,
}).then(() => { }).then(() => {
focus(); focus();
}); });
} }
function react(viaKeyboard = false): void { function react(_viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
blur(); blur();
reactionPicker.show( reactionPicker.show(
reactButton.value, reactButton.value!,
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: appearNote.value.id, noteId: appearNote.value.id,
@ -388,14 +389,15 @@ function menu(viaKeyboard = false): void {
} }
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement | null) => {
if (el == null) return;
if (el.tagName === "A") return true; if (el.tagName === "A") return true;
if (el.parentElement) { if (el.parentElement) {
return isLink(el.parentElement); return isLink(el.parentElement);
} }
}; };
if (isLink(ev.target)) return; if (isLink(ev.target as HTMLElement | null)) return;
if (window.getSelection().toString() !== "") return; if (window.getSelection()?.toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault(); ev.preventDefault();
@ -414,7 +416,7 @@ function onContextmenu(ev: MouseEvent): void {
os.pageWindow(notePage(appearNote.value)); os.pageWindow(notePage(appearNote.value));
}, },
}, },
notePage(appearNote.value) != location.pathname notePage(appearNote.value) !== location.pathname
? { ? {
icon: `${icon("ph-arrows-out-simple")}`, icon: `${icon("ph-arrows-out-simple")}`,
text: i18n.ts.showInPage, text: i18n.ts.showInPage,
@ -454,15 +456,15 @@ function onContextmenu(ev: MouseEvent): void {
} }
function focus() { function focus() {
el.value.focus(); el.value!.focus();
} }
function blur() { function blur() {
el.value.blur(); el.value!.blur();
} }
function noteClick(e) { function noteClick(e: MouseEvent) {
if (document.getSelection().type === "Range" || !expandOnNoteClick) { if (document.getSelection()?.type === "Range" || !expandOnNoteClick) {
e.stopPropagation(); e.stopPropagation();
} else { } else {
router.push(notePage(props.note)); router.push(notePage(props.note));

View File

@ -40,8 +40,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import type {
MkPaginationType,
PagingKeyOf,
PagingOf,
} from "@/components/MkPagination.vue";
import type { entities } from "firefish-js"; import type { entities } from "firefish-js";
import type { PagingOf } from "@/components/MkPagination.vue";
import XNote from "@/components/MkNote.vue"; import XNote from "@/components/MkNote.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
@ -56,10 +60,14 @@ defineProps<{
disableAutoLoad?: boolean; disableAutoLoad?: boolean;
}>(); }>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = ref<MkPaginationType<
PagingKeyOf<entities.Note>
> | null>(null);
function scrollTop() { function scrollTop() {
scroll(tlEl.value, { top: 0, behavior: "smooth" }); if (tlEl.value) {
scroll(tlEl.value, { top: 0, behavior: "smooth" });
}
} }
defineExpose({ defineExpose({

View File

@ -12,12 +12,12 @@
:user="notification.note.user" :user="notification.note.user"
/> />
<MkAvatar <MkAvatar
v-else-if="notification.user" v-else-if="'user' in notification"
class="icon" class="icon"
:user="notification.user" :user="notification.user"
/> />
<img <img
v-else-if="notification.icon" v-else-if="'icon' in notification && notification.icon"
class="icon" class="icon"
:src="notification.icon" :src="notification.icon"
alt="" alt=""
@ -95,7 +95,7 @@
i18n.ts._notification.pollEnded i18n.ts._notification.pollEnded
}}</span> }}</span>
<MkA <MkA
v-else-if="notification.user" v-else-if="'user' in notification"
v-user-preview="notification.user.id" v-user-preview="notification.user.id"
class="name" class="name"
:to="userPage(notification.user)" :to="userPage(notification.user)"
@ -133,7 +133,7 @@
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:lang="notification.note.lang" :lang="notification.note.lang"
:custom-emojis="notification.note.renote.emojis" :custom-emojis="notification.note.renote!.emojis"
/> />
</MkA> </MkA>
<MkA <MkA
@ -212,6 +212,7 @@
style="opacity: 0.7" style="opacity: 0.7"
>{{ i18n.ts.youGotNewFollower }} >{{ i18n.ts.youGotNewFollower }}
<div v-if="full && !hideFollowButton"> <div v-if="full && !hideFollowButton">
<!-- FIXME: Provide a UserDetailed here -->
<MkFollowButton <MkFollowButton
:user="notification.user" :user="notification.user"
:full="true" :full="true"
@ -269,7 +270,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref, watch } from "vue"; import { onMounted, onUnmounted, ref, toRef, watch } from "vue";
import type { entities } from "firefish-js"; import type { entities } from "firefish-js";
import XReactionIcon from "@/components/MkReactionIcon.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
@ -284,6 +285,8 @@ import { useTooltip } from "@/scripts/use-tooltip";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { instance } from "@/instance"; import { instance } from "@/instance";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { Connection } from "firefish-js/src/streaming";
import type { Channels } from "firefish-js/src/streaming.types";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -299,8 +302,8 @@ const props = withDefaults(
const stream = useStream(); const stream = useStream();
const elRef = ref<HTMLElement>(null); const elRef = ref<HTMLElement | null>(null);
const reactionRef = ref(null); const reactionRef = ref<InstanceType<typeof XReactionIcon> | null>(null);
const hideFollowButton = defaultStore.state.hideFollowButtons; const hideFollowButton = defaultStore.state.hideFollowButtons;
const showEmojiReactions = const showEmojiReactions =
@ -311,7 +314,7 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
: "⭐"; : "⭐";
let readObserver: IntersectionObserver | undefined; let readObserver: IntersectionObserver | undefined;
let connection; let connection: Connection<Channels["main"]> | null = null;
onMounted(() => { onMounted(() => {
if (!props.notification.isRead) { if (!props.notification.isRead) {
@ -323,13 +326,13 @@ onMounted(() => {
observer.disconnect(); observer.disconnect();
}); });
readObserver.observe(elRef.value); readObserver.observe(elRef.value!);
connection = stream.useChannel("main"); connection = stream.useChannel("main");
connection.on("readAllNotifications", () => readObserver.disconnect()); connection.on("readAllNotifications", () => readObserver!.disconnect());
watch(props.notification.isRead, () => { watch(toRef(props.notification.isRead), () => {
readObserver.disconnect(); readObserver!.disconnect();
}); });
} }
}); });
@ -344,38 +347,47 @@ const groupInviteDone = ref(false);
const acceptFollowRequest = () => { const acceptFollowRequest = () => {
followRequestDone.value = true; followRequestDone.value = true;
os.api("following/requests/accept", { userId: props.notification.user.id }); os.api("following/requests/accept", {
userId: (props.notification as entities.ReceiveFollowRequestNotification)
.user.id,
});
}; };
const rejectFollowRequest = () => { const rejectFollowRequest = () => {
followRequestDone.value = true; followRequestDone.value = true;
os.api("following/requests/reject", { userId: props.notification.user.id }); os.api("following/requests/reject", {
userId: (props.notification as entities.ReceiveFollowRequestNotification)
.user.id,
});
}; };
const acceptGroupInvitation = () => { const acceptGroupInvitation = () => {
groupInviteDone.value = true; groupInviteDone.value = true;
os.apiWithDialog("users/groups/invitations/accept", { os.apiWithDialog("users/groups/invitations/accept", {
invitationId: props.notification.invitation.id, invitationId: (props.notification as entities.GroupInvitedNotification)
.invitation.id,
}); });
}; };
const rejectGroupInvitation = () => { const rejectGroupInvitation = () => {
groupInviteDone.value = true; groupInviteDone.value = true;
os.api("users/groups/invitations/reject", { os.api("users/groups/invitations/reject", {
invitationId: props.notification.invitation.id, invitationId: (props.notification as entities.GroupInvitedNotification)
.invitation.id,
}); });
}; };
useTooltip(reactionRef, (showing) => { useTooltip(reactionRef, (showing) => {
const n = props.notification as entities.ReactionNotification;
os.popup( os.popup(
XReactionTooltip, XReactionTooltip,
{ {
showing, showing,
reaction: props.notification.reaction reaction: n.reaction
? props.notification.reaction.replace(/^:(\w+):$/, ":$1@.:") ? n.reaction.replace(/^:(\w+):$/, ":$1@.:")
: props.notification.reaction, : n.reaction,
emojis: props.notification.note.emojis, emojis: n.note.emojis,
targetElement: reactionRef.value.$el, targetElement: reactionRef.value!.$el,
}, },
{}, {},
"closed", "closed",

View File

@ -6,7 +6,7 @@
:with-ok-button="true" :with-ok-button="true"
:ok-button-disabled="false" :ok-button-disabled="false"
@ok="ok()" @ok="ok()"
@close="dialog.close()" @close="dialog!.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.notificationSetting }}</template> <template #header>{{ i18n.ts.notificationSetting }}</template>
@ -68,7 +68,7 @@ const includingTypes = computed(() => props.includingTypes || []);
const dialog = ref<InstanceType<typeof XModalWindow>>(); const dialog = ref<InstanceType<typeof XModalWindow>>();
const typesMap = ref<Record<(typeof notificationTypes)[number], boolean>>({}); const typesMap = ref({} as Record<(typeof notificationTypes)[number], boolean>);
const useGlobalSetting = ref( const useGlobalSetting = ref(
(includingTypes.value === null || includingTypes.value.length === 0) && (includingTypes.value === null || includingTypes.value.length === 0) &&
props.showGlobalToggle, props.showGlobalToggle,
@ -89,7 +89,7 @@ function ok() {
}); });
} }
dialog.value.close(); dialog.value!.close();
} }
function disableAll() { function disableAll() {

View File

@ -19,9 +19,10 @@ import { onMounted, ref } from "vue";
import XNotification from "@/components/MkNotification.vue"; import XNotification from "@/components/MkNotification.vue";
import * as os from "@/os"; import * as os from "@/os";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import type { entities } from "firefish-js";
defineProps<{ defineProps<{
notification: any; // TODO notification: entities.Notification;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

View File

@ -44,7 +44,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from "vue"; import { computed, onMounted, onUnmounted, ref } from "vue";
import type { StreamTypes, entities, notificationTypes } from "firefish-js"; import type { StreamTypes, entities, notificationTypes } from "firefish-js";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination, {
type MkPaginationType,
} from "@/components/MkPagination.vue";
import XNotification from "@/components/MkNotification.vue"; import XNotification from "@/components/MkNotification.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MkNote.vue"; import XNote from "@/components/MkNote.vue";
@ -59,7 +61,7 @@ const props = defineProps<{
const stream = useStream(); const stream = useStream();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = ref<MkPaginationType<"i/notifications"> | null>(null);
const pagination = { const pagination = {
endpoint: "i/notifications" as const, endpoint: "i/notifications" as const,

View File

@ -3,7 +3,7 @@
:to="`/@${page.user.username}/pages/${page.name}`" :to="`/@${page.user.username}/pages/${page.name}`"
class="vhpxefrj _block" class="vhpxefrj _block"
tabindex="-1" tabindex="-1"
:behavior="`${ui === 'deck' ? 'window' : null}`" :behavior="ui === 'deck' ? 'window' : null"
> >
<div <div
v-if="page.eyeCatchingImage" v-if="page.eyeCatchingImage"
@ -36,9 +36,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { userName } from "@/filters/user"; import { userName } from "@/filters/user";
import { ui } from "@/config"; import { ui } from "@/config";
import type { entities } from "firefish-js";
defineProps<{ defineProps<{
page: any; page: entities.Page;
}>(); }>();
</script> </script>

View File

@ -56,23 +56,22 @@ const router = new Router(routes, props.initialPath);
const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
const windowEl = ref<InstanceType<typeof XWindow>>(); const windowEl = ref<InstanceType<typeof XWindow>>();
const history = ref<{ path: string; key: any }[]>([ const history = ref<{ path: string; key: string }[]>([
{ {
path: router.getCurrentPath(), path: router.getCurrentPath(),
key: router.getCurrentKey(), key: router.getCurrentKey(),
}, },
]); ]);
const buttonsLeft = computed(() => { const buttonsLeft = computed(() => {
const buttons = [];
if (history.value.length > 1) { if (history.value.length > 1) {
buttons.push({ return [
icon: `${icon("ph-caret-left")}`, {
onClick: back, icon: `${icon("ph-caret-left")}`,
}); onClick: back,
},
];
} }
return [];
return buttons;
}); });
const buttonsRight = computed(() => { const buttonsRight = computed(() => {
const buttons = [ const buttons = [
@ -114,7 +113,7 @@ const contextmenu = computed(() => [
text: i18n.ts.openInNewTab, text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(url + router.getCurrentPath(), "_blank"); window.open(url + router.getCurrentPath(), "_blank");
windowEl.value.close(); windowEl.value!.close();
}, },
}, },
{ {
@ -135,17 +134,17 @@ function back() {
} }
function close() { function close() {
windowEl.value.close(); windowEl.value!.close();
} }
function expand() { function expand() {
mainRouter.push(router.getCurrentPath(), "forcePage"); mainRouter.push(router.getCurrentPath(), "forcePage");
windowEl.value.close(); windowEl.value!.close();
} }
function popout() { function popout() {
_popout(router.getCurrentPath(), windowEl.value.$el); _popout(router.getCurrentPath(), windowEl.value!.$el);
windowEl.value.close(); windowEl.value!.close();
} }
defineExpose({ defineExpose({

View File

@ -67,7 +67,7 @@
</template> </template>
<script lang="ts" setup generic="E extends PagingKey"> <script lang="ts" setup generic="E extends PagingKey">
import type { ComputedRef } from "vue"; import type { ComponentPublicInstance, ComputedRef } from "vue";
import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue"; import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
import type { Endpoints, TypeUtils } from "firefish-js"; import type { Endpoints, TypeUtils } from "firefish-js";
import * as os from "@/os"; import * as os from "@/os";
@ -81,8 +81,30 @@ import MkButton from "@/components/MkButton.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
/**
* ref type of MkPagination<E>
* Due to Vue's incomplete type support for generic components,
* we have to manually maintain this type instead of
* using `InstanceType<typeof MkPagination>`
*/
export type MkPaginationType<
E extends PagingKey,
Item = Endpoints[E]["res"][number],
> = ComponentPublicInstance & {
items: Item[];
queue: Item[];
backed: boolean;
reload: () => Promise<void>;
refresh: () => Promise<void>;
prepend: (item: Item) => Promise<void>;
append: (item: Item) => Promise<void>;
removeItem: (finder: (item: Item) => boolean) => boolean;
updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
};
export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally // biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
export type PagingKey = TypeUtils.EndpointsOf<any[]>; export type PagingKey = PagingKeyOf<any>;
export interface Paging<E extends PagingKey = PagingKey> { export interface Paging<E extends PagingKey = PagingKey> {
endpoint: E; endpoint: E;

View File

@ -84,25 +84,20 @@ import { formatDateTimeString } from "@/scripts/format-time-string";
import { addTime } from "@/scripts/time"; import { addTime } from "@/scripts/time";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { PollType } from "@/types/post-form";
const props = defineProps<{ const props = defineProps<{
modelValue: { modelValue: PollType;
expiresAt: string;
expiredAfter: number;
choices: string[];
multiple: boolean;
};
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
( "update:modelValue": [
ev: "update:modelValue",
v: { v: {
expiresAt: string; expiresAt?: number;
expiredAfter: number; expiredAfter?: number | null;
choices: string[]; choices: string[];
multiple: boolean; multiple: boolean;
}, },
): void; ];
}>(); }>();
const choices = ref(props.modelValue.choices); const choices = ref(props.modelValue.choices);
@ -147,19 +142,19 @@ function get() {
}; };
const calcAfter = () => { const calcAfter = () => {
let base = parseInt(after.value); let base = Number.parseInt(after.value.toString());
switch (unit.value) { switch (unit.value) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
case "day": case "day":
base *= 24; base *= 24;
// fallthrough // biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
case "hour": case "hour":
base *= 60; base *= 60;
// fallthrough // biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
case "minute": case "minute":
base *= 60; base *= 60;
// fallthrough
case "second": case "second":
return (base *= 1000); return base * 1000;
default: default:
return null; return null;
} }

View File

@ -35,7 +35,7 @@ defineProps<{
align?: "center" | string; align?: "center" | string;
width?: number; width?: number;
viaKeyboard?: boolean; viaKeyboard?: boolean;
src?: any; src?: HTMLElement | null;
noReturnFocus?; noReturnFocus?;
}>(); }>();

View File

@ -20,7 +20,7 @@
class="account _button" class="account _button"
@click="openAccountMenu" @click="openAccountMenu"
> >
<MkAvatar :user="postAccount ?? me" class="avatar" /> <MkAvatar :user="postAccount ?? me!" class="avatar" />
</button> </button>
<div class="right"> <div class="right">
<span <span
@ -297,14 +297,22 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, nextTick, onMounted, ref, watch } from "vue"; import {
type Ref,
computed,
inject,
nextTick,
onMounted,
ref,
watch,
} from "vue";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import autosize from "autosize"; import autosize from "autosize";
import insertTextAtCursor from "insert-text-at-cursor"; import insertTextAtCursor from "insert-text-at-cursor";
import { length } from "stringz"; import { length } from "stringz";
import { toASCII } from "punycode/"; import { toASCII } from "punycode/";
import { acct } from "firefish-js"; import { acct } from "firefish-js";
import type { entities, languages } from "firefish-js"; import type { ApiTypes, entities, languages } from "firefish-js";
import { throttle } from "throttle-debounce"; import { throttle } from "throttle-debounce";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MkNoteSimple.vue";
import XNotePreview from "@/components/MkNotePreview.vue"; import XNotePreview from "@/components/MkNotePreview.vue";
@ -341,6 +349,7 @@ import type { MenuItem } from "@/types/menu";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import MkVisibilityPicker from "@/components/MkVisibilityPicker.vue"; import MkVisibilityPicker from "@/components/MkVisibilityPicker.vue";
import type { NoteVisibility } from "@/types/note"; import type { NoteVisibility } from "@/types/note";
import type { NoteDraft, PollType } from "@/types/post-form";
const modal = inject("modal"); const modal = inject("modal");
@ -348,16 +357,16 @@ const props = withDefaults(
defineProps<{ defineProps<{
reply?: entities.Note; reply?: entities.Note;
renote?: entities.Note; renote?: entities.Note;
channel?: any; // TODO channel?: entities.Channel;
mention?: entities.User; mention?: entities.User;
specified?: entities.User; specified?: entities.User;
initialText?: string; initialText?: string;
initialVisibility?: NoteVisibility; initialVisibility?: NoteVisibility;
initialLanguage?: typeof languages; initialLanguage?: (typeof languages)[number];
initialFiles?: entities.DriveFile[]; initialFiles?: entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: entities.User[]; initialVisibleUsers?: entities.User[];
initialNote?: entities.Note; initialNote?: NoteDraft;
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
autofocus?: boolean; autofocus?: boolean;
@ -390,12 +399,7 @@ const showBigPostButton = defaultStore.state.showBigPostButton;
const posting = ref(false); const posting = ref(false);
const text = ref(props.initialText ?? ""); const text = ref(props.initialText ?? "");
const files = ref(props.initialFiles ?? ([] as entities.DriveFile[])); const files = ref(props.initialFiles ?? ([] as entities.DriveFile[]));
const poll = ref<{ const poll = ref<PollType | null>(null);
choices: string[];
multiple: boolean;
expiresAt: string | null;
expiredAfter: string | null;
} | null>(null);
const useCw = ref(false); const useCw = ref(false);
const showPreview = ref(defaultStore.state.showPreviewByDefault); const showPreview = ref(defaultStore.state.showPreviewByDefault);
const cw = ref<string | null>(null); const cw = ref<string | null>(null);
@ -411,12 +415,12 @@ const visibility = ref(
: defaultStore.state.defaultNoteVisibility), : defaultStore.state.defaultNoteVisibility),
); );
const visibleUsers = ref([]); const visibleUsers = ref<entities.User[]>([]);
if (props.initialVisibleUsers) { if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(pushVisibleUser); props.initialVisibleUsers.forEach(pushVisibleUser);
} }
const draghover = ref(false); const draghover = ref(false);
const quoteId = ref(null); const quoteId = ref<string | null>(null);
const hasNotSpecifiedMentions = ref(false); const hasNotSpecifiedMentions = ref(false);
const recentHashtags = ref( const recentHashtags = ref(
JSON.parse(localStorage.getItem("hashtags") || "[]"), JSON.parse(localStorage.getItem("hashtags") || "[]"),
@ -500,7 +504,9 @@ const canPost = computed((): boolean => {
const withHashtags = computed( const withHashtags = computed(
defaultStore.makeGetterSetter("postFormWithHashtags"), defaultStore.makeGetterSetter("postFormWithHashtags"),
); );
const hashtags = computed(defaultStore.makeGetterSetter("postFormHashtags")); const hashtags = computed(
defaultStore.makeGetterSetter("postFormHashtags"),
) as Ref<string | null>;
watch(text, () => { watch(text, () => {
checkMissingMention(); checkMissingMention();
@ -525,7 +531,7 @@ if (props.mention) {
if ( if (
props.reply && props.reply &&
(props.reply.user.username !== me.username || (props.reply.user.username !== me!.username ||
(props.reply.user.host != null && props.reply.user.host !== host)) (props.reply.user.host != null && props.reply.user.host !== host))
) { ) {
text.value = `@${props.reply.user.username}${ text.value = `@${props.reply.user.username}${
@ -545,7 +551,7 @@ if (props.reply && props.reply.text != null) {
: `@${x.username}@${toASCII(otherHost)}`; : `@${x.username}@${toASCII(otherHost)}`;
// exclude me // exclude me
if (me.username === x.username && (x.host == null || x.host === host)) if (me!.username === x.username && (x.host == null || x.host === host))
continue; continue;
// remove duplicates // remove duplicates
@ -579,7 +585,7 @@ if (
if (props.reply.visibleUserIds) { if (props.reply.visibleUserIds) {
os.api("users/show", { os.api("users/show", {
userIds: props.reply.visibleUserIds.filter( userIds: props.reply.visibleUserIds.filter(
(uid) => uid !== me.id && uid !== props.reply.userId, (uid) => uid !== me!.id && uid !== props.reply!.userId,
), ),
}).then((users) => { }).then((users) => {
users.forEach(pushVisibleUser); users.forEach(pushVisibleUser);
@ -588,7 +594,7 @@ if (
visibility.value = "private"; visibility.value = "private";
} }
if (props.reply.userId !== me.id) { if (props.reply.userId !== me!.id) {
os.api("users/show", { userId: props.reply.userId }).then((user) => { os.api("users/show", { userId: props.reply.userId }).then((user) => {
pushVisibleUser(user); pushVisibleUser(user);
}); });
@ -615,7 +621,7 @@ const addRe = (s: string) => {
if (defaultStore.state.keepCw && props.reply && props.reply.cw) { if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
useCw.value = true; useCw.value = true;
cw.value = cw.value =
props.reply.user.username === me.username props.reply.user.username === me!.username
? props.reply.cw ? props.reply.cw
: addRe(props.reply.cw); : addRe(props.reply.cw);
} }
@ -894,11 +900,14 @@ function onCompositionEnd(ev: CompositionEvent) {
} }
async function onPaste(ev: ClipboardEvent) { async function onPaste(ev: ClipboardEvent) {
if (ev.clipboardData == null) return;
for (const { item, i } of Array.from(ev.clipboardData.items).map( for (const { item, i } of Array.from(ev.clipboardData.items).map(
(item, i) => ({ item, i }), (item, i) => ({ item, i }),
)) { )) {
if (item.kind === "file") { if (item.kind === "file") {
const file = item.getAsFile(); const file = item.getAsFile();
if (file == null) continue;
const lio = file.name.lastIndexOf("."); const lio = file.name.lastIndexOf(".");
const ext = lio >= 0 ? file.name.slice(lio) : ""; const ext = lio >= 0 ? file.name.slice(lio) : "";
const formatted = `${formatTimeString( const formatted = `${formatTimeString(
@ -911,7 +920,7 @@ async function onPaste(ev: ClipboardEvent) {
const paste = ev.clipboardData?.getData("text") ?? ""; const paste = ev.clipboardData?.getData("text") ?? "";
if (!props.renote && !quoteId.value && paste.startsWith(url + "/notes/")) { if (!props.renote && !quoteId.value && paste.startsWith(`${url}/notes/`)) {
ev.preventDefault(); ev.preventDefault();
os.yesno({ os.yesno({
@ -919,13 +928,13 @@ async function onPaste(ev: ClipboardEvent) {
text: i18n.ts.quoteQuestion, text: i18n.ts.quoteQuestion,
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) { if (canceled) {
insertTextAtCursor(textareaEl.value, paste); insertTextAtCursor(textareaEl.value!, paste);
return; return;
} }
quoteId.value = paste quoteId.value = paste
.substring(url.length) .substring(url.length)
.match(/^\/notes\/(.+?)\/?$/)[1]; .match(/^\/notes\/(.+?)\/?$/)![1];
}); });
} }
} }
@ -956,16 +965,17 @@ function onDragover(ev) {
} }
} }
function onDragenter(ev) { function onDragenter(_ev) {
draghover.value = true; draghover.value = true;
} }
function onDragleave(ev) { function onDragleave(_ev) {
draghover.value = false; draghover.value = false;
} }
function onDrop(ev): void { function onDrop(ev: DragEvent): void {
draghover.value = false; draghover.value = false;
if (ev.dataTransfer == null) return;
// //
if (ev.dataTransfer.files.length > 0) { if (ev.dataTransfer.files.length > 0) {
@ -1064,7 +1074,7 @@ async function post() {
const processedText = preprocess(text.value); const processedText = preprocess(text.value);
let postData = { let postData: ApiTypes.NoteSubmitReq = {
editId: props.editId ? props.editId : undefined, editId: props.editId ? props.editId : undefined,
text: processedText === "" ? undefined : processedText, text: processedText === "" ? undefined : processedText,
fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined, fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
@ -1092,7 +1102,7 @@ async function post() {
const hashtags_ = hashtags.value const hashtags_ = hashtags.value
.trim() .trim()
.split(" ") .split(" ")
.map((x) => (x.startsWith("#") ? x : "#" + x)) .map((x) => (x.startsWith("#") ? x : `#${x}`))
.join(" "); .join(" ");
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_; postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
} }
@ -1104,11 +1114,11 @@ async function post() {
} }
} }
let token; let token: string | undefined;
if (postAccount.value) { if (postAccount.value) {
const storedAccounts = await getAccounts(); const storedAccounts = await getAccounts();
token = storedAccounts.find((x) => x.id === postAccount.value.id)?.token; token = storedAccounts.find((x) => x.id === postAccount.value!.id)?.token;
} }
posting.value = true; posting.value = true;
@ -1119,10 +1129,11 @@ async function post() {
deleteDraft(); deleteDraft();
emit("posted"); emit("posted");
if (postData.text && postData.text !== "") { if (postData.text && postData.text !== "") {
const hashtags_ = mfm const hashtags_ = (
.parse(postData.text) mfm
.filter((x) => x.type === "hashtag") .parse(postData.text)
.map((x) => x.props.hashtag); .filter((x) => x.type === "hashtag") as mfm.MfmHashtag[]
).map((x) => x.props.hashtag);
const history = JSON.parse( const history = JSON.parse(
localStorage.getItem("hashtags") || "[]", localStorage.getItem("hashtags") || "[]",
) as string[]; ) as string[];
@ -1133,14 +1144,14 @@ async function post() {
} }
posting.value = false; posting.value = false;
postAccount.value = null; postAccount.value = null;
nextTick(() => autosize.update(textareaEl.value)); nextTick(() => autosize.update(textareaEl.value!));
}); });
}) })
.catch((err) => { .catch((err: { message: string; id: string }) => {
posting.value = false; posting.value = false;
os.alert({ os.alert({
type: "error", type: "error",
text: err.message + "\n" + (err as any).id, text: `${err.message}\n${err.id}`,
}); });
}); });
vibrate([10, 20, 10, 20, 10, 20, 60]); vibrate([10, 20, 10, 20, 10, 20, 60]);
@ -1169,19 +1180,23 @@ function cancel() {
function insertMention() { function insertMention() {
os.selectUser().then((user) => { os.selectUser().then((user) => {
insertTextAtCursor(textareaEl.value, "@" + acct.toString(user) + " "); insertTextAtCursor(textareaEl.value!, `@${acct.toString(user)} `);
}); });
} }
async function insertEmoji(ev: MouseEvent) { async function insertEmoji(ev: MouseEvent) {
os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl.value); os.openEmojiPicker(
(ev.currentTarget ?? ev.target) as HTMLElement,
{},
textareaEl.value,
);
} }
async function openCheatSheet(ev: MouseEvent) { async function openCheatSheet(ev: MouseEvent) {
os.popup(XCheatSheet, {}, {}, "closed"); os.popup(XCheatSheet, {}, {}, "closed");
} }
function showActions(ev) { function showActions(ev: MouseEvent) {
os.popupMenu( os.popupMenu(
postFormActions.map((action) => ({ postFormActions.map((action) => ({
text: action.title, text: action.title,
@ -1198,7 +1213,7 @@ function showActions(ev) {
); );
}, },
})), })),
ev.currentTarget ?? ev.target, (ev.currentTarget ?? ev.target) as HTMLElement,
); );
} }
@ -1209,9 +1224,9 @@ function openAccountMenu(ev: MouseEvent) {
{ {
withExtraOperation: false, withExtraOperation: false,
includeCurrentAccount: true, includeCurrentAccount: true,
active: postAccount.value != null ? postAccount.value.id : me.id, active: postAccount.value != null ? postAccount.value.id : me!.id,
onChoose: (account) => { onChoose: (account) => {
if (account.id === me.id) { if (account.id === me!.id) {
postAccount.value = null; postAccount.value = null;
} else { } else {
postAccount.value = account; postAccount.value = account;
@ -1232,14 +1247,14 @@ onMounted(() => {
} }
// TODO: detach when unmount // TODO: detach when unmount
new Autocomplete(textareaEl.value, text); new Autocomplete(textareaEl.value!, text);
new Autocomplete(cwInputEl.value, cw); new Autocomplete(cwInputEl.value!, cw as Ref<string>);
new Autocomplete(hashtagsInputEl.value, hashtags); new Autocomplete(hashtagsInputEl.value!, hashtags as Ref<string>);
autosize(textareaEl.value); autosize(textareaEl.value!);
nextTick(() => { nextTick(() => {
autosize(textareaEl.value); autosize(textareaEl.value!);
// 稿 // 稿
if (!props.instant && !props.mention && !props.specified) { if (!props.instant && !props.mention && !props.specified) {
const draft = JSON.parse(localStorage.getItem("drafts") || "{}")[ const draft = JSON.parse(localStorage.getItem("drafts") || "{}")[
@ -1275,8 +1290,8 @@ onMounted(() => {
}; };
} }
visibility.value = init.visibility; visibility.value = init.visibility;
localOnly.value = init.localOnly; localOnly.value = init.localOnly ?? false;
language.value = init.lang; language.value = init.lang ?? null;
quoteId.value = init.renote ? init.renote.id : null; quoteId.value = init.renote ? init.renote.id : null;
} }
@ -1289,7 +1304,7 @@ onMounted(() => {
} }
nextTick(() => watchForDraft()); nextTick(() => watchForDraft());
nextTick(() => autosize.update(textareaEl.value)); nextTick(() => autosize.update(textareaEl.value!));
}); });
}); });
</script> </script>

View File

@ -2,7 +2,7 @@
<MkModal <MkModal
ref="modal" ref="modal"
:prefer-type="'dialog'" :prefer-type="'dialog'"
@click="modal.close()" @click="modal!.close()"
@closed="onModalClosed()" @closed="onModalClosed()"
> >
<MkPostForm <MkPostForm
@ -12,8 +12,8 @@
autofocus autofocus
freeze-after-posted freeze-after-posted
@posted="onPosted" @posted="onPosted"
@cancel="modal.close()" @cancel="modal!.close()"
@esc="modal.close()" @esc="modal!.close()"
/> />
</MkModal> </MkModal>
</template> </template>
@ -25,20 +25,21 @@ import type { entities, languages } from "firefish-js";
import MkModal from "@/components/MkModal.vue"; import MkModal from "@/components/MkModal.vue";
import MkPostForm from "@/components/MkPostForm.vue"; import MkPostForm from "@/components/MkPostForm.vue";
import type { NoteVisibility } from "@/types/note"; import type { NoteVisibility } from "@/types/note";
import type { NoteDraft } from "@/types/post-form";
const props = defineProps<{ const props = defineProps<{
reply?: entities.Note; reply?: entities.Note;
renote?: entities.Note; renote?: entities.Note;
channel?: any; // TODO channel?: entities.Channel;
mention?: entities.User; mention?: entities.User;
specified?: entities.User; specified?: entities.User;
initialText?: string; initialText?: string;
initialVisibility?: NoteVisibility; initialVisibility?: NoteVisibility;
initialLanguage?: typeof languages; initialLanguage?: (typeof languages)[number];
initialFiles?: entities.DriveFile[]; initialFiles?: entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: entities.User[]; initialVisibleUsers?: entities.User[];
initialNote?: entities.Note; initialNote?: NoteDraft;
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
autofocus?: boolean; autofocus?: boolean;
@ -53,7 +54,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
const form = shallowRef<InstanceType<typeof MkPostForm>>(); const form = shallowRef<InstanceType<typeof MkPostForm>>();
function onPosted() { function onPosted() {
modal.value.close({ modal.value!.close({
useSendAnimation: true, useSendAnimation: true,
}); });
} }

View File

@ -9,9 +9,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { entities } from "firefish-js";
defineProps<{ defineProps<{
reaction: string; reaction: string;
customEmojis?: any[]; // TODO customEmojis?: entities.EmojiLite[];
noStyle?: boolean; noStyle?: boolean;
}>(); }>();
</script> </script>

View File

@ -3,6 +3,7 @@
ref="tooltip" ref="tooltip"
:target-element="targetElement" :target-element="targetElement"
:max-width="340" :max-width="340"
:showing="showing"
@closed="emit('closed')" @closed="emit('closed')"
> >
<div class="beeadbfb"> <div class="beeadbfb">
@ -18,12 +19,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from "vue";
import MkTooltip from "./MkTooltip.vue"; import MkTooltip from "./MkTooltip.vue";
import XReactionIcon from "@/components/MkReactionIcon.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue";
import type { entities } from "firefish-js";
defineProps<{ defineProps<{
showing: Ref<boolean>;
reaction: string; reaction: string;
emojis: any[]; // TODO emojis: entities.EmojiLite[];
targetElement: HTMLElement; targetElement: HTMLElement;
}>(); }>();

View File

@ -4,6 +4,7 @@
:target-element="targetElement" :target-element="targetElement"
:max-width="340" :max-width="340"
@closed="emit('closed')" @closed="emit('closed')"
:showing="showing"
> >
<div class="bqxuuuey"> <div class="bqxuuuey">
<div class="reaction"> <div class="reaction">
@ -29,15 +30,18 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from "vue";
import MkTooltip from "./MkTooltip.vue"; import MkTooltip from "./MkTooltip.vue";
import XReactionIcon from "@/components/MkReactionIcon.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue";
import type { entities } from "firefish-js";
defineProps<{ defineProps<{
showing: Ref<boolean>;
reaction: string; reaction: string;
users: any[]; // TODO users: entities.User[]; // TODO
count: number; count: number;
emojis: any[]; // TODO emojis: entities.EmojiLite[]; // TODO
targetElement: HTMLElement; targetElement?: HTMLElement;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

View File

@ -89,7 +89,7 @@ useTooltip(
emojis: props.note.emojis, emojis: props.note.emojis,
users, users,
count: props.count, count: props.count,
targetElement: buttonRef.value, targetElement: buttonRef.value!,
}, },
{}, {},
"closed", "closed",

View File

@ -46,7 +46,7 @@ const buttonRef = ref<HTMLElement>();
const canRenote = computed( const canRenote = computed(
() => () =>
["public", "home"].includes(props.note.visibility) || ["public", "home"].includes(props.note.visibility) ||
props.note.userId === me.id, props.note.userId === me?.id,
); );
useTooltip(buttonRef, async (showing) => { useTooltip(buttonRef, async (showing) => {
@ -77,7 +77,7 @@ const hasRenotedBefore = ref(false);
if (isSignedIn) { if (isSignedIn) {
os.api("notes/renotes", { os.api("notes/renotes", {
noteId: props.note.id, noteId: props.note.id,
userId: me.id, userId: me!.id,
limit: 1, limit: 1,
}).then((res) => { }).then((res) => {
hasRenotedBefore.value = res.length > 0; hasRenotedBefore.value = res.length > 0;
@ -251,6 +251,10 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard }); os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
}; };
defineExpose({
renote,
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -145,9 +145,10 @@ import * as os from "@/os";
import { signIn } from "@/account"; import { signIn } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { entities } from "firefish-js";
const signing = ref(false); const signing = ref(false);
const user = ref(null); const user = ref<entities.UserDetailed | null>(null);
const username = ref(""); const username = ref("");
const password = ref(""); const password = ref("");
const token = ref(""); const token = ref("");
@ -249,7 +250,7 @@ function queryKey() {
function onSubmit() { function onSubmit() {
signing.value = true; signing.value = true;
console.log("submit"); console.log("submit");
if (window.PublicKeyCredential && user.value.securityKeys) { if (window.PublicKeyCredential && user.value?.securityKeys) {
os.api("signin", { os.api("signin", {
username: username.value, username: username.value,
password: password.value, password: password.value,
@ -263,7 +264,7 @@ function onSubmit() {
return queryKey(); return queryKey();
}) })
.catch(loginFailed); .catch(loginFailed);
} else if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { } else if (!totpLogin.value && user.value?.twoFactorEnabled) {
totpLogin.value = true; totpLogin.value = true;
signing.value = false; signing.value = false;
} else { } else {
@ -272,8 +273,7 @@ function onSubmit() {
password: password.value, password: password.value,
"hcaptcha-response": hCaptchaResponse.value, "hcaptcha-response": hCaptchaResponse.value,
"g-recaptcha-response": reCaptchaResponse.value, "g-recaptcha-response": reCaptchaResponse.value,
token: token: user.value?.twoFactorEnabled ? token.value : undefined,
user.value && user.value.twoFactorEnabled ? token.value : undefined,
}) })
.then((res) => { .then((res) => {
emit("login", res); emit("login", res);

View File

@ -305,12 +305,12 @@ const host = toUnicode(config.host);
const hcaptcha = ref(); const hcaptcha = ref();
const recaptcha = ref(); const recaptcha = ref();
const username: string = ref(""); const username = ref<string>("");
const password: string = ref(""); const password = ref<string>("");
const retypedPassword: string = ref(""); const retypedPassword = ref<string>("");
const invitationCode: string = ref(""); const invitationCode = ref<string>("");
const email = ref(""); const email = ref("");
const usernameState: const usernameState = ref<
| null | null
| "wait" | "wait"
| "ok" | "ok"
@ -318,9 +318,10 @@ const usernameState:
| "error" | "error"
| "invalid-format" | "invalid-format"
| "min-range" | "min-range"
| "max-range" = ref(null); | "max-range"
const invitationState: null | "entered" = ref(null); >(null);
const emailState: const invitationState = ref<null | "entered">(null);
const emailState = ref<
| null | null
| "wait" | "wait"
| "ok" | "ok"
@ -330,11 +331,12 @@ const emailState:
| "unavailable:mx" | "unavailable:mx"
| "unavailable:smtp" | "unavailable:smtp"
| "unavailable" | "unavailable"
| "error" = ref(null); | "error"
const passwordStrength: "" | "low" | "medium" | "high" = ref(""); >(null);
const passwordRetypeState: null | "match" | "not-match" = ref(null); const passwordStrength = ref<"" | "low" | "medium" | "high">("");
const submitting: boolean = ref(false); const passwordRetypeState = ref<null | "match" | "not-match">(null);
const ToSAgreement: boolean = ref(false); const submitting = ref(false);
const ToSAgreement = ref(false);
const hCaptchaResponse = ref(null); const hCaptchaResponse = ref(null);
const reCaptchaResponse = ref(null); const reCaptchaResponse = ref(null);

View File

@ -31,7 +31,6 @@
:text="note.cw" :text="note.cw"
:author="note.user" :author="note.user"
:lang="note.lang" :lang="note.lang"
:i="me"
:custom-emojis="note.emojis" :custom-emojis="note.emojis"
/> />
</p> </p>
@ -63,8 +62,8 @@
<div <div
class="body" class="body"
v-bind="{ v-bind="{
'aria-hidden': note.cw && !showContent ? 'true' : null, 'aria-hidden': note.cw && !showContent ? 'true' : undefined,
tabindex: !showContent ? '-1' : null, tabindex: !showContent ? '-1' : undefined,
}" }"
> >
<span v-if="note.deletedAt" style="opacity: 0.5" <span v-if="note.deletedAt" style="opacity: 0.5"
@ -103,7 +102,6 @@
v-if="note.text" v-if="note.text"
:text="note.text" :text="note.text"
:author="note.user" :author="note.user"
:i="me"
:lang="note.lang" :lang="note.lang"
:custom-emojis="note.emojis" :custom-emojis="note.emojis"
/> />
@ -256,7 +254,7 @@ async function toggleMfm() {
} }
function focusFooter(ev) { function focusFooter(ev) {
if (ev.key == "Tab" && !ev.getModifierState("Shift")) { if (ev.key === "Tab" && !ev.getModifierState("Shift")) {
emit("focusfooter"); emit("focusfooter");
} }
} }

View File

@ -76,6 +76,7 @@ onMounted(() => {
src: "/client-assets/tagcanvas.min.js", src: "/client-assets/tagcanvas.min.js",
}), }),
) )
// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
.addEventListener("load", () => (available.value = true)); .addEventListener("load", () => (available.value = true));
} }
}); });

View File

@ -5,13 +5,13 @@
@after-leave="emit('closed')" @after-leave="emit('closed')"
> >
<div <div
v-show="showing" v-show="unref(showing)"
ref="el" ref="el"
class="buebdbiu _acrylic _shadow" class="buebdbiu _acrylic _shadow"
:style="{ zIndex, maxWidth: maxWidth + 'px' }" :style="{ zIndex, maxWidth: maxWidth + 'px' }"
> >
<slot> <slot>
<Mfm v-if="asMfm" :text="text" /> <Mfm v-if="asMfm" :text="text!" />
<span v-else>{{ text }}</span> <span v-else>{{ text }}</span>
</slot> </slot>
</div> </div>
@ -19,15 +19,22 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, ref } from "vue"; import {
type MaybeRef,
nextTick,
onMounted,
onUnmounted,
ref,
unref,
} from "vue";
import * as os from "@/os"; import * as os from "@/os";
import { calcPopupPosition } from "@/scripts/popup-position"; import { calcPopupPosition } from "@/scripts/popup-position";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
showing: boolean; showing: MaybeRef<boolean>;
targetElement?: HTMLElement; targetElement?: HTMLElement | null;
x?: number; x?: number;
y?: number; y?: number;
text?: string; text?: string;
@ -40,6 +47,7 @@ const props = withDefaults(
maxWidth: 250, maxWidth: 250,
direction: "top", direction: "top",
innerMargin: 0, innerMargin: 0,
targetElement: null,
}, },
); );
@ -51,7 +59,7 @@ const el = ref<HTMLElement>();
const zIndex = os.claimZIndex("high"); const zIndex = os.claimZIndex("high");
function setPosition() { function setPosition() {
const data = calcPopupPosition(el.value, { const data = calcPopupPosition(el.value!, {
anchorElement: props.targetElement, anchorElement: props.targetElement,
direction: props.direction, direction: props.direction,
align: "center", align: "center",
@ -60,12 +68,12 @@ function setPosition() {
y: props.y, y: props.y,
}); });
el.value.style.transformOrigin = data.transformOrigin; el.value!.style.transformOrigin = data.transformOrigin;
el.value.style.left = data.left + "px"; el.value!.style.left = `${data.left}px`;
el.value.style.top = data.top + "px"; el.value!.style.top = `${data.top}px`;
} }
let loopHandler; let loopHandler: number;
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {

View File

@ -181,10 +181,10 @@ function adjustTweetHeight(message: any) {
if (height) tweetHeight.value = height; if (height) tweetHeight.value = height;
} }
(window as any).addEventListener("message", adjustTweetHeight); window.addEventListener("message", adjustTweetHeight);
onUnmounted(() => { onUnmounted(() => {
(window as any).removeEventListener("message", adjustTweetHeight); window.removeEventListener("message", adjustTweetHeight);
}); });
</script> </script>

View File

@ -11,10 +11,10 @@
</div> </div>
</template> </template>
<template #default="{ items: users }"> <template #default="{ items }: { items: entities.UserDetailed[] }">
<div class="efvhhmdq"> <div class="efvhhmdq">
<MkUserInfo <MkUserInfo
v-for="user in users" v-for="user in items"
:key="user.id" :key="user.id"
class="user" class="user"
:user="user" :user="user"
@ -27,16 +27,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import MkUserInfo from "@/components/MkUserInfo.vue"; import MkUserInfo from "@/components/MkUserInfo.vue";
import type { Paging } from "@/components/MkPagination.vue"; import type {
MkPaginationType,
PagingKeyOf,
PagingOf,
} from "@/components/MkPagination.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import type { entities } from "firefish-js";
defineProps<{ defineProps<{
pagination: Paging; pagination: PagingOf<entities.UserDetailed>;
noGap?: boolean; noGap?: boolean;
}>(); }>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = ref<MkPaginationType<PagingKeyOf<entities.User>>>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -98,16 +98,16 @@ import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "ok", selected: entities.UserDetailed): void; ok: [selected: entities.UserDetailed];
(ev: "cancel"): void; cancel: [];
(ev: "closed"): void; closed: [];
}>(); }>();
const username = ref(""); const username = ref("");
const host = ref(""); const host = ref("");
const users: entities.UserDetailed[] = ref([]); const users = ref<entities.UserDetailed[]>([]);
const recentUsers: entities.UserDetailed[] = ref([]); const recentUsers = ref<entities.UserDetailed[]>([]);
const selected: entities.UserDetailed | null = ref(null); const selected = ref<entities.UserDetailed | null>(null);
const dialogEl = ref(); const dialogEl = ref();
const search = () => { const search = () => {
@ -132,7 +132,7 @@ const ok = () => {
// 使 // 使
let recents = defaultStore.state.recentlyUsedUsers; let recents = defaultStore.state.recentlyUsedUsers;
recents = recents.filter((x) => x !== selected.value.id); recents = recents.filter((x) => x !== selected.value!.id);
recents.unshift(selected.value.id); recents.unshift(selected.value.id);
defaultStore.set("recentlyUsedUsers", recents.splice(0, 16)); defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
}; };

View File

@ -94,9 +94,9 @@ import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "ok", selected: entities.UserDetailed): void; ok: [selected: entities.UserDetailed];
(ev: "cancel"): void; cancel: [];
(ev: "closed"): void; closed: [];
}>(); }>();
const username = ref(""); const username = ref("");
@ -114,7 +114,7 @@ const search = () => {
query: username.value, query: username.value,
origin: "local", origin: "local",
limit: 10, limit: 10,
detail: false, detail: true,
}).then((_users) => { }).then((_users) => {
users.value = _users; users.value = _users;
}); });
@ -127,7 +127,7 @@ const ok = () => {
// 使 // 使
let recents = defaultStore.state.recentlyUsedUsers; let recents = defaultStore.state.recentlyUsedUsers;
recents = recents.filter((x) => x !== selected.value.id); recents = recents.filter((x) => x !== selected.value!.id);
recents.unshift(selected.value.id); recents.unshift(selected.value.id);
defaultStore.set("recentlyUsedUsers", recents.splice(0, 16)); defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
}; };

View File

@ -4,6 +4,7 @@
:target-element="targetElement" :target-element="targetElement"
:max-width="250" :max-width="250"
@closed="emit('closed')" @closed="emit('closed')"
:showing="showing"
> >
<div class="beaffaef"> <div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user"> <div v-for="u in users" :key="u.id" class="user">
@ -18,12 +19,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from "vue";
import MkTooltip from "./MkTooltip.vue"; import MkTooltip from "./MkTooltip.vue";
import type { entities } from "firefish-js";
defineProps<{ defineProps<{
users: any[]; // TODO showing: Ref<boolean>;
users: entities.User[];
count: number; count: number;
targetElement: HTMLElement; targetElement?: HTMLElement;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

View File

@ -3,7 +3,7 @@
ref="modal" ref="modal"
:z-priority="'high'" :z-priority="'high'"
:src="src" :src="src"
@click="modal.close()" @click="modal!.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<div class="_popup" :class="$style.root"> <div class="_popup" :class="$style.root">
@ -153,15 +153,15 @@ const props = withDefaults(
defineProps<{ defineProps<{
currentVisibility: NoteVisibility; currentVisibility: NoteVisibility;
currentLocalOnly: boolean; currentLocalOnly: boolean;
src?: HTMLElement; src?: HTMLElement | null;
}>(), }>(),
{}, {},
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "changeVisibility", v: NoteVisibility): void; changeVisibility: [v: NoteVisibility];
(ev: "changeLocalOnly", v: boolean): void; changeLocalOnly: [v: boolean];
(ev: "closed"): void; closed: [];
}>(); }>();
const v = ref(props.currentVisibility); const v = ref(props.currentVisibility);
@ -175,7 +175,7 @@ function choose(visibility: NoteVisibility): void {
v.value = visibility; v.value = visibility;
emit("changeVisibility", visibility); emit("changeVisibility", visibility);
nextTick(() => { nextTick(() => {
modal.value.close(); modal.value!.close();
}); });
} }
</script> </script>

View File

@ -13,7 +13,7 @@
]" ]"
> >
<i <i
v-if="success" v-if="unref(success)"
:class="[$style.icon, $style.success, iconify('ph-check')]" :class="[$style.icon, $style.success, iconify('ph-check')]"
></i> ></i>
<MkLoading <MkLoading
@ -29,15 +29,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef, watch } from "vue"; import { MaybeRef, shallowRef, watch, unref } from "vue";
import MkModal from "@/components/MkModal.vue"; import MkModal from "@/components/MkModal.vue";
import iconify from "@/scripts/icon"; import iconify from "@/scripts/icon";
const modal = shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const props = defineProps<{ const props = defineProps<{
success: boolean; success: MaybeRef<boolean>;
showing: boolean; showing: MaybeRef<boolean>;
text?: string; text?: string;
}>(); }>();

View File

@ -85,7 +85,7 @@ import icon from "@/scripts/icon";
interface Widget { interface Widget {
name: string; name: string;
id: string; id: string;
data: Record<string, any>; data: Record<string, unknown>;
} }
const props = defineProps<{ const props = defineProps<{
@ -137,12 +137,12 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
return isLink(el.parentElement); return isLink(el.parentElement);
} }
}; };
if (isLink(ev.target)) return; if (isLink(ev.target as HTMLElement)) return;
if ( if (
["INPUT", "TEXTAREA", "IMG", "VIDEO", "CANVAS"].includes( ["INPUT", "TEXTAREA", "IMG", "VIDEO", "CANVAS"].includes(
ev.target.tagName, (ev.target as HTMLElement).tagName,
) || ) ||
ev.target.attributes.contenteditable (ev.target as HTMLElement).getAttribute("contentEditable")
) )
return; return;
if (window.getSelection()?.toString() !== "") return; if (window.getSelection()?.toString() !== "") return;

View File

@ -271,7 +271,7 @@ function onHeaderMousedown(evt: MouseEvent) {
? evt.touches[0].clientY ? evt.touches[0].clientY
: evt.clientY; : evt.clientY;
const moveBaseX = beforeMaximized const moveBaseX = beforeMaximized
? parseInt(unMaximizedWidth, 10) / 2 ? Number.parseInt(unMaximizedWidth, 10) / 2
: clickX - position.left; // TODO: parseInt : clickX - position.left; // TODO: parseInt
const moveBaseY = beforeMaximized ? 20 : clickY - position.top; const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
const browserWidth = window.innerWidth; const browserWidth = window.innerWidth;
@ -321,8 +321,8 @@ function onTopHandleMousedown(evt) {
const main = rootEl.value; const main = rootEl.value;
const base = evt.clientY; const base = evt.clientY;
const height = parseInt(getComputedStyle(main, "").height, 10); const height = Number.parseInt(getComputedStyle(main, "").height, 10);
const top = parseInt(getComputedStyle(main, "").top, 10); const top = Number.parseInt(getComputedStyle(main, "").top, 10);
// //
dragListen((me) => { dragListen((me) => {
@ -349,8 +349,8 @@ function onRightHandleMousedown(evt) {
const main = rootEl.value; const main = rootEl.value;
const base = evt.clientX; const base = evt.clientX;
const width = parseInt(getComputedStyle(main, "").width, 10); const width = Number.parseInt(getComputedStyle(main, "").width, 10);
const left = parseInt(getComputedStyle(main, "").left, 10); const left = Number.parseInt(getComputedStyle(main, "").left, 10);
const browserWidth = window.innerWidth; const browserWidth = window.innerWidth;
// //
@ -375,8 +375,8 @@ function onBottomHandleMousedown(evt) {
const main = rootEl.value; const main = rootEl.value;
const base = evt.clientY; const base = evt.clientY;
const height = parseInt(getComputedStyle(main, "").height, 10); const height = Number.parseInt(getComputedStyle(main, "").height, 10);
const top = parseInt(getComputedStyle(main, "").top, 10); const top = Number.parseInt(getComputedStyle(main, "").top, 10);
const browserHeight = window.innerHeight; const browserHeight = window.innerHeight;
// //
@ -401,8 +401,8 @@ function onLeftHandleMousedown(evt) {
const main = rootEl.value; const main = rootEl.value;
const base = evt.clientX; const base = evt.clientX;
const width = parseInt(getComputedStyle(main, "").width, 10); const width = Number.parseInt(getComputedStyle(main, "").width, 10);
const left = parseInt(getComputedStyle(main, "").left, 10); const left = Number.parseInt(getComputedStyle(main, "").left, 10);
// //
dragListen((me) => { dragListen((me) => {

Some files were not shown because too many files have changed in this diff Show More