Merge branch 'develop' into feat/schedule-create
This commit is contained in:
commit
452728b352
85 changed files with 840 additions and 988 deletions
|
@ -98,6 +98,7 @@ test:build:backend_ts_only:
|
|||
changes:
|
||||
paths:
|
||||
- packages/backend/**/*
|
||||
- packages/firefish-js/**/*
|
||||
- packages/megalodon/**/*
|
||||
when: always
|
||||
before_script:
|
||||
|
@ -115,7 +116,7 @@ test:build:backend_ts_only:
|
|||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm --filter 'backend' --filter 'megalodon' run build:debug
|
||||
- pnpm --filter 'backend' --filter 'firefish-js' --filter 'megalodon' run build:debug
|
||||
- pnpm run migrate
|
||||
|
||||
test:build:client_only:
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.0.0",
|
||||
"firefish-js": "workspace:*",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.2.1",
|
||||
|
|
|
@ -1,234 +1,7 @@
|
|||
import {
|
||||
packedUserLiteSchema,
|
||||
packedUserDetailedNotMeOnlySchema,
|
||||
packedMeDetailedOnlySchema,
|
||||
packedUserDetailedNotMeSchema,
|
||||
packedMeDetailedSchema,
|
||||
packedUserDetailedSchema,
|
||||
packedUserSchema,
|
||||
} from "@/models/schema/user.js";
|
||||
import { packedNoteSchema } from "@/models/schema/note.js";
|
||||
import { packedUserListSchema } from "@/models/schema/user-list.js";
|
||||
import { packedAppSchema } from "@/models/schema/app.js";
|
||||
import { packedMessagingMessageSchema } from "@/models/schema/messaging-message.js";
|
||||
import { packedNotificationSchema } from "@/models/schema/notification.js";
|
||||
import { packedDriveFileSchema } from "@/models/schema/drive-file.js";
|
||||
import { packedDriveFolderSchema } from "@/models/schema/drive-folder.js";
|
||||
import { packedFollowingSchema } from "@/models/schema/following.js";
|
||||
import { packedMutingSchema } from "@/models/schema/muting.js";
|
||||
import { packedRenoteMutingSchema } from "@/models/schema/renote-muting.js";
|
||||
import { packedReplyMutingSchema } from "@/models/schema/reply-muting.js";
|
||||
import { packedBlockingSchema } from "@/models/schema/blocking.js";
|
||||
import { packedNoteReactionSchema } from "@/models/schema/note-reaction.js";
|
||||
import { packedHashtagSchema } from "@/models/schema/hashtag.js";
|
||||
import { packedPageSchema } from "@/models/schema/page.js";
|
||||
import { packedUserGroupSchema } from "@/models/schema/user-group.js";
|
||||
import { packedNoteFavoriteSchema } from "@/models/schema/note-favorite.js";
|
||||
import { packedChannelSchema } from "@/models/schema/channel.js";
|
||||
import { packedAntennaSchema } from "@/models/schema/antenna.js";
|
||||
import { packedClipSchema } from "@/models/schema/clip.js";
|
||||
import { packedFederationInstanceSchema } from "@/models/schema/federation-instance.js";
|
||||
import { packedQueueCountSchema } from "@/models/schema/queue.js";
|
||||
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";
|
||||
// TODO: use firefish-js
|
||||
import { Schema as _Schema } from "firefish-js";
|
||||
|
||||
export const refs = {
|
||||
AbuseUserReport: packedAbuseUserReportSchema,
|
||||
UserLite: packedUserLiteSchema,
|
||||
UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
|
||||
MeDetailedOnly: packedMeDetailedOnlySchema,
|
||||
UserDetailedNotMe: packedUserDetailedNotMeSchema,
|
||||
MeDetailed: packedMeDetailedSchema,
|
||||
UserDetailed: packedUserDetailedSchema,
|
||||
User: packedUserSchema,
|
||||
|
||||
UserList: packedUserListSchema,
|
||||
UserGroup: packedUserGroupSchema,
|
||||
App: packedAppSchema,
|
||||
MessagingMessage: packedMessagingMessageSchema,
|
||||
Note: packedNoteSchema,
|
||||
NoteFile: packedNoteFileSchema,
|
||||
NoteEdit: packedNoteEdit,
|
||||
NoteReaction: packedNoteReactionSchema,
|
||||
NoteFavorite: packedNoteFavoriteSchema,
|
||||
Notification: packedNotificationSchema,
|
||||
DriveFile: packedDriveFileSchema,
|
||||
DriveFolder: packedDriveFolderSchema,
|
||||
Following: packedFollowingSchema,
|
||||
Muting: packedMutingSchema,
|
||||
RenoteMuting: packedRenoteMutingSchema,
|
||||
ReplyMuting: packedReplyMutingSchema,
|
||||
Blocking: packedBlockingSchema,
|
||||
Hashtag: packedHashtagSchema,
|
||||
Page: packedPageSchema,
|
||||
Channel: packedChannelSchema,
|
||||
QueueCount: packedQueueCountSchema,
|
||||
Antenna: packedAntennaSchema,
|
||||
Clip: packedClipSchema,
|
||||
FederationInstance: packedFederationInstanceSchema,
|
||||
GalleryPost: packedGalleryPostSchema,
|
||||
Emoji: packedEmojiSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<(typeof refs)[x]>;
|
||||
|
||||
type TypeStringef =
|
||||
| "null"
|
||||
| "boolean"
|
||||
| "integer"
|
||||
| "number"
|
||||
| "string"
|
||||
| "array"
|
||||
| "object"
|
||||
| "any";
|
||||
type StringDefToType<T extends TypeStringef> = T extends "null"
|
||||
? null
|
||||
: T extends "boolean"
|
||||
? boolean
|
||||
: T extends "integer"
|
||||
? number
|
||||
: T extends "number"
|
||||
? number
|
||||
: T extends "string"
|
||||
? string | Date
|
||||
: T extends "array"
|
||||
? ReadonlyArray<any>
|
||||
: T extends "object"
|
||||
? Record<string, any>
|
||||
: any;
|
||||
|
||||
// https://swagger.io/specification/?sbsearch=optional#schema-object
|
||||
type OfSchema = {
|
||||
readonly anyOf?: ReadonlyArray<Schema>;
|
||||
readonly oneOf?: ReadonlyArray<Schema>;
|
||||
readonly allOf?: ReadonlyArray<Schema>;
|
||||
};
|
||||
|
||||
export interface Schema extends OfSchema {
|
||||
readonly type?: TypeStringef;
|
||||
readonly nullable?: boolean;
|
||||
readonly optional?: boolean;
|
||||
readonly items?: Schema;
|
||||
readonly properties?: Obj;
|
||||
readonly required?: ReadonlyArray<
|
||||
Extract<keyof NonNullable<this["properties"]>, string>
|
||||
>;
|
||||
readonly description?: string;
|
||||
readonly example?: any;
|
||||
readonly format?: string;
|
||||
readonly ref?: keyof typeof refs;
|
||||
readonly enum?: ReadonlyArray<string>;
|
||||
readonly default?:
|
||||
| (this["type"] extends TypeStringef ? StringDefToType<this["type"]> : any)
|
||||
| null;
|
||||
readonly maxLength?: number;
|
||||
readonly minLength?: number;
|
||||
readonly maximum?: number;
|
||||
readonly minimum?: number;
|
||||
readonly pattern?: string;
|
||||
}
|
||||
|
||||
type RequiredPropertyNames<s extends Obj> = {
|
||||
[K in keyof s]: // K is not optional
|
||||
s[K]["optional"] extends false
|
||||
? K
|
||||
: // K has default value
|
||||
s[K]["default"] extends
|
||||
| null
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Record<string, unknown>
|
||||
? K
|
||||
: never;
|
||||
}[keyof s];
|
||||
|
||||
export type Obj = Record<string, Schema>;
|
||||
|
||||
// https://github.com/misskey-dev/misskey/issues/8535
|
||||
// To avoid excessive stack depth error,
|
||||
// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
|
||||
export type ObjType<
|
||||
s extends Obj,
|
||||
RequiredProps extends keyof s,
|
||||
> = UnionToIntersection<
|
||||
{
|
||||
-readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]>;
|
||||
} & {
|
||||
-readonly [R in RequiredProps]-?: SchemaType<s[R]>;
|
||||
} & {
|
||||
-readonly [P in keyof s]?: SchemaType<s[P]>;
|
||||
}
|
||||
>;
|
||||
|
||||
type NullOrUndefined<p extends Schema, T> =
|
||||
| (p["nullable"] extends true ? null : never)
|
||||
| (p["optional"] extends true ? undefined : never)
|
||||
| T;
|
||||
|
||||
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
||||
// Get intersection from union
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I,
|
||||
) => void
|
||||
? I
|
||||
: never;
|
||||
|
||||
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
|
||||
// To get union, we use `Foo extends any ? Hoge<Foo> : never`
|
||||
type UnionSchemaType<
|
||||
a extends readonly any[],
|
||||
X extends Schema = a[number],
|
||||
> = X extends any ? SchemaType<X> : never;
|
||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
||||
|
||||
export type SchemaTypeDef<p extends Schema> = p["type"] extends "null"
|
||||
? null
|
||||
: p["type"] extends "integer"
|
||||
? number
|
||||
: p["type"] extends "number"
|
||||
? number
|
||||
: p["type"] extends "string"
|
||||
? p["enum"] extends readonly string[]
|
||||
? p["enum"][number]
|
||||
: p["format"] extends "date-time"
|
||||
? string
|
||||
: // Dateにする??
|
||||
string
|
||||
: p["type"] extends "boolean"
|
||||
? boolean
|
||||
: p["type"] extends "object"
|
||||
? p["ref"] extends keyof typeof refs
|
||||
? Packed<p["ref"]>
|
||||
: p["properties"] extends NonNullable<Obj>
|
||||
? ObjType<p["properties"], NonNullable<p["required"]>[number]>
|
||||
: p["anyOf"] extends ReadonlyArray<Schema>
|
||||
? UnionSchemaType<p["anyOf"]> &
|
||||
Partial<UnionToIntersection<UnionSchemaType<p["anyOf"]>>>
|
||||
: p["allOf"] extends ReadonlyArray<Schema>
|
||||
? UnionToIntersection<UnionSchemaType<p["allOf"]>>
|
||||
: any
|
||||
: p["type"] extends "array"
|
||||
? p["items"] extends OfSchema
|
||||
? p["items"]["anyOf"] extends ReadonlyArray<Schema>
|
||||
? UnionSchemaType<NonNullable<p["items"]["anyOf"]>>[]
|
||||
: p["items"]["oneOf"] extends ReadonlyArray<Schema>
|
||||
? ArrayUnion<
|
||||
UnionSchemaType<NonNullable<p["items"]["oneOf"]>>
|
||||
>
|
||||
: p["items"]["allOf"] extends ReadonlyArray<Schema>
|
||||
? UnionToIntersection<
|
||||
UnionSchemaType<NonNullable<p["items"]["allOf"]>>
|
||||
>[]
|
||||
: never
|
||||
: p["items"] extends NonNullable<Schema>
|
||||
? SchemaTypeDef<p["items"]>[]
|
||||
: any[]
|
||||
: p["oneOf"] extends ReadonlyArray<Schema>
|
||||
? UnionSchemaType<p["oneOf"]>
|
||||
: any;
|
||||
|
||||
export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;
|
||||
export const refs = _Schema.refs;
|
||||
export type Packed<T extends keyof typeof refs> = _Schema.Packed<T>;
|
||||
export type Schema = _Schema.Schema;
|
||||
export type SchemaType<P extends _Schema.Schema> = _Schema.SchemaType<P>;
|
||||
|
|
|
@ -53,7 +53,7 @@ import { UserProfiles } from "@/models/index.js";
|
|||
import { In } from "typeorm";
|
||||
import { config } from "@/config.js";
|
||||
import { truncate } from "@/misc/truncate.js";
|
||||
import { langmap } from "@/misc/langmap.js";
|
||||
import { langmap } from "firefish-js";
|
||||
import { inspect } from "node:util";
|
||||
|
||||
export function validateNote(object: any, uri: string) {
|
||||
|
|
|
@ -18,8 +18,8 @@ import { ApiError } from "@/server/api/error.js";
|
|||
import define from "@/server/api/define.js";
|
||||
import { HOUR, genId } from "backend-rs";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import { langmap } from "@/misc/langmap.js";
|
||||
import { createScheduledCreateNoteJob } from "@/queue";
|
||||
import { langmap } from "firefish-js";
|
||||
import { createScheduledCreateNoteJob } from "@/queue/index.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
|
|
@ -33,7 +33,7 @@ import renderNote from "@/remote/activitypub/renderer/note.js";
|
|||
import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
||||
import { deliverToRelays } from "@/services/relay.js";
|
||||
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||
import { langmap } from "@/misc/langmap.js";
|
||||
import { langmap } from "firefish-js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ApiError } from "@/server/api/error.js";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import { translate } from "@/misc/translate.js";
|
||||
import type { PostLanguage } from "@/misc/langmap.js";
|
||||
import type { PostLanguage } from "firefish-js";
|
||||
import define from "@/server/api/define.js";
|
||||
|
||||
export const meta = {
|
||||
|
|
|
@ -63,7 +63,7 @@ import { db } from "@/db/postgre.js";
|
|||
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
||||
import { redisClient } from "@/db/redis.js";
|
||||
import { Mutex } from "redis-semaphore";
|
||||
import { langmap } from "@/misc/langmap.js";
|
||||
import { langmap } from "firefish-js";
|
||||
import Logger from "@/services/logger.js";
|
||||
import { inspect } from "node:util";
|
||||
import { toRustObject } from "@/prelude/undefined-to-null.js";
|
||||
|
|
|
@ -7,6 +7,7 @@ import { alert, api, popup, popupMenu, waiting } from "@/os";
|
|||
import icon from "@/scripts/icon";
|
||||
import { del, get, set } from "@/scripts/idb-proxy";
|
||||
import { reloadChannel, unisonReload } from "@/scripts/unison-reload";
|
||||
import type { MenuButton, MenuUser } from "./types/menu";
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
|
@ -16,7 +17,7 @@ export async function signOut() {
|
|||
waiting();
|
||||
localStorage.removeItem("account");
|
||||
|
||||
await removeAccount(me.id);
|
||||
await removeAccount(me!.id);
|
||||
|
||||
const accounts = await getAccounts();
|
||||
|
||||
|
@ -26,12 +27,9 @@ export async function signOut() {
|
|||
const registration = await navigator.serviceWorker.ready;
|
||||
const push = await registration.pushManager.getSubscription();
|
||||
if (push) {
|
||||
await fetch(`${apiUrl}/sw/unregister`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
i: me.token,
|
||||
endpoint: push.endpoint,
|
||||
}),
|
||||
await api("sw/unregister", {
|
||||
endpoint: push.endpoint,
|
||||
i: me!.token, // FIXME: This parameter seems to be removable but I didn't test it
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -117,13 +115,13 @@ function showSuspendedDialog() {
|
|||
|
||||
export function updateAccount(accountData) {
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
me[key] = value;
|
||||
me![key] = value;
|
||||
}
|
||||
localStorage.setItem("account", JSON.stringify(me));
|
||||
}
|
||||
|
||||
export async function refreshAccount() {
|
||||
const accountData = await fetchAccount(me.token);
|
||||
const accountData = await fetchAccount(me!.token);
|
||||
return updateAccount(accountData);
|
||||
}
|
||||
|
||||
|
@ -189,7 +187,7 @@ export async function openAccountMenu(
|
|||
|
||||
async function switchAccount(account: entities.UserDetailed) {
|
||||
const storedAccounts = await getAccounts();
|
||||
const token = storedAccounts.find((x) => x.id === account.id).token;
|
||||
const token = storedAccounts.find((x) => x.id === account.id)!.token;
|
||||
switchAccountWithToken(token);
|
||||
}
|
||||
|
||||
|
@ -198,15 +196,15 @@ export async function openAccountMenu(
|
|||
}
|
||||
|
||||
const storedAccounts = await getAccounts().then((accounts) =>
|
||||
accounts.filter((x) => x.id !== me.id),
|
||||
accounts.filter((x) => x.id !== me!.id),
|
||||
);
|
||||
const accountsPromise = api("users/show", {
|
||||
userIds: storedAccounts.map((x) => x.id),
|
||||
});
|
||||
|
||||
function createItem(account: entities.UserDetailed) {
|
||||
function createItem(account: entities.UserDetailed): MenuUser {
|
||||
return {
|
||||
type: "user",
|
||||
type: "user" as const,
|
||||
user: account,
|
||||
active: opts.active != null ? opts.active === account.id : false,
|
||||
action: () => {
|
||||
|
@ -221,10 +219,14 @@ export async function openAccountMenu(
|
|||
|
||||
const accountItemPromises = storedAccounts.map(
|
||||
(a) =>
|
||||
new Promise((res) => {
|
||||
new Promise<MenuUser>((res) => {
|
||||
accountsPromise.then((accounts) => {
|
||||
const account = accounts.find((x) => x.id === a.id);
|
||||
if (account == null) return res(null);
|
||||
if (account == null) {
|
||||
// The user is deleted, remove it
|
||||
removeAccount(a.id);
|
||||
return res(null as unknown as MenuUser);
|
||||
}
|
||||
res(createItem(account));
|
||||
});
|
||||
}),
|
||||
|
@ -233,74 +235,72 @@ export async function openAccountMenu(
|
|||
if (opts.withExtraOperation) {
|
||||
popupMenu(
|
||||
[
|
||||
...[
|
||||
...(isMobile ?? false
|
||||
? [
|
||||
{
|
||||
type: "parent",
|
||||
icon: `${icon("ph-plus")}`,
|
||||
text: i18n.ts.addAccount,
|
||||
children: [
|
||||
{
|
||||
text: i18n.ts.existingAccount,
|
||||
action: () => {
|
||||
showSigninDialog();
|
||||
},
|
||||
...(isMobile ?? false
|
||||
? [
|
||||
{
|
||||
type: "parent" as const,
|
||||
icon: `${icon("ph-plus")}`,
|
||||
text: i18n.ts.addAccount,
|
||||
children: [
|
||||
{
|
||||
text: i18n.ts.existingAccount,
|
||||
action: () => {
|
||||
showSigninDialog();
|
||||
},
|
||||
{
|
||||
text: i18n.ts.createAccount,
|
||||
action: () => {
|
||||
createAccount();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: i18n.ts.createAccount,
|
||||
action: () => {
|
||||
createAccount();
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: "link",
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${me.username}`,
|
||||
avatar: me,
|
||||
},
|
||||
null,
|
||||
]),
|
||||
...(opts.includeCurrentAccount ? [createItem(me)] : []),
|
||||
...accountItemPromises,
|
||||
...(isMobile ?? false
|
||||
? [
|
||||
null,
|
||||
{
|
||||
type: "link",
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${me.username}`,
|
||||
avatar: me,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: "parent",
|
||||
icon: `${icon("ph-plus")}`,
|
||||
text: i18n.ts.addAccount,
|
||||
children: [
|
||||
{
|
||||
text: i18n.ts.existingAccount,
|
||||
action: () => {
|
||||
showSigninDialog();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: "link" as const,
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${me!.username}`,
|
||||
avatar: me!,
|
||||
},
|
||||
null,
|
||||
]),
|
||||
...(opts.includeCurrentAccount ? [createItem(me!)] : []),
|
||||
...accountItemPromises,
|
||||
...(isMobile ?? false
|
||||
? [
|
||||
null,
|
||||
{
|
||||
type: "link" as const,
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${me!.username}`,
|
||||
avatar: me!,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: "parent" as const,
|
||||
icon: `${icon("ph-plus")}`,
|
||||
text: i18n.ts.addAccount,
|
||||
children: [
|
||||
{
|
||||
text: i18n.ts.existingAccount,
|
||||
action: () => {
|
||||
showSigninDialog();
|
||||
},
|
||||
{
|
||||
text: i18n.ts.createAccount,
|
||||
action: () => {
|
||||
createAccount();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: i18n.ts.createAccount,
|
||||
action: () => {
|
||||
createAccount();
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
],
|
||||
ev.currentTarget ?? ev.target,
|
||||
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||
{
|
||||
align: "left",
|
||||
},
|
||||
|
@ -308,10 +308,10 @@ export async function openAccountMenu(
|
|||
} else {
|
||||
popupMenu(
|
||||
[
|
||||
...(opts.includeCurrentAccount ? [createItem(me)] : []),
|
||||
...(opts.includeCurrentAccount ? [createItem(me!)] : []),
|
||||
...accountItemPromises,
|
||||
],
|
||||
ev.currentTarget ?? ev.target,
|
||||
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||
{
|
||||
align: "left",
|
||||
},
|
||||
|
|
121
packages/client/src/cold-store.ts
Normal file
121
packages/client/src/cold-store.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { ref as vueRef } from "vue";
|
||||
import type { UnwrapRef } from "vue";
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
const PREFIX = "miux:";
|
||||
|
||||
interface Plugin {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
configData: Record<string, unknown>;
|
||||
token: string;
|
||||
ast: unknown[];
|
||||
}
|
||||
|
||||
import darkTheme from "@/themes/d-rosepine.json5";
|
||||
/**
|
||||
* Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
|
||||
*/
|
||||
import lightTheme from "@/themes/l-rosepinedawn.json5";
|
||||
|
||||
const ColdStoreDefault = {
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
syncDeviceDarkMode: true,
|
||||
plugins: [] as Plugin[],
|
||||
mediaVolume: 0.5,
|
||||
vibrate: false,
|
||||
sound_masterVolume: 0.3,
|
||||
sound_note: { type: "none", volume: 0 },
|
||||
sound_noteMy: { type: "syuilo/up", volume: 1 },
|
||||
sound_notification: { type: "syuilo/pope2", volume: 1 },
|
||||
sound_chat: { type: "syuilo/pope1", volume: 1 },
|
||||
sound_chatBg: { type: "syuilo/waon", volume: 1 },
|
||||
sound_antenna: { type: "syuilo/triple", volume: 1 },
|
||||
sound_channel: { type: "syuilo/square-pico", volume: 1 },
|
||||
};
|
||||
|
||||
const watchers: {
|
||||
key: string;
|
||||
callback: (value) => void;
|
||||
}[] = [];
|
||||
|
||||
function get<T extends keyof typeof ColdStoreDefault>(
|
||||
key: T,
|
||||
): (typeof ColdStoreDefault)[T] {
|
||||
// TODO: indexedDBにする
|
||||
// ただしその際はnullチェックではなくキー存在チェックにしないとダメ
|
||||
// (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
|
||||
const value = localStorage.getItem(PREFIX + key);
|
||||
if (value == null) {
|
||||
return ColdStoreDefault[key];
|
||||
} else {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
}
|
||||
|
||||
function set<T extends keyof typeof ColdStoreDefault>(
|
||||
key: T,
|
||||
value: (typeof ColdStoreDefault)[T],
|
||||
): void {
|
||||
// 呼び出し側のバグ等で undefined が来ることがある
|
||||
// undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
|
||||
if (value === undefined) {
|
||||
console.error(`attempt to store undefined value for key '${key}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(PREFIX + key, JSON.stringify(value));
|
||||
|
||||
for (const watcher of watchers) {
|
||||
if (watcher.key === key) watcher.callback(value);
|
||||
}
|
||||
}
|
||||
|
||||
function watch<T extends keyof typeof ColdStoreDefault>(
|
||||
key: T,
|
||||
callback: (value: (typeof ColdStoreDefault)[T]) => void,
|
||||
) {
|
||||
watchers.push({ key, callback });
|
||||
}
|
||||
|
||||
// TODO: VueのcustomRef使うと良い感じになるかも
|
||||
function ref<T extends keyof typeof ColdStoreDefault>(key: T) {
|
||||
const v = get(key);
|
||||
const r = vueRef(v);
|
||||
// TODO: このままではwatcherがリークするので開放する方法を考える
|
||||
watch(key, (v) => {
|
||||
r.value = v as UnwrapRef<typeof v>;
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||
* 主にvue場で設定コントロールのmodelとして使う用
|
||||
*/
|
||||
function makeGetterSetter<K extends keyof typeof ColdStoreDefault>(key: K) {
|
||||
// TODO: VueのcustomRef使うと良い感じになるかも
|
||||
const valueRef = ref(key);
|
||||
return {
|
||||
get: () => {
|
||||
return valueRef.value;
|
||||
},
|
||||
set: (value: (typeof ColdStoreDefault)[K]) => {
|
||||
const val = value;
|
||||
set(key, val);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
default: ColdStoreDefault,
|
||||
watchers,
|
||||
get,
|
||||
set,
|
||||
watch,
|
||||
ref,
|
||||
makeGetterSetter,
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
|
||||
<MkModal ref="modal" :z-priority="'middle'" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.title">
|
||||
<MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle>
|
||||
|
@ -41,6 +41,10 @@ const props = defineProps<{
|
|||
announcement: entities.Announcement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
closed: [];
|
||||
}>();
|
||||
|
||||
const { id, text, title, imageUrl, isGoodNews } = props.announcement;
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
|
|
@ -182,7 +182,7 @@ export default {
|
|||
const props = defineProps<{
|
||||
type: string;
|
||||
q: string | null;
|
||||
textarea: HTMLTextAreaElement;
|
||||
textarea: HTMLTextAreaElement | HTMLInputElement;
|
||||
close: () => void;
|
||||
x: number;
|
||||
y: number;
|
||||
|
@ -435,7 +435,7 @@ onUpdated(() => {
|
|||
onMounted(() => {
|
||||
setPosition();
|
||||
|
||||
props.textarea.addEventListener("keydown", onKeydown);
|
||||
(props.textarea as HTMLTextAreaElement).addEventListener("keydown", onKeydown);
|
||||
document.body.addEventListener("mousedown", onMousedown);
|
||||
|
||||
nextTick(() => {
|
||||
|
@ -453,7 +453,7 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.textarea.removeEventListener("keydown", onKeydown);
|
||||
(props.textarea as HTMLTextAreaElement).removeEventListener("keydown", onKeydown);
|
||||
document.body.removeEventListener("mousedown", onMousedown);
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
|
||||
<MkModal ref="modal" :z-priority="'middle'" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<p :class="$style.title">
|
||||
{{ i18n.ts.youHaveUnreadAnnouncements }}
|
||||
|
@ -21,6 +21,10 @@ import MkModal from "@/components/MkModal.vue";
|
|||
import MkButton from "@/components/MkButton.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const emit = defineEmits<{
|
||||
closed: [];
|
||||
}>();
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
const checkAnnouncements = () => {
|
||||
modal.value!.close();
|
||||
|
|
|
@ -852,13 +852,17 @@ function setLanguage() {
|
|||
actions.push(null);
|
||||
}
|
||||
|
||||
if (language.value != null)
|
||||
if (language.value != null && langmap[language.value] != null) {
|
||||
actions.push({
|
||||
text: langmap[language.value].nativeName,
|
||||
danger: false,
|
||||
active: true,
|
||||
action: () => {},
|
||||
});
|
||||
} else {
|
||||
// Unrecognized language, set to null
|
||||
language.value = null;
|
||||
}
|
||||
|
||||
const langs = Object.keys(langmap);
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ const hCaptchaResponse = ref(null);
|
|||
const reCaptchaResponse = ref(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: "login", v: any): void;
|
||||
login: [v: { id: string; i: string }];
|
||||
}>();
|
||||
|
||||
const props = defineProps({
|
||||
|
|
|
@ -30,7 +30,7 @@ withDefaults(
|
|||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: "done"): void;
|
||||
(ev: "done", res: { id: string; i: string }): void;
|
||||
(ev: "closed"): void;
|
||||
(ev: "cancelled"): void;
|
||||
}>();
|
||||
|
@ -39,11 +39,11 @@ const dialog = ref<InstanceType<typeof XModalWindow>>();
|
|||
|
||||
function onClose() {
|
||||
emit("cancelled");
|
||||
dialog.value.close();
|
||||
dialog.value!.close();
|
||||
}
|
||||
|
||||
function onLogin(res) {
|
||||
function onLogin(res: { id: string; i: string }) {
|
||||
emit("done", res);
|
||||
dialog.value.close();
|
||||
dialog.value!.close();
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -248,7 +248,7 @@
|
|||
v-model="hCaptchaResponse"
|
||||
class="_formBlock captcha"
|
||||
provider="hcaptcha"
|
||||
:sitekey="instance.hcaptchaSiteKey"
|
||||
:sitekey="instance.hcaptchaSiteKey!"
|
||||
/>
|
||||
<MkCaptcha
|
||||
v-if="instance.enableRecaptcha"
|
||||
|
@ -256,7 +256,7 @@
|
|||
v-model="reCaptchaResponse"
|
||||
class="_formBlock captcha"
|
||||
provider="recaptcha"
|
||||
:sitekey="instance.recaptchaSiteKey"
|
||||
:sitekey="instance.recaptchaSiteKey!"
|
||||
/>
|
||||
<MkButton
|
||||
class="_formBlock"
|
||||
|
@ -296,7 +296,7 @@ const props = withDefaults(
|
|||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: "signup", user: Record<string, any>): void;
|
||||
(ev: "signup", user: { id: string; i: string }): void;
|
||||
(ev: "signupEmailPending"): void;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -36,13 +36,13 @@ withDefaults(
|
|||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: "done"): void;
|
||||
(ev: "done", res: { id: string; i: string }): void;
|
||||
(ev: "closed"): void;
|
||||
}>();
|
||||
|
||||
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
||||
|
||||
function onSignup(res) {
|
||||
function onSignup(res: { id: string; i: string }) {
|
||||
emit("done", res);
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<MkModal
|
||||
ref="modal"
|
||||
:z-priority="'middle'"
|
||||
@click="$refs.modal.close()"
|
||||
@closed="$emit('closed')"
|
||||
@click="modal!.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.title">
|
||||
|
@ -14,7 +14,7 @@
|
|||
:class="$style.gotIt"
|
||||
primary
|
||||
full
|
||||
@click="$refs.modal.close()"
|
||||
@click="modal!.close()"
|
||||
>{{ i18n.ts.gotIt }}</MkButton
|
||||
>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { markRaw } from "vue";
|
||||
import { locale } from "@/config";
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: temporary use any
|
||||
class I18n<T extends Record<string, any>> {
|
||||
public ts: T;
|
||||
|
||||
|
|
|
@ -245,7 +245,12 @@ function checkForSplash() {
|
|||
|
||||
try {
|
||||
// 変なバージョン文字列来るとcompareVersionsでエラーになるため
|
||||
if (lastVersion < version && defaultStore.state.showUpdates) {
|
||||
// If a strange version string comes, an error will occur in compareVersions.
|
||||
if (
|
||||
lastVersion != null &&
|
||||
lastVersion < version &&
|
||||
defaultStore.state.showUpdates
|
||||
) {
|
||||
// ログインしてる場合だけ
|
||||
if (me) {
|
||||
popup(
|
||||
|
@ -281,7 +286,7 @@ function checkForSplash() {
|
|||
"closed",
|
||||
);
|
||||
} else {
|
||||
unreadAnnouncements.forEach((item) => {
|
||||
for (const item of unreadAnnouncements) {
|
||||
if (item.showPopup)
|
||||
popup(
|
||||
defineAsyncComponent(
|
||||
|
@ -291,7 +296,7 @@ function checkForSplash() {
|
|||
{},
|
||||
"closed",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// NIRAX --- A lightweight router
|
||||
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import type { Component, ShallowRef } from "vue";
|
||||
import type { Component } from "vue";
|
||||
import { shallowRef } from "vue";
|
||||
import { safeURIDecode } from "@/scripts/safe-uri-decode";
|
||||
import { pleaseLogin } from "@/scripts/please-login";
|
||||
|
@ -36,6 +36,7 @@ export interface Resolved {
|
|||
function parsePath(path: string): ParsedPath {
|
||||
const res = [] as ParsedPath;
|
||||
|
||||
// biome-ignore lint/style/noParameterAssign: assign it intentionally
|
||||
path = path.substring(1);
|
||||
|
||||
for (const part of path.split("/")) {
|
||||
|
@ -76,13 +77,13 @@ export class Router extends EventEmitter<{
|
|||
same: () => void;
|
||||
}> {
|
||||
private routes: RouteDef[];
|
||||
public current: Resolved;
|
||||
public currentRef: ShallowRef<Resolved> = shallowRef();
|
||||
public currentRoute: ShallowRef<RouteDef> = shallowRef();
|
||||
public current!: Resolved; // It is assigned in this.navigate
|
||||
public currentRef = shallowRef<Resolved>();
|
||||
public currentRoute = shallowRef<RouteDef>();
|
||||
private currentPath: string;
|
||||
private currentKey = Date.now().toString();
|
||||
|
||||
public navHook: ((path: string, flag?: any) => boolean) | null = null;
|
||||
public navHook: ((path: string, flag?: unknown) => boolean) | null = null;
|
||||
|
||||
constructor(routes: Router["routes"], currentPath: Router["currentPath"]) {
|
||||
super();
|
||||
|
@ -92,9 +93,10 @@ export class Router extends EventEmitter<{
|
|||
this.navigate(currentPath, null, false);
|
||||
}
|
||||
|
||||
public resolve(path: string): Resolved | null {
|
||||
public resolve(_path: string): Resolved | null {
|
||||
let queryString: string | null = null;
|
||||
let hash: string | null = null;
|
||||
let path = _path;
|
||||
if (path[0] === "/") path = path.substring(1);
|
||||
if (path.includes("#")) {
|
||||
hash = path.substring(path.indexOf("#") + 1);
|
||||
|
@ -168,9 +170,16 @@ export class Router extends EventEmitter<{
|
|||
}
|
||||
|
||||
if (route.query != null && queryString != null) {
|
||||
const queryObject = [
|
||||
...new URLSearchParams(queryString).entries(),
|
||||
].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
|
||||
// const queryObject = [
|
||||
// ...new URLSearchParams(queryString).entries(),
|
||||
// ].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
|
||||
|
||||
const queryObject: Record<string, string> = Object.assign(
|
||||
{},
|
||||
...[...new URLSearchParams(queryString).entries()].map(
|
||||
(entry) => ({ [entry[0]]: entry[1] }),
|
||||
),
|
||||
);
|
||||
|
||||
for (const q in route.query) {
|
||||
const as = route.query[q];
|
||||
|
@ -227,6 +236,7 @@ export class Router extends EventEmitter<{
|
|||
}
|
||||
|
||||
const isSamePath = beforePath === path;
|
||||
// biome-ignore lint/style/noParameterAssign: assign it intentionally
|
||||
if (isSamePath && key == null) key = this.currentKey;
|
||||
this.current = res;
|
||||
this.currentRef.value = res;
|
||||
|
@ -253,7 +263,7 @@ export class Router extends EventEmitter<{
|
|||
return this.currentKey;
|
||||
}
|
||||
|
||||
public push(path: string, flag?: any) {
|
||||
public push(path: string, flag?: unknown) {
|
||||
const beforePath = this.currentPath;
|
||||
if (path === beforePath) {
|
||||
this.emit("same");
|
||||
|
|
|
@ -896,9 +896,6 @@ export async function openEmojiPicker(
|
|||
...opts,
|
||||
},
|
||||
{
|
||||
chosen: (emoji) => {
|
||||
insertTextAtCursor(activeTextarea, emoji);
|
||||
},
|
||||
done: (emoji) => {
|
||||
insertTextAtCursor(activeTextarea, emoji);
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
i18n.ts._accountDelete.sendEmail
|
||||
}}</FormInfo>
|
||||
<FormButton
|
||||
v-if="!me?.isDeleted"
|
||||
v-if="!me!.isDeleted"
|
||||
danger
|
||||
class="_formBlock"
|
||||
@click="deleteAccount"
|
||||
|
|
|
@ -5,12 +5,13 @@ import { onUnmounted, ref, watch } from "vue";
|
|||
import { api } from "./os";
|
||||
import { useStream } from "./stream";
|
||||
import { isSignedIn, me } from "@/me";
|
||||
import type { TypeUtils } from "firefish-js";
|
||||
|
||||
type StateDef = Record<
|
||||
string,
|
||||
{
|
||||
where: "account" | "device" | "deviceAccount";
|
||||
default: any;
|
||||
default: unknown;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -82,11 +83,12 @@ export class Storage<T extends StateDef> {
|
|||
for (const [k, v] of Object.entries(state)) {
|
||||
reactiveState[k] = ref(v);
|
||||
}
|
||||
this.state = state as any;
|
||||
this.reactiveState = reactiveState as any;
|
||||
this.state = state as typeof this.state;
|
||||
this.reactiveState = reactiveState as typeof this.reactiveState;
|
||||
|
||||
if (isSignedIn(me)) {
|
||||
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
|
||||
// For some reason, if I don't setTimeout, an error occurs in the api function (probably caused by circular references)
|
||||
window.setTimeout(() => {
|
||||
api("i/registry/get-all", { scope: ["client", this.key] }).then(
|
||||
(kvs) => {
|
||||
|
@ -104,7 +106,7 @@ export class Storage<T extends StateDef> {
|
|||
}
|
||||
}
|
||||
localStorage.setItem(
|
||||
`${this.keyForLocalStorage}::cache::${me.id}`,
|
||||
`${this.keyForLocalStorage}::cache::${me!.id}`,
|
||||
JSON.stringify(cache),
|
||||
);
|
||||
},
|
||||
|
@ -118,11 +120,12 @@ export class Storage<T extends StateDef> {
|
|||
key,
|
||||
value,
|
||||
}: {
|
||||
scope: string[];
|
||||
scope?: string[];
|
||||
key: keyof T;
|
||||
value: T[typeof key]["default"];
|
||||
}) => {
|
||||
if (
|
||||
scope == null ||
|
||||
scope.length !== 2 ||
|
||||
scope[0] !== "client" ||
|
||||
scope[1] !== this.key ||
|
||||
|
@ -135,13 +138,13 @@ export class Storage<T extends StateDef> {
|
|||
|
||||
const cache = JSON.parse(
|
||||
localStorage.getItem(
|
||||
`${this.keyForLocalStorage}::cache::${me.id}`,
|
||||
`${this.keyForLocalStorage}::cache::${me!.id}`,
|
||||
) || "{}",
|
||||
);
|
||||
if (cache[key] !== value) {
|
||||
cache[key] = value;
|
||||
localStorage.setItem(
|
||||
`${this.keyForLocalStorage}::cache::${me.id}`,
|
||||
`${this.keyForLocalStorage}::cache::${me!.id}`,
|
||||
JSON.stringify(cache),
|
||||
);
|
||||
}
|
||||
|
@ -150,7 +153,7 @@ export class Storage<T extends StateDef> {
|
|||
}
|
||||
}
|
||||
|
||||
public set<K extends keyof T>(key: K, value: T[K]["default"]): void {
|
||||
public set<K extends keyof T>(key: K & string, value: T[K]["default"]): void {
|
||||
if (_DEV_) console.log("set", key, value);
|
||||
|
||||
this.state[key] = value;
|
||||
|
@ -201,15 +204,15 @@ export class Storage<T extends StateDef> {
|
|||
}
|
||||
}
|
||||
|
||||
public push<K extends keyof T>(
|
||||
key: K,
|
||||
public push<K extends TypeUtils.PropertyOfType<T, { default: unknown[] }>>(
|
||||
key: K & string,
|
||||
value: ArrayElement<T[K]["default"]>,
|
||||
): void {
|
||||
const currentState = this.state[key];
|
||||
const currentState = this.state[key] as unknown[];
|
||||
this.set(key, [...currentState, value]);
|
||||
}
|
||||
|
||||
public reset(key: keyof T) {
|
||||
public reset(key: keyof T & string) {
|
||||
this.set(key, this.def[key].default);
|
||||
}
|
||||
|
||||
|
@ -218,11 +221,11 @@ export class Storage<T extends StateDef> {
|
|||
* 主にvue場で設定コントロールのmodelとして使う用
|
||||
*/
|
||||
public makeGetterSetter<K extends keyof T>(
|
||||
key: K,
|
||||
getter?: (v: T[K]) => unknown,
|
||||
setter?: (v: unknown) => T[K],
|
||||
key: K & string,
|
||||
getter?: (oldV: T[K]["default"]) => T[K]["default"],
|
||||
setter?: (oldV: T[K]["default"]) => T[K]["default"],
|
||||
) {
|
||||
const valueRef = ref(this.state[key]);
|
||||
const valueRef = ref(this.state[key]) as Ref<T[K]["default"]>;
|
||||
|
||||
const stop = watch(this.reactiveState[key], (val) => {
|
||||
valueRef.value = val;
|
||||
|
@ -242,7 +245,7 @@ export class Storage<T extends StateDef> {
|
|||
return valueRef.value;
|
||||
}
|
||||
},
|
||||
set: (value: unknown) => {
|
||||
set: (value: T[K]["default"]) => {
|
||||
const val = setter ? setter(value) : value;
|
||||
this.set(key, val);
|
||||
valueRef.value = val;
|
||||
|
|
|
@ -42,7 +42,20 @@ export function install(plugin) {
|
|||
aiscript.exec(parser.parse(plugin.src));
|
||||
}
|
||||
|
||||
function createPluginEnv(opts) {
|
||||
interface Plugin {
|
||||
config?: Record<
|
||||
string,
|
||||
{
|
||||
default: unknown;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
>;
|
||||
configData: Record<string, unknown>;
|
||||
token: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function createPluginEnv(opts: { plugin: Plugin; storageKey: string }) {
|
||||
const config = new Map<string, values.Value>();
|
||||
for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
|
||||
config.set(
|
||||
|
@ -172,7 +185,7 @@ function registerNoteAction({ pluginId, title, handler }) {
|
|||
if (!pluginContext) {
|
||||
return;
|
||||
}
|
||||
pluginContext.execFn(handler, [utils.jsToVal(user)]);
|
||||
pluginContext.execFn(handler, [utils.jsToVal(note)]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -205,16 +218,18 @@ function registerNotePostInterruptor({ pluginId, handler }) {
|
|||
});
|
||||
}
|
||||
|
||||
// FIXME: where is pageViewInterruptors?
|
||||
// This function currently can't do anything
|
||||
function registerPageViewInterruptor({ pluginId, handler }): void {
|
||||
pageViewInterruptors.push({
|
||||
handler: async (page) => {
|
||||
const pluginContext = pluginContexts.get(pluginId);
|
||||
if (!pluginContext) {
|
||||
return;
|
||||
}
|
||||
return utils.valToJs(
|
||||
await pluginContext.execFn(handler, [utils.jsToVal(page)]),
|
||||
);
|
||||
},
|
||||
});
|
||||
// pageViewInterruptors.push({
|
||||
// handler: async (page) => {
|
||||
// const pluginContext = pluginContexts.get(pluginId);
|
||||
// if (!pluginContext) {
|
||||
// return;
|
||||
// }
|
||||
// return utils.valToJs(
|
||||
// await pluginContext.execFn(handler, [utils.jsToVal(page)]),
|
||||
// );
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export function byteify(string: string, encoding: "ascii" | "base64" | "hex") {
|
|||
);
|
||||
case "hex":
|
||||
return new Uint8Array(
|
||||
string.match(/.{1,2}/g).map((byte) => Number.parseInt(byte, 16)),
|
||||
string.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { EndoRelation, Predicate } from "./relation";
|
||||
import type { EndoRelation, Predicate } from "@/types/relation";
|
||||
|
||||
/**
|
||||
* Count the number of elements that satisfy the predicate
|
||||
|
@ -126,7 +126,7 @@ export function lessThan(xs: number[], ys: number[]): boolean {
|
|||
* Returns the longest prefix of elements that satisfy the predicate
|
||||
*/
|
||||
export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
|
||||
const ys = [];
|
||||
const ys: T[] = [];
|
||||
for (const x of xs) {
|
||||
if (f(x)) {
|
||||
ys.push(x);
|
||||
|
|
|
@ -13,7 +13,7 @@ export class Autocomplete {
|
|||
} | null;
|
||||
|
||||
private textarea: HTMLInputElement | HTMLTextAreaElement;
|
||||
private currentType: string;
|
||||
private currentType?: string;
|
||||
private textRef: Ref<string>;
|
||||
private opening: boolean;
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class Autocomplete {
|
|||
* テキスト入力時
|
||||
*/
|
||||
private onInput() {
|
||||
const caretPos = this.textarea.selectionStart;
|
||||
const caretPos = this.textarea.selectionStart!;
|
||||
const text = this.text.substring(0, caretPos).split("\n").pop()!;
|
||||
|
||||
const mentionIndex = text.lastIndexOf("@");
|
||||
|
@ -147,10 +147,10 @@ export class Autocomplete {
|
|||
this.opening = true;
|
||||
this.currentType = type;
|
||||
|
||||
// #region サジェストを表示すべき位置を計算
|
||||
// #region Calculate the position where suggestions should be displayed
|
||||
const caretPosition = getCaretCoordinates(
|
||||
this.textarea,
|
||||
this.textarea.selectionStart,
|
||||
this.textarea.selectionStart!,
|
||||
);
|
||||
|
||||
const rect = this.textarea.getBoundingClientRect();
|
||||
|
@ -216,7 +216,7 @@ export class Autocomplete {
|
|||
private complete({ type, value }) {
|
||||
this.close();
|
||||
|
||||
const caret = this.textarea.selectionStart;
|
||||
const caret = this.textarea.selectionStart!;
|
||||
|
||||
if (type === "user") {
|
||||
const source = this.text;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { entities } from "firefish-js";
|
||||
import { detectLanguage, languageContains } from "./language-utils";
|
||||
|
||||
export interface Muted {
|
||||
muted: boolean;
|
||||
|
@ -12,11 +13,12 @@ function checkLangMute(
|
|||
note: entities.Note,
|
||||
mutedLangs: Array<string | string[]>,
|
||||
): Muted {
|
||||
const mutedLangList = new Set(
|
||||
mutedLangs.reduce((arr, x) => [...arr, ...(Array.isArray(x) ? x : [x])]),
|
||||
);
|
||||
if (mutedLangList.has((note.lang?.[0]?.lang || "").split("-")[0])) {
|
||||
return { muted: true, matched: [note.lang?.[0]?.lang] };
|
||||
const mutedLangList = mutedLangs.flat();
|
||||
const noteLang = note.lang ?? detectLanguage(note.text ?? "") ?? "no-lang";
|
||||
for (const mutedLang of mutedLangList) {
|
||||
if (languageContains(mutedLang, noteLang)) {
|
||||
return { muted: true, matched: [noteLang] };
|
||||
}
|
||||
}
|
||||
return NotMuted;
|
||||
}
|
||||
|
@ -32,7 +34,7 @@ function checkWordMute(
|
|||
|
||||
if (text === "") return NotMuted;
|
||||
|
||||
const result = { muted: false, matched: [] };
|
||||
const result = { muted: false, matched: [] as string[] };
|
||||
|
||||
for (const mutePattern of mutedWords) {
|
||||
if (Array.isArray(mutePattern)) {
|
||||
|
@ -74,7 +76,7 @@ function checkWordMute(
|
|||
}
|
||||
|
||||
export function getWordSoftMute(
|
||||
note: firefish.entities.Note,
|
||||
note: entities.Note,
|
||||
meId: string | null | undefined,
|
||||
mutedWords: Array<string | string[]>,
|
||||
mutedLangs: Array<string | string[]>,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export function collectPageVars(content) {
|
||||
const pageVars = [];
|
||||
const collect = (xs: any[]) => {
|
||||
import type { PageContent, PageVar } from "@/types/page";
|
||||
|
||||
export function collectPageVars(content: PageContent[]) {
|
||||
const pageVars: PageVar[] = [];
|
||||
const collect = (xs: PageContent[]) => {
|
||||
for (const x of xs) {
|
||||
if (x.type === "textInput") {
|
||||
pageVars.push({
|
||||
|
@ -24,7 +26,7 @@ export function collectPageVars(content) {
|
|||
pageVars.push({
|
||||
name: x.name,
|
||||
type: "boolean",
|
||||
value: x.default,
|
||||
value: x.default!,
|
||||
});
|
||||
} else if (x.type === "counter") {
|
||||
pageVars.push({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Clipboardに値をコピー(TODO: 文字列以外も対応)
|
||||
*/
|
||||
export default (val) => {
|
||||
function obsoleteCopyToClipboard(val: string) {
|
||||
// 空div 生成
|
||||
const tmp = document.createElement("div");
|
||||
// 選択用のタグ生成
|
||||
|
@ -21,7 +21,7 @@ export default (val) => {
|
|||
// body に追加
|
||||
document.body.appendChild(tmp);
|
||||
// 要素を選択
|
||||
document.getSelection().selectAllChildren(tmp);
|
||||
document.getSelection()?.selectAllChildren(tmp);
|
||||
|
||||
// クリップボードにコピー
|
||||
const result = document.execCommand("copy");
|
||||
|
@ -30,4 +30,20 @@ export default (val) => {
|
|||
document.body.removeChild(tmp);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function (val?: string | null) {
|
||||
if (val == null) return true;
|
||||
const clipboardObj = window.navigator?.clipboard;
|
||||
if (clipboardObj == null) {
|
||||
// not supported
|
||||
return obsoleteCopyToClipboard(val);
|
||||
} else {
|
||||
return new Promise<boolean>((res) => {
|
||||
clipboardObj
|
||||
.writeText(val)
|
||||
.then(() => res(true))
|
||||
.catch(() => res(obsoleteCopyToClipboard(val)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,10 @@ export function extractMentions(
|
|||
nodes: mfm.MfmNode[],
|
||||
): mfm.MfmMention["props"][] {
|
||||
// TODO: 重複を削除
|
||||
const mentionNodes = mfm.extract(nodes, (node) => node.type === "mention");
|
||||
const mentionNodes = mfm.extract(
|
||||
nodes,
|
||||
(node) => node.type === "mention",
|
||||
) as mfm.MfmMention[];
|
||||
const mentions = mentionNodes.map((x) => x.props);
|
||||
|
||||
return mentions;
|
||||
|
|
|
@ -15,7 +15,8 @@ const animatedMfm = [
|
|||
export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] {
|
||||
const mfmNodes = mfm.extract(nodes, (node) => {
|
||||
return node.type === "fn" && animatedMfm.includes(node.props.name);
|
||||
});
|
||||
}) as mfm.MfmFn[];
|
||||
// FIXME: mfm type error
|
||||
const mfms = mfmNodes.map((x) => x.props.fn);
|
||||
|
||||
return mfms;
|
||||
|
|
|
@ -14,7 +14,7 @@ export function extractUrlFromMfm(
|
|||
node.type === "url" ||
|
||||
(node.type === "link" && !(respectSilentFlag && node.props.silent))
|
||||
);
|
||||
});
|
||||
}) as (mfm.MfmLink | mfm.MfmUrl)[];
|
||||
const urls: string[] = unique(urlNodes.map((x) => x.props.url));
|
||||
|
||||
return urls.reduce((array, url) => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export function focusPrev(el: Element | null, self = false, scroll = true) {
|
||||
if (el == null) return;
|
||||
// biome-ignore lint/style/noParameterAssign: assign it intentionally
|
||||
if (!self) el = el.previousElementSibling;
|
||||
if (el) {
|
||||
if (el.hasAttribute("tabindex")) {
|
||||
|
@ -14,6 +15,7 @@ export function focusPrev(el: Element | null, self = false, scroll = true) {
|
|||
|
||||
export function focusNext(el: Element | null, self = false, scroll = true) {
|
||||
if (el == null) return;
|
||||
// biome-ignore lint/style/noParameterAssign: assign it intentionally
|
||||
if (!self) el = el.nextElementSibling;
|
||||
if (el) {
|
||||
if (el.hasAttribute("tabindex")) {
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { acct } from "firefish-js";
|
||||
import { host as localHost } from "@/config";
|
||||
|
||||
export async function genSearchQuery(v: any, q: string) {
|
||||
let host: string;
|
||||
let userId: string;
|
||||
if (q.split(" ").some((x) => x.startsWith("@"))) {
|
||||
for (const at of q
|
||||
.split(" ")
|
||||
.filter((x) => x.startsWith("@"))
|
||||
.map((x) => x.slice(1))) {
|
||||
if (at.includes(".")) {
|
||||
if (at === localHost || at === ".") {
|
||||
host = null;
|
||||
} else {
|
||||
host = at;
|
||||
}
|
||||
} else {
|
||||
const user = await v.os
|
||||
.api("users/show", acct.parse(at))
|
||||
.catch((x) => null);
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
} else {
|
||||
// todo: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
query: q
|
||||
.split(" ")
|
||||
.filter((x) => !(x.startsWith("/") || x.startsWith("@")))
|
||||
.join(" "),
|
||||
host,
|
||||
userId,
|
||||
};
|
||||
}
|
|
@ -15,6 +15,7 @@ import { useRouter } from "@/router";
|
|||
import { notePage } from "@/filters/note";
|
||||
import type { NoteTranslation } from "@/types/note";
|
||||
import type { MenuItem } from "@/types/menu";
|
||||
import type { NoteDraft } from "@/types/post-form";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -72,7 +73,7 @@ export function getNoteMenu(props: {
|
|||
});
|
||||
|
||||
os.post({
|
||||
initialNote: appearNote,
|
||||
initialNote: appearNote as NoteDraft,
|
||||
renote: appearNote.renote,
|
||||
reply: appearNote.reply,
|
||||
channel: appearNote.channel,
|
||||
|
@ -83,7 +84,7 @@ export function getNoteMenu(props: {
|
|||
|
||||
async function edit() {
|
||||
os.post({
|
||||
initialNote: appearNote,
|
||||
initialNote: appearNote as NoteDraft,
|
||||
renote: appearNote.renote,
|
||||
reply: appearNote.reply,
|
||||
channel: appearNote.channel,
|
||||
|
|
|
@ -1,385 +1,6 @@
|
|||
// TODO: sharedに置いてバックエンドのと統合したい
|
||||
export const iso639Langs1 = {
|
||||
af: {
|
||||
nativeName: "Afrikaans",
|
||||
},
|
||||
ak: {
|
||||
nativeName: "Tɕɥi",
|
||||
},
|
||||
ar: {
|
||||
nativeName: "العربية",
|
||||
rtl: true,
|
||||
},
|
||||
ay: {
|
||||
nativeName: "Aymar aru",
|
||||
},
|
||||
az: {
|
||||
nativeName: "Azərbaycan dili",
|
||||
},
|
||||
be: {
|
||||
nativeName: "Беларуская",
|
||||
},
|
||||
bg: {
|
||||
nativeName: "Български",
|
||||
},
|
||||
bn: {
|
||||
nativeName: "বাংলা",
|
||||
},
|
||||
br: {
|
||||
nativeName: "Brezhoneg",
|
||||
},
|
||||
bs: {
|
||||
nativeName: "Bosanski",
|
||||
},
|
||||
ca: {
|
||||
nativeName: "Català",
|
||||
},
|
||||
cs: {
|
||||
nativeName: "Čeština",
|
||||
},
|
||||
cy: {
|
||||
nativeName: "Cymraeg",
|
||||
},
|
||||
da: {
|
||||
nativeName: "Dansk",
|
||||
},
|
||||
de: {
|
||||
nativeName: "Deutsch",
|
||||
},
|
||||
el: {
|
||||
nativeName: "Ελληνικά",
|
||||
},
|
||||
en: {
|
||||
nativeName: "English",
|
||||
},
|
||||
eo: {
|
||||
nativeName: "Esperanto",
|
||||
},
|
||||
es: {
|
||||
nativeName: "Español",
|
||||
},
|
||||
et: {
|
||||
nativeName: "eesti keel",
|
||||
},
|
||||
eu: {
|
||||
nativeName: "Euskara",
|
||||
},
|
||||
fa: {
|
||||
nativeName: "فارسی",
|
||||
rtl: true,
|
||||
},
|
||||
ff: {
|
||||
nativeName: "Fulah",
|
||||
},
|
||||
fi: {
|
||||
nativeName: "Suomi",
|
||||
},
|
||||
fo: {
|
||||
nativeName: "Føroyskt",
|
||||
},
|
||||
fr: {
|
||||
nativeName: "Français",
|
||||
},
|
||||
fy: {
|
||||
nativeName: "Frysk",
|
||||
},
|
||||
ga: {
|
||||
nativeName: "Gaeilge",
|
||||
},
|
||||
gd: {
|
||||
nativeName: "Gàidhlig",
|
||||
},
|
||||
gl: {
|
||||
nativeName: "Galego",
|
||||
},
|
||||
gn: {
|
||||
nativeName: "Avañe'ẽ",
|
||||
},
|
||||
gu: {
|
||||
nativeName: "ગુજરાતી",
|
||||
},
|
||||
gv: {
|
||||
nativeName: "Gaelg",
|
||||
},
|
||||
he: {
|
||||
nativeName: "עברית",
|
||||
rtl: true,
|
||||
},
|
||||
hi: {
|
||||
nativeName: "हिन्दी",
|
||||
},
|
||||
hr: {
|
||||
nativeName: "Hrvatski",
|
||||
},
|
||||
ht: {
|
||||
nativeName: "Kreyòl",
|
||||
},
|
||||
hu: {
|
||||
nativeName: "Magyar",
|
||||
},
|
||||
hy: {
|
||||
nativeName: "Հայերեն",
|
||||
},
|
||||
id: {
|
||||
nativeName: "Bahasa Indonesia",
|
||||
},
|
||||
is: {
|
||||
nativeName: "Íslenska",
|
||||
},
|
||||
it: {
|
||||
nativeName: "Italiano",
|
||||
},
|
||||
ja: {
|
||||
nativeName: "日本語",
|
||||
},
|
||||
jv: {
|
||||
nativeName: "Basa Jawa",
|
||||
},
|
||||
ka: {
|
||||
nativeName: "ქართული",
|
||||
},
|
||||
kk: {
|
||||
nativeName: "Қазақша",
|
||||
},
|
||||
kl: {
|
||||
nativeName: "kalaallisut",
|
||||
},
|
||||
km: {
|
||||
nativeName: "ភាសាខ្មែរ",
|
||||
},
|
||||
kn: {
|
||||
nativeName: "ಕನ್ನಡ",
|
||||
},
|
||||
ko: {
|
||||
nativeName: "한국어",
|
||||
},
|
||||
ku: {
|
||||
nativeName: "Kurdî",
|
||||
},
|
||||
kw: {
|
||||
nativeName: "Kernewek",
|
||||
},
|
||||
la: {
|
||||
nativeName: "Latin",
|
||||
},
|
||||
lb: {
|
||||
nativeName: "Lëtzebuergesch",
|
||||
},
|
||||
li: {
|
||||
nativeName: "Lèmbörgs",
|
||||
},
|
||||
lt: {
|
||||
nativeName: "Lietuvių",
|
||||
},
|
||||
lv: {
|
||||
nativeName: "Latviešu",
|
||||
},
|
||||
mg: {
|
||||
nativeName: "Malagasy",
|
||||
},
|
||||
mk: {
|
||||
nativeName: "Македонски",
|
||||
},
|
||||
ml: {
|
||||
nativeName: "മലയാളം",
|
||||
},
|
||||
mn: {
|
||||
nativeName: "Монгол",
|
||||
},
|
||||
mr: {
|
||||
nativeName: "मराठी",
|
||||
},
|
||||
ms: {
|
||||
nativeName: "Bahasa Melayu",
|
||||
},
|
||||
mt: {
|
||||
nativeName: "Malti",
|
||||
},
|
||||
my: {
|
||||
nativeName: "ဗမာစကာ",
|
||||
},
|
||||
no: {
|
||||
nativeName: "Norsk",
|
||||
},
|
||||
nb: {
|
||||
nativeName: "Norsk (bokmål)",
|
||||
},
|
||||
ne: {
|
||||
nativeName: "नेपाली",
|
||||
},
|
||||
nl: {
|
||||
nativeName: "Nederlands",
|
||||
},
|
||||
nn: {
|
||||
nativeName: "Norsk (nynorsk)",
|
||||
},
|
||||
oc: {
|
||||
nativeName: "Occitan",
|
||||
},
|
||||
or: {
|
||||
nativeName: "ଓଡ଼ିଆ",
|
||||
},
|
||||
pa: {
|
||||
nativeName: "ਪੰਜਾਬੀ",
|
||||
},
|
||||
pl: {
|
||||
nativeName: "Polski",
|
||||
},
|
||||
ps: {
|
||||
nativeName: "پښتو",
|
||||
rtl: true,
|
||||
},
|
||||
pt: {
|
||||
nativeName: "Português",
|
||||
},
|
||||
qu: {
|
||||
nativeName: "Qhichwa",
|
||||
},
|
||||
rm: {
|
||||
nativeName: "Rumantsch",
|
||||
},
|
||||
ro: {
|
||||
nativeName: "Română",
|
||||
},
|
||||
ru: {
|
||||
nativeName: "Русский",
|
||||
},
|
||||
sa: {
|
||||
nativeName: "संस्कृतम्",
|
||||
},
|
||||
se: {
|
||||
nativeName: "Davvisámegiella",
|
||||
},
|
||||
sh: {
|
||||
nativeName: "српскохрватски",
|
||||
},
|
||||
si: {
|
||||
nativeName: "සිංහල",
|
||||
},
|
||||
sk: {
|
||||
nativeName: "Slovenčina",
|
||||
},
|
||||
sl: {
|
||||
nativeName: "Slovenščina",
|
||||
},
|
||||
so: {
|
||||
nativeName: "Soomaaliga",
|
||||
},
|
||||
sq: {
|
||||
nativeName: "Shqip",
|
||||
},
|
||||
sr: {
|
||||
nativeName: "Српски",
|
||||
},
|
||||
su: {
|
||||
nativeName: "Basa Sunda",
|
||||
},
|
||||
sv: {
|
||||
nativeName: "Svenska",
|
||||
},
|
||||
sw: {
|
||||
nativeName: "Kiswahili",
|
||||
},
|
||||
ta: {
|
||||
nativeName: "தமிழ்",
|
||||
},
|
||||
te: {
|
||||
nativeName: "తెలుగు",
|
||||
},
|
||||
tg: {
|
||||
nativeName: "забо́ни тоҷикӣ́",
|
||||
},
|
||||
th: {
|
||||
nativeName: "ภาษาไทย",
|
||||
},
|
||||
tr: {
|
||||
nativeName: "Türkçe",
|
||||
},
|
||||
tt: {
|
||||
nativeName: "татарча",
|
||||
},
|
||||
uk: {
|
||||
nativeName: "Українська",
|
||||
},
|
||||
ur: {
|
||||
nativeName: "اردو",
|
||||
rtl: true,
|
||||
},
|
||||
uz: {
|
||||
nativeName: "O'zbek",
|
||||
},
|
||||
vi: {
|
||||
nativeName: "Tiếng Việt",
|
||||
},
|
||||
xh: {
|
||||
nativeName: "isiXhosa",
|
||||
},
|
||||
yi: {
|
||||
nativeName: "ייִדיש",
|
||||
rtl: true,
|
||||
},
|
||||
zh: {
|
||||
nativeName: "中文",
|
||||
},
|
||||
zu: {
|
||||
nativeName: "isiZulu",
|
||||
},
|
||||
};
|
||||
import { langmap as _langmap } from "firefish-js";
|
||||
|
||||
export const iso639Langs3 = {
|
||||
ach: {
|
||||
nativeName: "Lwo",
|
||||
},
|
||||
ady: {
|
||||
nativeName: "Адыгэбзэ",
|
||||
},
|
||||
cak: {
|
||||
nativeName: "Maya Kaqchikel",
|
||||
},
|
||||
chr: {
|
||||
nativeName: "ᏣᎳᎩ (tsalagi)",
|
||||
},
|
||||
dsb: {
|
||||
nativeName: "Dolnoserbšćina",
|
||||
},
|
||||
fil: {
|
||||
nativeName: "Filipino",
|
||||
},
|
||||
hsb: {
|
||||
nativeName: "Hornjoserbšćina",
|
||||
},
|
||||
kab: {
|
||||
nativeName: "Taqbaylit",
|
||||
},
|
||||
mai: {
|
||||
nativeName: "मैथिली, মৈথিলী",
|
||||
},
|
||||
tlh: {
|
||||
nativeName: "tlhIngan-Hol",
|
||||
},
|
||||
tok: {
|
||||
nativeName: "Toki Pona",
|
||||
},
|
||||
yue: {
|
||||
nativeName: "粵語",
|
||||
},
|
||||
nan: {
|
||||
nativeName: "閩南語",
|
||||
},
|
||||
};
|
||||
|
||||
export const langmapNoRegion = Object.assign({}, iso639Langs1, iso639Langs3);
|
||||
|
||||
export const iso639Regional = {
|
||||
"zh-hans": {
|
||||
nativeName: "中文(简体)",
|
||||
},
|
||||
"zh-hant": {
|
||||
nativeName: "中文(繁體)",
|
||||
},
|
||||
};
|
||||
|
||||
export const langmap = Object.assign({}, langmapNoRegion, iso639Regional);
|
||||
export const langmap = _langmap;
|
||||
|
||||
/**
|
||||
* @see https://github.com/komodojp/tinyld/blob/develop/docs/langs.md
|
||||
|
|
|
@ -39,7 +39,7 @@ export function languageContains(
|
|||
) {
|
||||
if (!langCode1 || !langCode2) return false;
|
||||
|
||||
return parentLanguage(langCode2) === langCode1;
|
||||
return langCode1 === langCode2 || parentLanguage(langCode2) === langCode1;
|
||||
}
|
||||
|
||||
export function parentLanguage(langCode: string | null) {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { markRaw, ref } from "vue";
|
||||
import { markRaw } from "vue";
|
||||
import type { ApiTypes, entities } from "firefish-js";
|
||||
import { isSignedIn, me } from "./me";
|
||||
import { Storage } from "./pizzax";
|
||||
import type { NoteVisibility } from "@/types/note";
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
type TODO = any;
|
||||
|
||||
export const postFormActions: {
|
||||
title: string;
|
||||
handler: (from, update) => void | Promise<void>;
|
||||
|
@ -152,7 +155,7 @@ export const defaultStore = markRaw(
|
|||
type: string;
|
||||
size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
|
||||
black: boolean;
|
||||
props: Record<string, any>;
|
||||
props: Record<string, TODO>;
|
||||
}[],
|
||||
},
|
||||
widgets: {
|
||||
|
@ -161,7 +164,7 @@ export const defaultStore = markRaw(
|
|||
name: string;
|
||||
id: string;
|
||||
place: string | null;
|
||||
data: Record<string, any>;
|
||||
data: Record<string, TODO>;
|
||||
}[],
|
||||
},
|
||||
tl: {
|
||||
|
@ -465,109 +468,6 @@ export const defaultStore = markRaw(
|
|||
}),
|
||||
);
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
import ColdStore from "./cold-store";
|
||||
|
||||
const PREFIX = "miux:";
|
||||
|
||||
interface Plugin {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
configData: Record<string, any>;
|
||||
token: string;
|
||||
ast: any[];
|
||||
}
|
||||
|
||||
import darkTheme from "@/themes/d-rosepine.json5";
|
||||
/**
|
||||
* Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
|
||||
*/
|
||||
import lightTheme from "@/themes/l-rosepinedawn.json5";
|
||||
|
||||
export class ColdDeviceStorage {
|
||||
public static default = {
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
syncDeviceDarkMode: true,
|
||||
plugins: [] as Plugin[],
|
||||
mediaVolume: 0.5,
|
||||
vibrate: false,
|
||||
sound_masterVolume: 0.3,
|
||||
sound_note: { type: "none", volume: 0 },
|
||||
sound_noteMy: { type: "syuilo/up", volume: 1 },
|
||||
sound_notification: { type: "syuilo/pope2", volume: 1 },
|
||||
sound_chat: { type: "syuilo/pope1", volume: 1 },
|
||||
sound_chatBg: { type: "syuilo/waon", volume: 1 },
|
||||
sound_antenna: { type: "syuilo/triple", volume: 1 },
|
||||
sound_channel: { type: "syuilo/square-pico", volume: 1 },
|
||||
};
|
||||
|
||||
public static watchers = [];
|
||||
|
||||
public static get<T extends keyof typeof ColdDeviceStorage.default>(
|
||||
key: T,
|
||||
): (typeof ColdDeviceStorage.default)[T] {
|
||||
// TODO: indexedDBにする
|
||||
// ただしその際はnullチェックではなくキー存在チェックにしないとダメ
|
||||
// (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
|
||||
const value = localStorage.getItem(PREFIX + key);
|
||||
if (value == null) {
|
||||
return ColdDeviceStorage.default[key];
|
||||
} else {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static set<T extends keyof typeof ColdDeviceStorage.default>(
|
||||
key: T,
|
||||
value: (typeof ColdDeviceStorage.default)[T],
|
||||
): void {
|
||||
// 呼び出し側のバグ等で undefined が来ることがある
|
||||
// undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
|
||||
if (value === undefined) {
|
||||
console.error(`attempt to store undefined value for key '${key}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(PREFIX + key, JSON.stringify(value));
|
||||
|
||||
for (const watcher of this.watchers) {
|
||||
if (watcher.key === key) watcher.callback(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static watch(key, callback) {
|
||||
this.watchers.push({ key, callback });
|
||||
}
|
||||
|
||||
// TODO: VueのcustomRef使うと良い感じになるかも
|
||||
public static ref<T extends keyof typeof ColdDeviceStorage.default>(key: T) {
|
||||
const v = ColdDeviceStorage.get(key);
|
||||
const r = ref(v);
|
||||
// TODO: このままではwatcherがリークするので開放する方法を考える
|
||||
this.watch(key, (v) => {
|
||||
r.value = v;
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||
* 主にvue場で設定コントロールのmodelとして使う用
|
||||
*/
|
||||
public static makeGetterSetter<
|
||||
K extends keyof typeof ColdDeviceStorage.default,
|
||||
>(key: K) {
|
||||
// TODO: VueのcustomRef使うと良い感じになるかも
|
||||
const valueRef = ColdDeviceStorage.ref(key);
|
||||
return {
|
||||
get: () => {
|
||||
return valueRef.value;
|
||||
},
|
||||
set: (value: unknown) => {
|
||||
const val = value;
|
||||
ColdDeviceStorage.set(key, val);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
export const ColdDeviceStorage = ColdStore;
|
||||
|
|
|
@ -31,6 +31,7 @@ export function reloadStream() {
|
|||
|
||||
isReloading = true;
|
||||
stream.close();
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
|
||||
stream.once("_connected_", () => (isReloading = false));
|
||||
stream.stream.reconnect();
|
||||
isReloading = false;
|
||||
|
|
|
@ -17,7 +17,8 @@ export async function fetchThemes(): Promise<void> {
|
|||
key: "themes",
|
||||
});
|
||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||
} catch (err) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Safely any
|
||||
} catch (err: any) {
|
||||
if (err.code === "NO_SUCH_KEY") return;
|
||||
throw err;
|
||||
}
|
||||
|
|
78
packages/client/src/types/page.ts
Normal file
78
packages/client/src/types/page.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import type { TypeUtils } from "firefish-js";
|
||||
|
||||
export type BasePageContent = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type PageContentTextInput = BasePageContent & {
|
||||
type: "textInput";
|
||||
default: string;
|
||||
};
|
||||
|
||||
export type PageContentTextareaInput = BasePageContent & {
|
||||
type: "textareaInput";
|
||||
default?: string;
|
||||
};
|
||||
|
||||
export type PageContentNumberInput = BasePageContent & {
|
||||
type: "numberInput";
|
||||
default?: number;
|
||||
};
|
||||
|
||||
export type PageContentSwitch = BasePageContent & {
|
||||
type: "switch";
|
||||
default?: boolean;
|
||||
};
|
||||
export type PageContentCounter = BasePageContent & {
|
||||
type: "counter";
|
||||
default?: number;
|
||||
};
|
||||
|
||||
export type PageContentRadioButton = BasePageContent & {
|
||||
type: "radioButton";
|
||||
default?: string;
|
||||
};
|
||||
|
||||
export type PageContentChildren =
|
||||
| PageContentTextInput
|
||||
| PageContentTextareaInput
|
||||
| PageContentNumberInput
|
||||
| PageContentSwitch
|
||||
| PageContentCounter
|
||||
| PageContentRadioButton;
|
||||
|
||||
export type PageContentParent = {
|
||||
type: "parent";
|
||||
children: PageContentChildren[];
|
||||
};
|
||||
|
||||
export type PageContent = PageContentParent | PageContentChildren;
|
||||
|
||||
export type GetPageVar<T extends PageContentChildren> = {
|
||||
name: string;
|
||||
type: TypeUtils.NonUndefinedAble<T["default"]> extends string
|
||||
? "string"
|
||||
: TypeUtils.NonUndefinedAble<T["default"]> extends boolean
|
||||
? "boolean"
|
||||
: TypeUtils.NonUndefinedAble<T["default"]> extends number
|
||||
? "number"
|
||||
: never;
|
||||
value: TypeUtils.NonUndefinedAble<T["default"]>;
|
||||
};
|
||||
|
||||
export type PageVar =
|
||||
| {
|
||||
name: string;
|
||||
type: "string";
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
type: "boolean";
|
||||
value: boolean;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
type: "number";
|
||||
value: number;
|
||||
};
|
5
packages/client/src/types/relation.ts
Normal file
5
packages/client/src/types/relation.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type Predicate<T> = (a: T) => boolean;
|
||||
|
||||
export type Relation<T, U> = (a: T, b: U) => boolean;
|
||||
|
||||
export type EndoRelation<T> = Relation<T, T>;
|
|
@ -17,7 +17,7 @@
|
|||
},
|
||||
"minify": false,
|
||||
"module": {
|
||||
"type": "commonjs",
|
||||
"type": "es6",
|
||||
"strict": true,
|
||||
"resolveFully": true
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"description": "Firefish SDK for JavaScript",
|
||||
"homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/firefish-js",
|
||||
"main": "./built/index.js",
|
||||
"type": "module",
|
||||
"types": "./src/index.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
@ -35,7 +36,7 @@
|
|||
"typescript": "5.4.5"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
"built", "src"
|
||||
],
|
||||
"dependencies": {
|
||||
"eventemitter3": "5.0.1",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Endpoints } from "./api.types";
|
||||
import type { Endpoints } from "./api.types.js";
|
||||
|
||||
const MK_API_ERROR = Symbol();
|
||||
|
||||
|
@ -7,10 +7,12 @@ export type APIError = {
|
|||
code: string;
|
||||
message: string;
|
||||
kind: "client" | "server";
|
||||
info: Record<string, any>;
|
||||
info: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export function isAPIError(reason: any): reason is APIError {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: used it intentially
|
||||
type ExplicitlyUsedAny = any;
|
||||
export function isAPIError(reason: ExplicitlyUsedAny): reason is APIError {
|
||||
return reason[MK_API_ERROR] === true;
|
||||
}
|
||||
|
||||
|
@ -24,7 +26,7 @@ export type FetchLike = (
|
|||
},
|
||||
) => Promise<{
|
||||
status: number;
|
||||
json(): Promise<any>;
|
||||
json(): Promise<ExplicitlyUsedAny>;
|
||||
}>;
|
||||
|
||||
type IsNeverType<T> = [T] extends [never] ? true : false;
|
||||
|
@ -36,7 +38,10 @@ type IsCaseMatched<
|
|||
P extends Endpoints[E]["req"],
|
||||
C extends number,
|
||||
> = IsNeverType<
|
||||
StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>
|
||||
StrictExtract<
|
||||
Endpoints[E]["res"]["$switch"]["$cases"][C],
|
||||
[P, ExplicitlyUsedAny]
|
||||
>
|
||||
> extends false
|
||||
? true
|
||||
: false;
|
||||
|
@ -45,7 +50,10 @@ type GetCaseResult<
|
|||
E extends keyof Endpoints,
|
||||
P extends Endpoints[E]["req"],
|
||||
C extends number,
|
||||
> = StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>[1];
|
||||
> = StrictExtract<
|
||||
Endpoints[E]["res"]["$switch"]["$cases"][C],
|
||||
[P, ExplicitlyUsedAny]
|
||||
>[1];
|
||||
|
||||
export class APIClient {
|
||||
public origin: string;
|
||||
|
@ -70,7 +78,7 @@ export class APIClient {
|
|||
credential?: string | null | undefined,
|
||||
): Promise<
|
||||
Endpoints[E]["res"] extends {
|
||||
$switch: { $cases: [any, any][]; $default: any };
|
||||
$switch: { $cases: [unknown, unknown][]; $default: unknown };
|
||||
}
|
||||
? IsCaseMatched<E, P, 0> extends true
|
||||
? GetCaseResult<E, P, 0>
|
||||
|
|
|
@ -38,7 +38,7 @@ import type {
|
|||
UserList,
|
||||
UserLite,
|
||||
UserSorting,
|
||||
} from "./entities";
|
||||
} from "./entities.js";
|
||||
|
||||
import type * as consts from "./consts";
|
||||
|
||||
|
@ -264,9 +264,9 @@ export type Endpoints = {
|
|||
|
||||
// clips
|
||||
"clips/add-note": { req: TODO; res: TODO };
|
||||
"clips/create": { req: TODO; res: TODO };
|
||||
"clips/create": { req: TODO; res: Clip };
|
||||
"clips/delete": { req: { clipId: Clip["id"] }; res: null };
|
||||
"clips/list": { req: TODO; res: TODO };
|
||||
"clips/list": { req: TODO; res: Clip[] };
|
||||
"clips/notes": { req: TODO; res: TODO };
|
||||
"clips/show": { req: TODO; res: TODO };
|
||||
"clips/update": { req: TODO; res: TODO };
|
||||
|
@ -363,6 +363,16 @@ export type Endpoints = {
|
|||
res: DriveFile[];
|
||||
};
|
||||
|
||||
"email-address/available": {
|
||||
req: {
|
||||
emailAddress: string;
|
||||
};
|
||||
res: {
|
||||
available?: boolean;
|
||||
reason: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
// endpoint
|
||||
endpoint: {
|
||||
req: { endpoint: string };
|
||||
|
@ -739,6 +749,18 @@ export type Endpoints = {
|
|||
};
|
||||
res: Note[];
|
||||
};
|
||||
"notes/thread-muting/create": {
|
||||
req: {
|
||||
noteId: Note["id"];
|
||||
};
|
||||
res: null;
|
||||
};
|
||||
"notes/thread-muting/delete": {
|
||||
req: {
|
||||
noteId: Note["id"];
|
||||
};
|
||||
res: null;
|
||||
};
|
||||
"notes/hybrid-timeline": {
|
||||
req: {
|
||||
limit?: number;
|
||||
|
@ -759,6 +781,12 @@ export type Endpoints = {
|
|||
};
|
||||
res: Note[];
|
||||
};
|
||||
"notes/make-private": {
|
||||
req: {
|
||||
noteId: Note["id"];
|
||||
};
|
||||
res: null;
|
||||
};
|
||||
"notes/mentions": {
|
||||
req: {
|
||||
following?: boolean;
|
||||
|
@ -900,6 +928,16 @@ export type Endpoints = {
|
|||
// promo
|
||||
"promo/read": { req: TODO; res: TODO };
|
||||
|
||||
// release
|
||||
release: {
|
||||
req: null;
|
||||
res: {
|
||||
version: string;
|
||||
notes: string;
|
||||
screenshots: string[];
|
||||
};
|
||||
};
|
||||
|
||||
// request-reset-password
|
||||
"request-reset-password": {
|
||||
req: { username: string; email: string };
|
||||
|
@ -922,8 +960,36 @@ export type Endpoints = {
|
|||
// ck specific
|
||||
"latest-version": { req: NoParams; res: TODO };
|
||||
|
||||
// signin
|
||||
signin: {
|
||||
req: {
|
||||
username: string;
|
||||
password: string;
|
||||
"hcaptcha-response"?: null | string;
|
||||
"g-recaptcha-response"?: null | string;
|
||||
};
|
||||
res:
|
||||
| {
|
||||
id: User["id"];
|
||||
i: string;
|
||||
}
|
||||
| {
|
||||
challenge: string;
|
||||
challengeId: string;
|
||||
securityKeys: {
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
// sw
|
||||
"sw/register": { req: TODO; res: TODO };
|
||||
"sw/unregister": {
|
||||
req: {
|
||||
endpoint: string;
|
||||
};
|
||||
res: null;
|
||||
};
|
||||
|
||||
// username
|
||||
"username/available": {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type * as consts from "./consts";
|
||||
import type * as consts from "./consts.js";
|
||||
import type { Packed } from "./misc/schema.js";
|
||||
|
||||
export type ID = string;
|
||||
export type DateString = string;
|
||||
|
@ -116,6 +117,7 @@ export type MeDetailed = UserDetailed & {
|
|||
preventAiLearning: boolean;
|
||||
receiveAnnouncementEmail: boolean;
|
||||
usePasswordLessLogin: boolean;
|
||||
token: string;
|
||||
[other: string]: any;
|
||||
};
|
||||
|
||||
|
@ -482,7 +484,7 @@ export type Announcement = {
|
|||
imageUrl: string | null;
|
||||
isRead?: boolean;
|
||||
isGoodNews: boolean;
|
||||
showPopUp: boolean;
|
||||
showPopup: boolean;
|
||||
};
|
||||
|
||||
export type Antenna = {
|
||||
|
@ -513,7 +515,7 @@ export type AuthSession = {
|
|||
|
||||
export type Ad = TODO;
|
||||
|
||||
export type Clip = TODO;
|
||||
export type Clip = Packed<"Clip">;
|
||||
|
||||
export type NoteFavorite = {
|
||||
id: ID;
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import * as acct from "./acct";
|
||||
import type { Acct } from "./acct";
|
||||
import { Endpoints } from "./api.types";
|
||||
import type * as ApiTypes from "./api.types";
|
||||
import * as consts from "./consts";
|
||||
import Stream, { Connection } from "./streaming";
|
||||
import * as StreamTypes from "./streaming.types";
|
||||
import type * as TypeUtils from "./type-utils";
|
||||
import * as acct from "./acct.js";
|
||||
import type { Acct } from "./acct.js";
|
||||
import type { Endpoints } from "./api.types.js";
|
||||
import type * as ApiTypes from "./api.types.js";
|
||||
import * as consts from "./consts.js";
|
||||
import Stream, { Connection } from "./streaming.js";
|
||||
import * as StreamTypes from "./streaming.types.js";
|
||||
import type * as TypeUtils from "./type-utils.js";
|
||||
|
||||
import type * as SchemaTypes from "./misc/schema.js";
|
||||
import * as Schema from "./misc/schema.js";
|
||||
|
||||
export {
|
||||
Endpoints,
|
||||
type Endpoints,
|
||||
type ApiTypes,
|
||||
Stream,
|
||||
Connection as ChannelConnection,
|
||||
|
@ -16,6 +19,8 @@ export {
|
|||
acct,
|
||||
type Acct,
|
||||
type TypeUtils,
|
||||
Schema,
|
||||
type SchemaTypes,
|
||||
};
|
||||
|
||||
export const permissions = consts.permissions;
|
||||
|
@ -26,9 +31,12 @@ export const languages = consts.languages;
|
|||
export const ffVisibility = consts.ffVisibility;
|
||||
export const instanceSortParam = consts.instanceSortParam;
|
||||
|
||||
import { langmap, type PostLanguage } from "./misc/langmap.js";
|
||||
export { langmap, type PostLanguage };
|
||||
|
||||
// api extractor not supported yet
|
||||
//export * as api from './api';
|
||||
//export * as entities from './entities';
|
||||
import * as api from "./api";
|
||||
import * as entities from "./entities";
|
||||
import * as api from "./api.js";
|
||||
import * as entities from "./entities.js";
|
||||
export { api, entities };
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// TODO: sharedに置いてバックエンドのと統合したい
|
||||
export const iso639Langs1 = {
|
||||
af: {
|
||||
nativeName: "Afrikaans",
|
241
packages/firefish-js/src/misc/schema.ts
Normal file
241
packages/firefish-js/src/misc/schema.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
import {
|
||||
packedUserLiteSchema,
|
||||
packedUserDetailedNotMeOnlySchema,
|
||||
packedMeDetailedOnlySchema,
|
||||
packedUserDetailedNotMeSchema,
|
||||
packedMeDetailedSchema,
|
||||
packedUserDetailedSchema,
|
||||
packedUserSchema,
|
||||
} from "../schema/user.js";
|
||||
import { packedNoteSchema } from "../schema/note.js";
|
||||
import { packedUserListSchema } from "../schema/user-list.js";
|
||||
import { packedAppSchema } from "../schema/app.js";
|
||||
import { packedMessagingMessageSchema } from "../schema/messaging-message.js";
|
||||
import { packedNotificationSchema } from "../schema/notification.js";
|
||||
import { packedDriveFileSchema } from "../schema/drive-file.js";
|
||||
import { packedDriveFolderSchema } from "../schema/drive-folder.js";
|
||||
import { packedFollowingSchema } from "../schema/following.js";
|
||||
import { packedMutingSchema } from "../schema/muting.js";
|
||||
import { packedRenoteMutingSchema } from "../schema/renote-muting.js";
|
||||
import { packedReplyMutingSchema } from "../schema/reply-muting.js";
|
||||
import { packedBlockingSchema } from "../schema/blocking.js";
|
||||
import { packedNoteReactionSchema } from "../schema/note-reaction.js";
|
||||
import { packedHashtagSchema } from "../schema/hashtag.js";
|
||||
import { packedPageSchema } from "../schema/page.js";
|
||||
import { packedUserGroupSchema } from "../schema/user-group.js";
|
||||
import { packedNoteFavoriteSchema } from "../schema/note-favorite.js";
|
||||
import { packedChannelSchema } from "../schema/channel.js";
|
||||
import { packedAntennaSchema } from "../schema/antenna.js";
|
||||
import { packedClipSchema } from "../schema/clip.js";
|
||||
import { packedFederationInstanceSchema } from "../schema/federation-instance.js";
|
||||
import { packedQueueCountSchema } from "../schema/queue.js";
|
||||
import { packedGalleryPostSchema } from "../schema/gallery-post.js";
|
||||
import { packedEmojiSchema } from "../schema/emoji.js";
|
||||
import { packedNoteEdit } from "../schema/note-edit.js";
|
||||
import { packedNoteFileSchema } from "../schema/note-file.js";
|
||||
import { packedAbuseUserReportSchema } from "../schema/abuse-user-report.js";
|
||||
|
||||
export const refs = {
|
||||
AbuseUserReport: packedAbuseUserReportSchema,
|
||||
UserLite: packedUserLiteSchema,
|
||||
UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
|
||||
MeDetailedOnly: packedMeDetailedOnlySchema,
|
||||
UserDetailedNotMe: packedUserDetailedNotMeSchema,
|
||||
MeDetailed: packedMeDetailedSchema,
|
||||
UserDetailed: packedUserDetailedSchema,
|
||||
User: packedUserSchema,
|
||||
|
||||
UserList: packedUserListSchema,
|
||||
UserGroup: packedUserGroupSchema,
|
||||
App: packedAppSchema,
|
||||
MessagingMessage: packedMessagingMessageSchema,
|
||||
Note: packedNoteSchema,
|
||||
NoteFile: packedNoteFileSchema,
|
||||
NoteEdit: packedNoteEdit,
|
||||
NoteReaction: packedNoteReactionSchema,
|
||||
NoteFavorite: packedNoteFavoriteSchema,
|
||||
Notification: packedNotificationSchema,
|
||||
DriveFile: packedDriveFileSchema,
|
||||
DriveFolder: packedDriveFolderSchema,
|
||||
Following: packedFollowingSchema,
|
||||
Muting: packedMutingSchema,
|
||||
RenoteMuting: packedRenoteMutingSchema,
|
||||
ReplyMuting: packedReplyMutingSchema,
|
||||
Blocking: packedBlockingSchema,
|
||||
Hashtag: packedHashtagSchema,
|
||||
Page: packedPageSchema,
|
||||
Channel: packedChannelSchema,
|
||||
QueueCount: packedQueueCountSchema,
|
||||
Antenna: packedAntennaSchema,
|
||||
Clip: packedClipSchema,
|
||||
FederationInstance: packedFederationInstanceSchema,
|
||||
GalleryPost: packedGalleryPostSchema,
|
||||
Emoji: packedEmojiSchema,
|
||||
};
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: used it intentially
|
||||
type ExplicitlyUsedAny = any;
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<(typeof refs)[x]>;
|
||||
|
||||
type TypeStringef =
|
||||
| "null"
|
||||
| "boolean"
|
||||
| "integer"
|
||||
| "number"
|
||||
| "string"
|
||||
| "array"
|
||||
| "object"
|
||||
| "any";
|
||||
type StringDefToType<T extends TypeStringef> = T extends "null"
|
||||
? null
|
||||
: T extends "boolean"
|
||||
? boolean
|
||||
: T extends "integer"
|
||||
? number
|
||||
: T extends "number"
|
||||
? number
|
||||
: T extends "string"
|
||||
? string | Date
|
||||
: T extends "array"
|
||||
? ReadonlyArray<ExplicitlyUsedAny>
|
||||
: T extends "object"
|
||||
? Record<string, ExplicitlyUsedAny>
|
||||
: ExplicitlyUsedAny;
|
||||
|
||||
// https://swagger.io/specification/?sbsearch=optional#schema-object
|
||||
type OfSchema = {
|
||||
readonly anyOf?: ReadonlyArray<Schema>;
|
||||
readonly oneOf?: ReadonlyArray<Schema>;
|
||||
readonly allOf?: ReadonlyArray<Schema>;
|
||||
};
|
||||
|
||||
export interface Schema extends OfSchema {
|
||||
readonly type?: TypeStringef;
|
||||
readonly nullable?: boolean;
|
||||
readonly optional?: boolean;
|
||||
readonly items?: Schema;
|
||||
readonly properties?: Obj;
|
||||
readonly required?: ReadonlyArray<
|
||||
Extract<keyof NonNullable<this["properties"]>, string>
|
||||
>;
|
||||
readonly description?: string;
|
||||
readonly example?: ExplicitlyUsedAny;
|
||||
readonly format?: string;
|
||||
readonly ref?: keyof typeof refs;
|
||||
readonly enum?: ReadonlyArray<string>;
|
||||
readonly default?:
|
||||
| (this["type"] extends TypeStringef
|
||||
? StringDefToType<this["type"]>
|
||||
: ExplicitlyUsedAny)
|
||||
| null;
|
||||
readonly maxLength?: number;
|
||||
readonly minLength?: number;
|
||||
readonly maximum?: number;
|
||||
readonly minimum?: number;
|
||||
readonly pattern?: string;
|
||||
}
|
||||
|
||||
type RequiredPropertyNames<s extends Obj> = {
|
||||
[K in keyof s]: // K is not optional
|
||||
s[K]["optional"] extends false
|
||||
? K
|
||||
: // K has default value
|
||||
s[K]["default"] extends
|
||||
| null
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Record<string, unknown>
|
||||
? K
|
||||
: never;
|
||||
}[keyof s];
|
||||
|
||||
export type Obj = Record<string, Schema>;
|
||||
|
||||
// https://github.com/misskey-dev/misskey/issues/8535
|
||||
// To avoid excessive stack depth error,
|
||||
// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
|
||||
export type ObjType<
|
||||
s extends Obj,
|
||||
RequiredProps extends keyof s,
|
||||
> = UnionToIntersection<
|
||||
{
|
||||
-readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]>;
|
||||
} & {
|
||||
-readonly [R in RequiredProps]-?: SchemaType<s[R]>;
|
||||
} & {
|
||||
-readonly [P in keyof s]?: SchemaType<s[P]>;
|
||||
}
|
||||
>;
|
||||
|
||||
type NullOrUndefined<p extends Schema, T> =
|
||||
| (p["nullable"] extends true ? null : never)
|
||||
| (p["optional"] extends true ? undefined : never)
|
||||
| T;
|
||||
|
||||
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
||||
// Get intersection from union
|
||||
type UnionToIntersection<U> = (
|
||||
U extends ExplicitlyUsedAny
|
||||
? (k: U) => void
|
||||
: never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never;
|
||||
|
||||
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
|
||||
// To get union, we use `Foo extends ExplicitlyUsedAny ? Hoge<Foo> : never`
|
||||
type UnionSchemaType<
|
||||
a extends readonly ExplicitlyUsedAny[],
|
||||
X extends Schema = a[number],
|
||||
> = X extends ExplicitlyUsedAny ? SchemaType<X> : never;
|
||||
type ArrayUnion<T> = T extends ExplicitlyUsedAny ? Array<T> : never;
|
||||
|
||||
export type SchemaTypeDef<p extends Schema> = p["type"] extends "null"
|
||||
? null
|
||||
: p["type"] extends "integer"
|
||||
? number
|
||||
: p["type"] extends "number"
|
||||
? number
|
||||
: p["type"] extends "string"
|
||||
? p["enum"] extends readonly string[]
|
||||
? p["enum"][number]
|
||||
: p["format"] extends "date-time"
|
||||
? string
|
||||
: // Dateにする??
|
||||
string
|
||||
: p["type"] extends "boolean"
|
||||
? boolean
|
||||
: p["type"] extends "object"
|
||||
? p["ref"] extends keyof typeof refs
|
||||
? Packed<p["ref"]>
|
||||
: p["properties"] extends NonNullable<Obj>
|
||||
? ObjType<p["properties"], NonNullable<p["required"]>[number]>
|
||||
: p["anyOf"] extends ReadonlyArray<Schema>
|
||||
? UnionSchemaType<p["anyOf"]> &
|
||||
Partial<UnionToIntersection<UnionSchemaType<p["anyOf"]>>>
|
||||
: p["allOf"] extends ReadonlyArray<Schema>
|
||||
? UnionToIntersection<UnionSchemaType<p["allOf"]>>
|
||||
: ExplicitlyUsedAny
|
||||
: p["type"] extends "array"
|
||||
? p["items"] extends OfSchema
|
||||
? p["items"]["anyOf"] extends ReadonlyArray<Schema>
|
||||
? UnionSchemaType<NonNullable<p["items"]["anyOf"]>>[]
|
||||
: p["items"]["oneOf"] extends ReadonlyArray<Schema>
|
||||
? ArrayUnion<
|
||||
UnionSchemaType<NonNullable<p["items"]["oneOf"]>>
|
||||
>
|
||||
: p["items"]["allOf"] extends ReadonlyArray<Schema>
|
||||
? UnionToIntersection<
|
||||
UnionSchemaType<NonNullable<p["items"]["allOf"]>>
|
||||
>[]
|
||||
: never
|
||||
: p["items"] extends NonNullable<Schema>
|
||||
? SchemaTypeDef<p["items"]>[]
|
||||
: ExplicitlyUsedAny[]
|
||||
: p["oneOf"] extends ReadonlyArray<Schema>
|
||||
? UnionSchemaType<p["oneOf"]>
|
||||
: ExplicitlyUsedAny;
|
||||
|
||||
export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;
|
|
@ -1,5 +1,3 @@
|
|||
import { config } from "@/config.js";
|
||||
|
||||
export const packedFederationInstanceSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
@ -83,7 +81,7 @@ export const packedFederationInstanceSchema = {
|
|||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
example: config.version,
|
||||
example: "20240424",
|
||||
},
|
||||
openRegistrations: {
|
||||
type: "boolean",
|
|
@ -1,4 +1,4 @@
|
|||
import { langmap } from "@/misc/langmap.js";
|
||||
import { langmap } from "../misc/langmap.js";
|
||||
|
||||
export const packedNoteSchema = {
|
||||
type: "object",
|
||||
|
@ -208,15 +208,5 @@ export const packedNoteSchema = {
|
|||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
myRenoteCount: {
|
||||
type: "number",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
},
|
||||
quoteCount: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
|
@ -1,4 +1,4 @@
|
|||
import { notificationTypes } from "@/types.js";
|
||||
import { notificationTypes } from "../consts.js";
|
||||
|
||||
export const packedNotificationSchema = {
|
||||
type: "object",
|
|
@ -1,6 +1,6 @@
|
|||
import { EventEmitter } from "eventemitter3";
|
||||
import ReconnectingWebsocket from "reconnecting";
|
||||
import type { BroadcastEvents, Channels } from "./streaming.types";
|
||||
import type { BroadcastEvents, Channels } from "./streaming.types.js";
|
||||
|
||||
function autobind(instance: any): void {
|
||||
const prototype = Object.getPrototypeOf(instance);
|
||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
|||
UserGroup,
|
||||
UserLite,
|
||||
} from "./entities";
|
||||
import type { Connection } from "./streaming";
|
||||
import type { Connection } from "./streaming.js";
|
||||
|
||||
type FIXME = any;
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import type { Endpoints } from "./api.types";
|
||||
import type { Endpoints } from "./api.types.js";
|
||||
|
||||
type PropertyOfType<Type, U> = {
|
||||
export type PropertyOfType<Type, U> = {
|
||||
[K in keyof Type]: Type[K] extends U ? K : never;
|
||||
}[keyof Type];
|
||||
|
||||
export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>;
|
||||
|
||||
export type NonUndefinedAble<T> = T extends undefined ? never : T;
|
||||
|
|
|
@ -141,6 +141,9 @@ importers:
|
|||
file-type:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0
|
||||
firefish-js:
|
||||
specifier: workspace:*
|
||||
version: link:../firefish-js
|
||||
fluent-ffmpeg:
|
||||
specifier: 2.1.2
|
||||
version: 2.1.2
|
||||
|
|
Loading…
Reference in a new issue