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:
commit
bce88ec199
156 changed files with 2200 additions and 1458 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
69
packages/backend/src/models/schema/abuse-user-report.ts
Normal file
69
packages/backend/src/models/schema/abuse-user-report.ts
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
1
packages/client/@types/global.d.ts
vendored
1
packages/client/@types/global.d.ts
vendored
|
@ -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
6
packages/client/@types/window.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
declare global {
|
||||
interface Window {
|
||||
__misskey_input_ref__?: HTMLInputElement | null;
|
||||
}
|
||||
}
|
||||
export type {};
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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})`;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
() => {},
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -48,7 +48,7 @@ const toggle = () => {
|
|||
};
|
||||
|
||||
function focus() {
|
||||
el.value.focus();
|
||||
el.value?.focus();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -26,7 +26,7 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
let intervalId;
|
||||
let intervalId: number;
|
||||
const hh = ref("");
|
||||
const mm = ref("");
|
||||
const ss = ref("");
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
class="_button"
|
||||
@click.stop="
|
||||
applyUnicodeSkinTone(
|
||||
props.skinTones.indexOf(skinTone) + 1,
|
||||
props.skinTones!.indexOf(skinTone) + 1,
|
||||
)
|
||||
"
|
||||
>
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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="{
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -19,11 +19,11 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
compiledFormula(): any {
|
||||
compiledFormula() {
|
||||
return katex.renderToString(this.formula, {
|
||||
throwOnError: false,
|
||||
displayMode: this.block,
|
||||
} as any);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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") ??
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ useTooltip(el, (showing) => {
|
|||
os.popup(
|
||||
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
|
||||
{
|
||||
showing,
|
||||
showing: showing.value,
|
||||
url: props.url,
|
||||
source: el.value,
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
>
|
||||
<video
|
||||
:poster="media.thumbnailUrl"
|
||||
:aria-label="media.comment"
|
||||
:aria-label="media.comment || undefined"
|
||||
preload="none"
|
||||
controls
|
||||
playsinline
|
||||
|
|
|
@ -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;
|
||||
}>(),
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ defineProps<{
|
|||
align?: "center" | string;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
src?: any;
|
||||
src?: HTMLElement | null;
|
||||
noReturnFocus?;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -89,7 +89,7 @@ useTooltip(
|
|||
emojis: props.note.emojis,
|
||||
users,
|
||||
count: props.count,
|
||||
targetElement: buttonRef.value,
|
||||
targetElement: buttonRef.value!,
|
||||
},
|
||||
{},
|
||||
"closed",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ onMounted(() => {
|
|||
src: "/client-assets/tagcanvas.min.js",
|
||||
}),
|
||||
)
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
|
||||
.addEventListener("load", () => (available.value = true));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue