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",
"organizeImports": {
"enabled": true
"enabled": false
},
"linter": {
"enabled": true,
@ -21,7 +21,8 @@
"useImportType": "warn",
"useShorthandFunctionType": "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 { packedNoteEdit } from "@/models/schema/note-edit.js";
import { packedNoteFileSchema } from "@/models/schema/note-file.js";
import { packedAbuseUserReportSchema } from "@/models/schema/abuse-user-report.js";
export const refs = {
AbuseUserReport: packedAbuseUserReportSchema,
UserLite: packedUserLiteSchema,
UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
MeDetailedOnly: packedMeDetailedOnlySchema,

View File

@ -2,6 +2,7 @@ import { db } from "@/db/postgre.js";
import { Users } from "../index.js";
import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
import { awaitAll } from "@/prelude/await-all.js";
import type { Packed } from "@/misc/schema.js";
export const AbuseUserReportRepository = db
.getRepository(AbuseUserReport)
@ -10,7 +11,7 @@ export const AbuseUserReportRepository = db
const report =
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({
const packed: Packed<"AbuseUserReport"> = await awaitAll({
id: report.id,
createdAt: report.createdAt.toISOString(),
comment: report.comment,
@ -31,9 +32,10 @@ export const AbuseUserReportRepository = db
: null,
forwarded: report.forwarded,
});
return packed;
},
packMany(reports: any[]) {
packMany(reports: (AbuseUserReport["id"] | AbuseUserReport)[]) {
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,
description: channel.description,
userId: channel.userId,
bannerId: channel.bannerId,
bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
usersCount: channel.usersCount,
notesCount: channel.notesCount,

View File

@ -19,7 +19,9 @@ export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
createdAt: post.createdAt.toISOString(),
updatedAt: post.updatedAt.toISOString(),
userId: post.userId,
user: Users.pack(post.user || post.userId, me),
user: Users.pack(post.user || post.userId, me, {
detail: true,
}),
title: post.title,
description: post.description,
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,
optional: false,
},
bannerId: {
type: "string",
optional: false,
nullable: true,
format: "id",
example: "xxxxxxxxxx",
},
notesCount: {
type: "number",
nullable: false,
@ -57,5 +64,10 @@ export const packedChannelSchema = {
optional: false,
format: "id",
},
hasUnreadNote: {
type: "boolean",
optional: true,
nullable: false,
},
},
} as const;

View File

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

View File

@ -16,68 +16,7 @@ export const meta = {
type: "object",
optional: false,
nullable: false,
properties: {
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",
},
},
ref: "AbuseUserReport",
},
},
} as const;

View File

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

View File

@ -1,3 +1,4 @@
// biome-ignore lint/suspicious/noExplicitAny:
type FIXME = any;
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 * as os from "@/os";
import { i18n } from "@/i18n";
import type { entities } from "firefish-js";
const props = defineProps<{
report: any;
report: entities.AbuseUserReport;
}>();
const emit = defineEmits<{
(ev: "resolved", reportId: string): void;
resolved: [reportId: string];
}>();
const forward = ref(props.report.forwarded);

View File

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

View File

@ -35,9 +35,10 @@ import MkSparkle from "@/components/MkSparkle.vue";
import MkButton from "@/components/MkButton.vue";
import { i18n } from "@/i18n";
import * as os from "@/os";
import type { entities } from "firefish-js";
const props = defineProps<{
announcement: Announcement;
announcement: entities.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 gotIt = () => {
modal.value.close();
modal.value!.close();
os.api("i/read-announcement", { announcementId: id });
};
</script>

View File

@ -62,7 +62,7 @@
<span v-else class="emoji">{{ emoji.emoji }}</span>
<span
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 v-if="emoji.aliasOf" class="alias"
>({{ emoji.aliasOf }})</span
@ -107,7 +107,7 @@ interface EmojiDef {
emoji: string;
name: string;
aliasOf?: string;
url?: string;
url: string;
isCustomEmoji?: boolean;
}

View File

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

View File

@ -16,7 +16,7 @@
v-else
class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full, mini }"
:to="to"
:to="to!"
@mousedown="onMousedown"
>
<div ref="ripples" class="ripples"></div>
@ -36,6 +36,7 @@ const props = defineProps<{
gradate?: boolean;
rounded?: boolean;
inline?: boolean;
// FIXME: if `link`, `to` is necessary
link?: boolean;
to?: string;
autofocus?: boolean;
@ -47,7 +48,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(ev: "click", payload: MouseEvent): void;
click: [payload: MouseEvent];
}>();
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);
}
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 dist1 = distance({ x: 0, 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 ripple = document.createElement("div");
ripple.style.top = (evt.clientY - rect.top - 1).toString() + "px";
ripple.style.left = (evt.clientX - rect.left - 1).toString() + "px";
ripple.style.top = `${(evt.clientY - rect.top - 1).toString()}px`;
ripple.style.left = `${(evt.clientX - rect.left - 1).toString()}px`;
ripples.value!.appendChild(ripple);
@ -97,7 +106,7 @@ function onMousedown(evt: MouseEvent): void {
vibrate(10);
window.setTimeout(() => {
ripple.style.transform = "scale(" + scale / 2 + ")";
ripple.style.transform = `scale(${scale / 2})`;
}, 1);
window.setTimeout(() => {
ripple.style.transition = "all 1s ease";

View File

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

View File

@ -27,10 +27,11 @@ import { ref } from "vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
import type { entities } from "firefish-js";
const props = withDefaults(
defineProps<{
channel: Record<string, any>;
channel: entities.Channel;
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);
async function onClick() {

View File

@ -11,7 +11,7 @@
</div>
</template>
<template #default="{ items }">
<template #default="{ items }: { items: entities.Channel[] }">
<MkChannelPreview
v-for="item in items"
:key="item.id"
@ -29,14 +29,15 @@ import type { PagingOf } from "@/components/MkPagination.vue";
import MkPagination from "@/components/MkPagination.vue";
import { i18n } from "@/i18n";
const props = withDefaults(
withDefaults(
defineProps<{
pagination: PagingOf<entities.Channel>;
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>

View File

@ -54,9 +54,10 @@
import { computed } from "vue";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
import type { entities } from "firefish-js";
const props = defineProps<{
channel: Record<string, any>;
channel: entities.Channel;
}>();
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 alpha = (hex, a) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
const r = Number.parseInt(result[1], 16);
const g = Number.parseInt(result[2], 16);
const b = Number.parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -68,40 +68,48 @@ let cropper: Cropper | null = null;
const loading = ref(true);
const ok = async () => {
const promise = new Promise<entities.DriveFile>(async (res) => {
async function UploadCroppedImg(): Promise<entities.DriveFile> {
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", {
method: "POST",
body: formData,
headers: {
authorization: `Bearer ${me.token}`,
},
})
.then((response) => response.json())
.then((f) => {
res(f);
});
const blob = await new Promise<Blob | null>((resolve) =>
croppedCanvas!.toBlob((blob) => resolve(blob)),
);
// 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) {
throw "Cropping image failed.";
}
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);
const f = await promise;
emit("ok", f);
dialogEl.value.close();
dialogEl.value!.close();
};
const cancel = () => {
emit("cancel");
dialogEl.value.close();
dialogEl.value!.close();
};
const onImageLoad = () => {
@ -114,7 +122,7 @@ const onImageLoad = () => {
};
onMounted(() => {
cropper = new Cropper(imgEl.value, {});
cropper = new Cropper(imgEl.value!, {});
const computedStyle = getComputedStyle(document.documentElement);
@ -127,13 +135,13 @@ onMounted(() => {
selection.outlined = true;
window.setTimeout(() => {
cropper.getCropperImage()!.$center("contain");
cropper!.getCropperImage()!.$center("contain");
selection.$center();
}, 100);
// 調
window.setTimeout(() => {
cropper.getCropperImage()!.$center("contain");
cropper!.getCropperImage()!.$center("contain");
selection.$center();
}, 500);
});

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@
<MkButton
v-if="instance.donationLink"
gradate
@click="openExternal(instance.donationLink)"
@click="openExternal(instance.donationLink!)"
>{{
i18n.t("_aboutFirefish.donateHost", {
host: hostname,
@ -73,7 +73,8 @@ const emit = defineEmits<{
(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");
@ -97,7 +98,7 @@ function neverShow() {
close();
}
function openExternal(link) {
function openExternal(link: string) {
window.open(link, "_blank");
}
</script>

View File

@ -47,6 +47,7 @@ import * as os from "@/os";
import { i18n } from "@/i18n";
import { me } from "@/me";
import icon from "@/scripts/icon";
import type { MenuItem } from "@/types/menu";
const props = withDefaults(
defineProps<{
@ -72,7 +73,7 @@ const title = computed(
() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`,
);
function getMenu() {
function getMenu(): MenuItem[] {
return [
{
text: i18n.ts.rename,
@ -180,12 +181,15 @@ function describe() {
image: props.file,
},
{
done: (result) => {
done: (result: {
canceled: boolean;
result?: string | null;
}) => {
if (!result || result.canceled) return;
const comment = result.result;
os.api("drive/files/update", {
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);
}
function onDragover(ev: DragEvent): any {
function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
//
@ -285,7 +285,7 @@ function onDragleave() {
draghover.value = false;
}
function onDrop(ev: DragEvent): any {
function onDrop(ev: DragEvent) {
draghover.value = false;
if (!ev.dataTransfer) return;
@ -493,14 +493,12 @@ function move(target?: entities.DriveFolder) {
if (!target) {
goRoot();
return;
} else if (typeof target === "object") {
target = target.id;
}
fetching.value = true;
os.api("drive/folders/show", {
folderId: target,
folderId: target.id,
}).then((folderToMove) => {
folder.value = folderToMove;
hierarchyFolders.value = [];

View File

@ -14,7 +14,7 @@
class="_button"
@click.stop="
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 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(
defineProps<{
showPinned?: boolean;
@ -193,7 +198,7 @@ const props = withDefaults(
);
const emit = defineEmits<{
(ev: "chosen", v: string, ev: MouseEvent): void;
chosen: [v: string, ev?: MouseEvent];
}>();
const search = ref<HTMLInputElement>();
@ -410,13 +415,17 @@ function reset() {
q.value = "";
}
function getKey(
emoji: string | entities.CustomEmoji | UnicodeEmojiDef,
): string {
return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`;
function getKey(emoji: EmojiDef): string {
if (typeof emoji === "string") {
return emoji;
}
if ("emoji" in emoji) {
return emoji.emoji;
}
return `:${emoji.name}:`;
}
function chosen(emoji: any, ev?: MouseEvent) {
function chosen(emoji: EmojiDef, ev?: MouseEvent) {
const el =
ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
if (el) {
@ -432,22 +441,33 @@ function chosen(emoji: any, ev?: MouseEvent) {
// 使
if (!pinned.value.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== key);
recents = recents.filter((emoji) => emoji !== key);
recents.unshift(key);
defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32));
}
}
function paste(event: ClipboardEvent) {
const paste = (event.clipboardData || window.clipboardData).getData("text");
if (done(paste)) {
async function paste(event: ClipboardEvent) {
let pasteStr: string | null = null;
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();
}
}
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 || typeof query !== "string") return;
if (query == null || typeof query !== "string") return false;
const q2 = query.replaceAll(":", "");
const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2);
@ -470,6 +490,7 @@ function done(query?: any): boolean | void {
chosen(searchResultUnicode.value[0]);
return true;
}
return false;
}
onMounted(() => {

View File

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

View File

@ -31,72 +31,76 @@
</section>
</template>
<script lang="ts">
import { defineComponent } from "vue";
<script lang="ts" setup>
import { ref, watch } from "vue";
import { getUniqueId } from "@/os";
import { defaultStore } from "@/store";
// import icon from "@/scripts/icon";
const localStoragePrefix = "ui:folder:";
export default defineComponent({
props: {
expanded: {
type: Boolean,
required: false,
default: true,
},
persistKey: {
type: String,
required: false,
default: null,
},
const props = withDefaults(
defineProps<{
expanded?: boolean;
persistKey?: string | null;
}>(),
{
expanded: true,
persistKey: 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 elementHeight = el.getBoundingClientRect().height;
el.style.height = 0;
el.offsetHeight; // reflow
el.style.height = elementHeight + "px";
},
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;
},
},
const bodyId = ref(getUniqueId());
const showBody = ref(
props.persistKey &&
localStorage.getItem(localStoragePrefix + props.persistKey)
? localStorage.getItem(localStoragePrefix + props.persistKey) === "t"
: props.expanded,
);
const animation = defaultStore.state.animation;
watch(showBody, () => {
if (props.persistKey) {
localStorage.setItem(
localStoragePrefix + props.persistKey,
showBody.value ? "t" : "f",
);
}
});
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>

View File

@ -8,7 +8,7 @@
<i :class="icon('ph-dots-three-outline')"></i>
</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}`"
class="kpoogebi _button follow-button"
:class="{

View File

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

View File

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

View File

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

View File

@ -2,10 +2,24 @@
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel">
<div class="thumbnail">
<ImgWithBlurhash
v-if="post.files && post.files.length > 0"
class="img"
:src="post.files[0].thumbnailUrl"
: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>
<article>
<header>
@ -20,9 +34,11 @@
<script lang="ts" setup>
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
import { i18n } from "@/i18n";
import type { entities } from "firefish-js";
const props = defineProps<{
post: any;
defineProps<{
post: entities.GalleryPost;
}>();
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@
media.type.startsWith('video') ||
media.type.startsWith('image')
"
:key="media.id"
:key="`m-${media.id}`"
:class="{ image: media.type.startsWith('image') }"
:data-id="media.id"
:media="media"
@ -30,7 +30,7 @@
/>
<XModPlayer
v-else-if="isModule(media)"
:key="media.id"
:key="`p-${media.id}`"
:module="media"
/>
</template>
@ -48,7 +48,7 @@ import "photoswipe/style.css";
import XBanner from "@/components/MkMediaBanner.vue";
import XMedia from "@/components/MkMedia.vue";
import XModPlayer from "@/components/MkModPlayer.vue";
import * as os from "@/os";
// import * as os from "@/os";
import {
FILE_EXT_TRACKER_MODULES,
FILE_TYPE_BROWSERSAFE,
@ -61,8 +61,8 @@ const props = defineProps<{
inDm?: boolean;
}>();
const gallery = ref(null);
const pswpZIndex = os.claimZIndex("middle");
const gallery = ref<HTMLElement | null>(null);
// const pswpZIndex = os.claimZIndex("middle");
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
@ -79,7 +79,7 @@ onMounted(() => {
src: media.url,
w: media.properties.width,
h: media.properties.height,
alt: media.comment,
alt: media.comment || undefined,
};
if (
media.properties.orientation != null &&
@ -89,7 +89,7 @@ onMounted(() => {
}
return item;
}),
gallery: gallery.value,
gallery: gallery.value || undefined,
children: ".image",
thumbSelector: ".image img",
loop: false,
@ -119,9 +119,13 @@ onMounted(() => {
// element is children
const { element } = itemData;
if (element == null) return;
const id = element.dataset.id;
const file = props.mediaList.find((media) => media.id === id);
if (file == null) return;
itemData.src = file.url;
itemData.w = Number(file.properties.width);
itemData.h = Number(file.properties.height);
@ -132,12 +136,12 @@ onMounted(() => {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
itemData.msrc = file.thumbnailUrl;
itemData.alt = file.comment;
itemData.alt = file.comment || undefined;
itemData.thumbCropped = true;
});
lightbox.on("uiRegister", () => {
lightbox.pswp.ui.registerElement({
lightbox.pswp?.ui?.registerElement({
name: "altText",
className: "pwsp__alt-text-container",
appendTo: "wrapper",
@ -146,7 +150,7 @@ onMounted(() => {
textBox.className = "pwsp__alt-text";
el.appendChild(textBox);
const preventProp = function (ev: Event): void {
const preventProp = (ev: Event): void => {
ev.stopPropagation();
};
@ -158,7 +162,7 @@ onMounted(() => {
el.onpointermove = preventProp;
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);
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.
lightbox.pswp.element.focus();
lightbox.pswp?.element?.focus();
});
lightbox.on("close", () => {
removeEventListener("popstate", close);
@ -180,7 +184,7 @@ onMounted(() => {
function close() {
removeEventListener("popstate", close);
history.forward();
lightbox.pswp.close();
lightbox.pswp?.close();
}
});
@ -198,7 +202,7 @@ const isModule = (file: entities.DriveFile): boolean => {
return (
FILE_TYPE_TRACKER_MODULES.includes(file.type) ||
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"
target="_blank"
rel="noopener"
:style="{ background: bgCss }"
@click.stop
>
<span class="main">
@ -54,7 +53,7 @@ const url = `/${canonical}`;
const isMe =
isSignedIn &&
`@${props.username}@${toUnicode(props.host)}`.toLowerCase() ===
`@${me.username}@${toUnicode(localHost)}`.toLowerCase();
`@${me!.username}@${toUnicode(localHost)}`.toLowerCase();
</script>
<style lang="scss" scoped>

View File

@ -37,8 +37,8 @@ function setPosition() {
const rect = props.targetElement.getBoundingClientRect();
const left = props.targetElement.offsetWidth;
const top = rect.top - rootRect.top - 8;
el.value.style.left = left + "px";
el.value.style.top = top + "px";
el.value!.style.left = `${left}px`;
el.value!.style.top = `${top}px`;
}
function onChildClosed(actioned?: boolean) {
@ -58,7 +58,7 @@ onMounted(() => {
defineExpose({
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
<template>
<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="header">
<MkUserName :user="me" />
<MkUserName :user="me!" />
</div>
<div class="body">
<div class="content">

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
:with-ok-button="true"
:ok-button-disabled="false"
@ok="ok()"
@close="dialog.close()"
@close="dialog!.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.notificationSetting }}</template>
@ -68,7 +68,7 @@ const includingTypes = computed(() => props.includingTypes || []);
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(
(includingTypes.value === null || includingTypes.value.length === 0) &&
props.showGlobalToggle,
@ -89,7 +89,7 @@ function ok() {
});
}
dialog.value.close();
dialog.value!.close();
}
function disableAll() {

View File

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

View File

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

View File

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

View File

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

View File

@ -67,7 +67,7 @@
</template>
<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 type { Endpoints, TypeUtils } from "firefish-js";
import * as os from "@/os";
@ -81,8 +81,30 @@ import MkButton from "@/components/MkButton.vue";
import { i18n } from "@/i18n";
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
export type PagingKey = TypeUtils.EndpointsOf<any[]>;
export type PagingKey = PagingKeyOf<any>;
export interface Paging<E extends PagingKey = PagingKey> {
endpoint: E;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -85,7 +85,7 @@ import icon from "@/scripts/icon";
interface Widget {
name: string;
id: string;
data: Record<string, any>;
data: Record<string, unknown>;
}
const props = defineProps<{
@ -137,12 +137,12 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (isLink(ev.target as HTMLElement)) return;
if (
["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;
if (window.getSelection()?.toString() !== "") return;

View File

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

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