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:
|
changes:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**/*
|
- packages/backend/**/*
|
||||||
|
- packages/firefish-js/**/*
|
||||||
- packages/megalodon/**/*
|
- packages/megalodon/**/*
|
||||||
when: always
|
when: always
|
||||||
before_script:
|
before_script:
|
||||||
|
@ -115,7 +116,7 @@ test:build:backend_ts_only:
|
||||||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
||||||
script:
|
script:
|
||||||
- pnpm install --frozen-lockfile
|
- 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
|
- pnpm run migrate
|
||||||
|
|
||||||
test:build:client_only:
|
test:build:client_only:
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.0.0",
|
"file-type": "19.0.0",
|
||||||
|
"firefish-js": "workspace:*",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "14.2.1",
|
"got": "14.2.1",
|
||||||
|
|
|
@ -1,234 +1,7 @@
|
||||||
import {
|
// TODO: use firefish-js
|
||||||
packedUserLiteSchema,
|
import { Schema as _Schema } from "firefish-js";
|
||||||
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";
|
|
||||||
|
|
||||||
export const refs = {
|
export const refs = _Schema.refs;
|
||||||
AbuseUserReport: packedAbuseUserReportSchema,
|
export type Packed<T extends keyof typeof refs> = _Schema.Packed<T>;
|
||||||
UserLite: packedUserLiteSchema,
|
export type Schema = _Schema.Schema;
|
||||||
UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
|
export type SchemaType<P extends _Schema.Schema> = _Schema.SchemaType<P>;
|
||||||
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>>;
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ import { UserProfiles } from "@/models/index.js";
|
||||||
import { In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import { config } from "@/config.js";
|
import { config } from "@/config.js";
|
||||||
import { truncate } from "@/misc/truncate.js";
|
import { truncate } from "@/misc/truncate.js";
|
||||||
import { langmap } from "@/misc/langmap.js";
|
import { langmap } from "firefish-js";
|
||||||
import { inspect } from "node:util";
|
import { inspect } from "node:util";
|
||||||
|
|
||||||
export function validateNote(object: any, uri: string) {
|
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 define from "@/server/api/define.js";
|
||||||
import { HOUR, genId } from "backend-rs";
|
import { HOUR, genId } from "backend-rs";
|
||||||
import { getNote } from "@/server/api/common/getters.js";
|
import { getNote } from "@/server/api/common/getters.js";
|
||||||
import { langmap } from "@/misc/langmap.js";
|
import { langmap } from "firefish-js";
|
||||||
import { createScheduledCreateNoteJob } from "@/queue";
|
import { createScheduledCreateNoteJob } from "@/queue/index.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["notes"],
|
tags: ["notes"],
|
||||||
|
|
|
@ -33,7 +33,7 @@ import renderNote from "@/remote/activitypub/renderer/note.js";
|
||||||
import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
||||||
import { deliverToRelays } from "@/services/relay.js";
|
import { deliverToRelays } from "@/services/relay.js";
|
||||||
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||||
import { langmap } from "@/misc/langmap.js";
|
import { langmap } from "firefish-js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["notes"],
|
tags: ["notes"],
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ApiError } from "@/server/api/error.js";
|
import { ApiError } from "@/server/api/error.js";
|
||||||
import { getNote } from "@/server/api/common/getters.js";
|
import { getNote } from "@/server/api/common/getters.js";
|
||||||
import { translate } from "@/misc/translate.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";
|
import define from "@/server/api/define.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
|
|
@ -63,7 +63,7 @@ import { db } from "@/db/postgre.js";
|
||||||
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
||||||
import { redisClient } from "@/db/redis.js";
|
import { redisClient } from "@/db/redis.js";
|
||||||
import { Mutex } from "redis-semaphore";
|
import { Mutex } from "redis-semaphore";
|
||||||
import { langmap } from "@/misc/langmap.js";
|
import { langmap } from "firefish-js";
|
||||||
import Logger from "@/services/logger.js";
|
import Logger from "@/services/logger.js";
|
||||||
import { inspect } from "node:util";
|
import { inspect } from "node:util";
|
||||||
import { toRustObject } from "@/prelude/undefined-to-null.js";
|
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 icon from "@/scripts/icon";
|
||||||
import { del, get, set } from "@/scripts/idb-proxy";
|
import { del, get, set } from "@/scripts/idb-proxy";
|
||||||
import { reloadChannel, unisonReload } from "@/scripts/unison-reload";
|
import { reloadChannel, unisonReload } from "@/scripts/unison-reload";
|
||||||
|
import type { MenuButton, MenuUser } from "./types/menu";
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ export async function signOut() {
|
||||||
waiting();
|
waiting();
|
||||||
localStorage.removeItem("account");
|
localStorage.removeItem("account");
|
||||||
|
|
||||||
await removeAccount(me.id);
|
await removeAccount(me!.id);
|
||||||
|
|
||||||
const accounts = await getAccounts();
|
const accounts = await getAccounts();
|
||||||
|
|
||||||
|
@ -26,12 +27,9 @@ export async function signOut() {
|
||||||
const registration = await navigator.serviceWorker.ready;
|
const registration = await navigator.serviceWorker.ready;
|
||||||
const push = await registration.pushManager.getSubscription();
|
const push = await registration.pushManager.getSubscription();
|
||||||
if (push) {
|
if (push) {
|
||||||
await fetch(`${apiUrl}/sw/unregister`, {
|
await api("sw/unregister", {
|
||||||
method: "POST",
|
endpoint: push.endpoint,
|
||||||
body: JSON.stringify({
|
i: me!.token, // FIXME: This parameter seems to be removable but I didn't test it
|
||||||
i: me.token,
|
|
||||||
endpoint: push.endpoint,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,13 +115,13 @@ function showSuspendedDialog() {
|
||||||
|
|
||||||
export function updateAccount(accountData) {
|
export function updateAccount(accountData) {
|
||||||
for (const [key, value] of Object.entries(accountData)) {
|
for (const [key, value] of Object.entries(accountData)) {
|
||||||
me[key] = value;
|
me![key] = value;
|
||||||
}
|
}
|
||||||
localStorage.setItem("account", JSON.stringify(me));
|
localStorage.setItem("account", JSON.stringify(me));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshAccount() {
|
export async function refreshAccount() {
|
||||||
const accountData = await fetchAccount(me.token);
|
const accountData = await fetchAccount(me!.token);
|
||||||
return updateAccount(accountData);
|
return updateAccount(accountData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +187,7 @@ export async function openAccountMenu(
|
||||||
|
|
||||||
async function switchAccount(account: entities.UserDetailed) {
|
async function switchAccount(account: entities.UserDetailed) {
|
||||||
const storedAccounts = await getAccounts();
|
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);
|
switchAccountWithToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,15 +196,15 @@ export async function openAccountMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedAccounts = await getAccounts().then((accounts) =>
|
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", {
|
const accountsPromise = api("users/show", {
|
||||||
userIds: storedAccounts.map((x) => x.id),
|
userIds: storedAccounts.map((x) => x.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
function createItem(account: entities.UserDetailed) {
|
function createItem(account: entities.UserDetailed): MenuUser {
|
||||||
return {
|
return {
|
||||||
type: "user",
|
type: "user" as const,
|
||||||
user: account,
|
user: account,
|
||||||
active: opts.active != null ? opts.active === account.id : false,
|
active: opts.active != null ? opts.active === account.id : false,
|
||||||
action: () => {
|
action: () => {
|
||||||
|
@ -221,10 +219,14 @@ export async function openAccountMenu(
|
||||||
|
|
||||||
const accountItemPromises = storedAccounts.map(
|
const accountItemPromises = storedAccounts.map(
|
||||||
(a) =>
|
(a) =>
|
||||||
new Promise((res) => {
|
new Promise<MenuUser>((res) => {
|
||||||
accountsPromise.then((accounts) => {
|
accountsPromise.then((accounts) => {
|
||||||
const account = accounts.find((x) => x.id === a.id);
|
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));
|
res(createItem(account));
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@ -233,74 +235,72 @@ export async function openAccountMenu(
|
||||||
if (opts.withExtraOperation) {
|
if (opts.withExtraOperation) {
|
||||||
popupMenu(
|
popupMenu(
|
||||||
[
|
[
|
||||||
...[
|
...(isMobile ?? false
|
||||||
...(isMobile ?? false
|
? [
|
||||||
? [
|
{
|
||||||
{
|
type: "parent" as const,
|
||||||
type: "parent",
|
icon: `${icon("ph-plus")}`,
|
||||||
icon: `${icon("ph-plus")}`,
|
text: i18n.ts.addAccount,
|
||||||
text: i18n.ts.addAccount,
|
children: [
|
||||||
children: [
|
{
|
||||||
{
|
text: i18n.ts.existingAccount,
|
||||||
text: i18n.ts.existingAccount,
|
action: () => {
|
||||||
action: () => {
|
showSigninDialog();
|
||||||
showSigninDialog();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
text: i18n.ts.createAccount,
|
{
|
||||||
action: () => {
|
text: i18n.ts.createAccount,
|
||||||
createAccount();
|
action: () => {
|
||||||
},
|
createAccount();
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
]
|
},
|
||||||
: [
|
]
|
||||||
{
|
: [
|
||||||
type: "link",
|
{
|
||||||
text: i18n.ts.profile,
|
type: "link" as const,
|
||||||
to: `/@${me.username}`,
|
text: i18n.ts.profile,
|
||||||
avatar: me,
|
to: `/@${me!.username}`,
|
||||||
},
|
avatar: me!,
|
||||||
null,
|
},
|
||||||
]),
|
null,
|
||||||
...(opts.includeCurrentAccount ? [createItem(me)] : []),
|
]),
|
||||||
...accountItemPromises,
|
...(opts.includeCurrentAccount ? [createItem(me!)] : []),
|
||||||
...(isMobile ?? false
|
...accountItemPromises,
|
||||||
? [
|
...(isMobile ?? false
|
||||||
null,
|
? [
|
||||||
{
|
null,
|
||||||
type: "link",
|
{
|
||||||
text: i18n.ts.profile,
|
type: "link" as const,
|
||||||
to: `/@${me.username}`,
|
text: i18n.ts.profile,
|
||||||
avatar: me,
|
to: `/@${me!.username}`,
|
||||||
},
|
avatar: me!,
|
||||||
]
|
},
|
||||||
: [
|
]
|
||||||
{
|
: [
|
||||||
type: "parent",
|
{
|
||||||
icon: `${icon("ph-plus")}`,
|
type: "parent" as const,
|
||||||
text: i18n.ts.addAccount,
|
icon: `${icon("ph-plus")}`,
|
||||||
children: [
|
text: i18n.ts.addAccount,
|
||||||
{
|
children: [
|
||||||
text: i18n.ts.existingAccount,
|
{
|
||||||
action: () => {
|
text: i18n.ts.existingAccount,
|
||||||
showSigninDialog();
|
action: () => {
|
||||||
},
|
showSigninDialog();
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
text: i18n.ts.createAccount,
|
{
|
||||||
action: () => {
|
text: i18n.ts.createAccount,
|
||||||
createAccount();
|
action: () => {
|
||||||
},
|
createAccount();
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
]),
|
},
|
||||||
],
|
]),
|
||||||
],
|
],
|
||||||
ev.currentTarget ?? ev.target,
|
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||||
{
|
{
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
|
@ -308,10 +308,10 @@ export async function openAccountMenu(
|
||||||
} else {
|
} else {
|
||||||
popupMenu(
|
popupMenu(
|
||||||
[
|
[
|
||||||
...(opts.includeCurrentAccount ? [createItem(me)] : []),
|
...(opts.includeCurrentAccount ? [createItem(me!)] : []),
|
||||||
...accountItemPromises,
|
...accountItemPromises,
|
||||||
],
|
],
|
||||||
ev.currentTarget ?? ev.target,
|
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||||
{
|
{
|
||||||
align: "left",
|
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>
|
<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.root">
|
||||||
<div :class="$style.title">
|
<div :class="$style.title">
|
||||||
<MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle>
|
<MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle>
|
||||||
|
@ -41,6 +41,10 @@ const props = defineProps<{
|
||||||
announcement: entities.Announcement;
|
announcement: entities.Announcement;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
closed: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const { id, text, title, imageUrl, isGoodNews } = props.announcement;
|
const { id, text, title, imageUrl, isGoodNews } = props.announcement;
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
|
@ -182,7 +182,7 @@ export default {
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: string;
|
type: string;
|
||||||
q: string | null;
|
q: string | null;
|
||||||
textarea: HTMLTextAreaElement;
|
textarea: HTMLTextAreaElement | HTMLInputElement;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
@ -435,7 +435,7 @@ onUpdated(() => {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setPosition();
|
setPosition();
|
||||||
|
|
||||||
props.textarea.addEventListener("keydown", onKeydown);
|
(props.textarea as HTMLTextAreaElement).addEventListener("keydown", onKeydown);
|
||||||
document.body.addEventListener("mousedown", onMousedown);
|
document.body.addEventListener("mousedown", onMousedown);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
@ -453,7 +453,7 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
props.textarea.removeEventListener("keydown", onKeydown);
|
(props.textarea as HTMLTextAreaElement).removeEventListener("keydown", onKeydown);
|
||||||
document.body.removeEventListener("mousedown", onMousedown);
|
document.body.removeEventListener("mousedown", onMousedown);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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.root">
|
||||||
<p :class="$style.title">
|
<p :class="$style.title">
|
||||||
{{ i18n.ts.youHaveUnreadAnnouncements }}
|
{{ i18n.ts.youHaveUnreadAnnouncements }}
|
||||||
|
@ -21,6 +21,10 @@ import MkModal from "@/components/MkModal.vue";
|
||||||
import MkButton from "@/components/MkButton.vue";
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
closed: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
const checkAnnouncements = () => {
|
const checkAnnouncements = () => {
|
||||||
modal.value!.close();
|
modal.value!.close();
|
||||||
|
|
|
@ -852,13 +852,17 @@ function setLanguage() {
|
||||||
actions.push(null);
|
actions.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (language.value != null)
|
if (language.value != null && langmap[language.value] != null) {
|
||||||
actions.push({
|
actions.push({
|
||||||
text: langmap[language.value].nativeName,
|
text: langmap[language.value].nativeName,
|
||||||
danger: false,
|
danger: false,
|
||||||
active: true,
|
active: true,
|
||||||
action: () => {},
|
action: () => {},
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Unrecognized language, set to null
|
||||||
|
language.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
const langs = Object.keys(langmap);
|
const langs = Object.keys(langmap);
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ const hCaptchaResponse = ref(null);
|
||||||
const reCaptchaResponse = ref(null);
|
const reCaptchaResponse = ref(null);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "login", v: any): void;
|
login: [v: { id: string; i: string }];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
@ -30,7 +30,7 @@ withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "done"): void;
|
(ev: "done", res: { id: string; i: string }): void;
|
||||||
(ev: "closed"): void;
|
(ev: "closed"): void;
|
||||||
(ev: "cancelled"): void;
|
(ev: "cancelled"): void;
|
||||||
}>();
|
}>();
|
||||||
|
@ -39,11 +39,11 @@ const dialog = ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
function onClose() {
|
function onClose() {
|
||||||
emit("cancelled");
|
emit("cancelled");
|
||||||
dialog.value.close();
|
dialog.value!.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLogin(res) {
|
function onLogin(res: { id: string; i: string }) {
|
||||||
emit("done", res);
|
emit("done", res);
|
||||||
dialog.value.close();
|
dialog.value!.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -248,7 +248,7 @@
|
||||||
v-model="hCaptchaResponse"
|
v-model="hCaptchaResponse"
|
||||||
class="_formBlock captcha"
|
class="_formBlock captcha"
|
||||||
provider="hcaptcha"
|
provider="hcaptcha"
|
||||||
:sitekey="instance.hcaptchaSiteKey"
|
:sitekey="instance.hcaptchaSiteKey!"
|
||||||
/>
|
/>
|
||||||
<MkCaptcha
|
<MkCaptcha
|
||||||
v-if="instance.enableRecaptcha"
|
v-if="instance.enableRecaptcha"
|
||||||
|
@ -256,7 +256,7 @@
|
||||||
v-model="reCaptchaResponse"
|
v-model="reCaptchaResponse"
|
||||||
class="_formBlock captcha"
|
class="_formBlock captcha"
|
||||||
provider="recaptcha"
|
provider="recaptcha"
|
||||||
:sitekey="instance.recaptchaSiteKey"
|
:sitekey="instance.recaptchaSiteKey!"
|
||||||
/>
|
/>
|
||||||
<MkButton
|
<MkButton
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
|
@ -296,7 +296,7 @@ const props = withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "signup", user: Record<string, any>): void;
|
(ev: "signup", user: { id: string; i: string }): void;
|
||||||
(ev: "signupEmailPending"): void;
|
(ev: "signupEmailPending"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -36,13 +36,13 @@ withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "done"): void;
|
(ev: "done", res: { id: string; i: string }): void;
|
||||||
(ev: "closed"): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
function onSignup(res) {
|
function onSignup(res: { id: string; i: string }) {
|
||||||
emit("done", res);
|
emit("done", res);
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<MkModal
|
<MkModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:z-priority="'middle'"
|
:z-priority="'middle'"
|
||||||
@click="$refs.modal.close()"
|
@click="modal!.close()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.title">
|
<div :class="$style.title">
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
:class="$style.gotIt"
|
:class="$style.gotIt"
|
||||||
primary
|
primary
|
||||||
full
|
full
|
||||||
@click="$refs.modal.close()"
|
@click="modal!.close()"
|
||||||
>{{ i18n.ts.gotIt }}</MkButton
|
>{{ i18n.ts.gotIt }}</MkButton
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
import { locale } from "@/config";
|
import { locale } from "@/config";
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: temporary use any
|
||||||
class I18n<T extends Record<string, any>> {
|
class I18n<T extends Record<string, any>> {
|
||||||
public ts: T;
|
public ts: T;
|
||||||
|
|
||||||
|
|
|
@ -245,7 +245,12 @@ function checkForSplash() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 変なバージョン文字列来るとcompareVersionsでエラーになるため
|
// 変なバージョン文字列来ると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) {
|
if (me) {
|
||||||
popup(
|
popup(
|
||||||
|
@ -281,7 +286,7 @@ function checkForSplash() {
|
||||||
"closed",
|
"closed",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
unreadAnnouncements.forEach((item) => {
|
for (const item of unreadAnnouncements) {
|
||||||
if (item.showPopup)
|
if (item.showPopup)
|
||||||
popup(
|
popup(
|
||||||
defineAsyncComponent(
|
defineAsyncComponent(
|
||||||
|
@ -291,7 +296,7 @@ function checkForSplash() {
|
||||||
{},
|
{},
|
||||||
"closed",
|
"closed",
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// NIRAX --- A lightweight router
|
// NIRAX --- A lightweight router
|
||||||
|
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from "eventemitter3";
|
||||||
import type { Component, ShallowRef } from "vue";
|
import type { Component } from "vue";
|
||||||
import { shallowRef } from "vue";
|
import { shallowRef } from "vue";
|
||||||
import { safeURIDecode } from "@/scripts/safe-uri-decode";
|
import { safeURIDecode } from "@/scripts/safe-uri-decode";
|
||||||
import { pleaseLogin } from "@/scripts/please-login";
|
import { pleaseLogin } from "@/scripts/please-login";
|
||||||
|
@ -36,6 +36,7 @@ export interface Resolved {
|
||||||
function parsePath(path: string): ParsedPath {
|
function parsePath(path: string): ParsedPath {
|
||||||
const res = [] as ParsedPath;
|
const res = [] as ParsedPath;
|
||||||
|
|
||||||
|
// biome-ignore lint/style/noParameterAssign: assign it intentionally
|
||||||
path = path.substring(1);
|
path = path.substring(1);
|
||||||
|
|
||||||
for (const part of path.split("/")) {
|
for (const part of path.split("/")) {
|
||||||
|
@ -76,13 +77,13 @@ export class Router extends EventEmitter<{
|
||||||
same: () => void;
|
same: () => void;
|
||||||
}> {
|
}> {
|
||||||
private routes: RouteDef[];
|
private routes: RouteDef[];
|
||||||
public current: Resolved;
|
public current!: Resolved; // It is assigned in this.navigate
|
||||||
public currentRef: ShallowRef<Resolved> = shallowRef();
|
public currentRef = shallowRef<Resolved>();
|
||||||
public currentRoute: ShallowRef<RouteDef> = shallowRef();
|
public currentRoute = shallowRef<RouteDef>();
|
||||||
private currentPath: string;
|
private currentPath: string;
|
||||||
private currentKey = Date.now().toString();
|
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"]) {
|
constructor(routes: Router["routes"], currentPath: Router["currentPath"]) {
|
||||||
super();
|
super();
|
||||||
|
@ -92,9 +93,10 @@ export class Router extends EventEmitter<{
|
||||||
this.navigate(currentPath, null, false);
|
this.navigate(currentPath, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public resolve(path: string): Resolved | null {
|
public resolve(_path: string): Resolved | null {
|
||||||
let queryString: string | null = null;
|
let queryString: string | null = null;
|
||||||
let hash: string | null = null;
|
let hash: string | null = null;
|
||||||
|
let path = _path;
|
||||||
if (path[0] === "/") path = path.substring(1);
|
if (path[0] === "/") path = path.substring(1);
|
||||||
if (path.includes("#")) {
|
if (path.includes("#")) {
|
||||||
hash = path.substring(path.indexOf("#") + 1);
|
hash = path.substring(path.indexOf("#") + 1);
|
||||||
|
@ -168,9 +170,16 @@ export class Router extends EventEmitter<{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query != null && queryString != null) {
|
if (route.query != null && queryString != null) {
|
||||||
const queryObject = [
|
// const queryObject = [
|
||||||
...new URLSearchParams(queryString).entries(),
|
// ...new URLSearchParams(queryString).entries(),
|
||||||
].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
|
// ].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) {
|
for (const q in route.query) {
|
||||||
const as = route.query[q];
|
const as = route.query[q];
|
||||||
|
@ -227,6 +236,7 @@ export class Router extends EventEmitter<{
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSamePath = beforePath === path;
|
const isSamePath = beforePath === path;
|
||||||
|
// biome-ignore lint/style/noParameterAssign: assign it intentionally
|
||||||
if (isSamePath && key == null) key = this.currentKey;
|
if (isSamePath && key == null) key = this.currentKey;
|
||||||
this.current = res;
|
this.current = res;
|
||||||
this.currentRef.value = res;
|
this.currentRef.value = res;
|
||||||
|
@ -253,7 +263,7 @@ export class Router extends EventEmitter<{
|
||||||
return this.currentKey;
|
return this.currentKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public push(path: string, flag?: any) {
|
public push(path: string, flag?: unknown) {
|
||||||
const beforePath = this.currentPath;
|
const beforePath = this.currentPath;
|
||||||
if (path === beforePath) {
|
if (path === beforePath) {
|
||||||
this.emit("same");
|
this.emit("same");
|
||||||
|
|
|
@ -896,9 +896,6 @@ export async function openEmojiPicker(
|
||||||
...opts,
|
...opts,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
chosen: (emoji) => {
|
|
||||||
insertTextAtCursor(activeTextarea, emoji);
|
|
||||||
},
|
|
||||||
done: (emoji) => {
|
done: (emoji) => {
|
||||||
insertTextAtCursor(activeTextarea, emoji);
|
insertTextAtCursor(activeTextarea, emoji);
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
i18n.ts._accountDelete.sendEmail
|
i18n.ts._accountDelete.sendEmail
|
||||||
}}</FormInfo>
|
}}</FormInfo>
|
||||||
<FormButton
|
<FormButton
|
||||||
v-if="!me?.isDeleted"
|
v-if="!me!.isDeleted"
|
||||||
danger
|
danger
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
@click="deleteAccount"
|
@click="deleteAccount"
|
||||||
|
|
|
@ -5,12 +5,13 @@ import { onUnmounted, ref, watch } from "vue";
|
||||||
import { api } from "./os";
|
import { api } from "./os";
|
||||||
import { useStream } from "./stream";
|
import { useStream } from "./stream";
|
||||||
import { isSignedIn, me } from "@/me";
|
import { isSignedIn, me } from "@/me";
|
||||||
|
import type { TypeUtils } from "firefish-js";
|
||||||
|
|
||||||
type StateDef = Record<
|
type StateDef = Record<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
where: "account" | "device" | "deviceAccount";
|
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)) {
|
for (const [k, v] of Object.entries(state)) {
|
||||||
reactiveState[k] = ref(v);
|
reactiveState[k] = ref(v);
|
||||||
}
|
}
|
||||||
this.state = state as any;
|
this.state = state as typeof this.state;
|
||||||
this.reactiveState = reactiveState as any;
|
this.reactiveState = reactiveState as typeof this.reactiveState;
|
||||||
|
|
||||||
if (isSignedIn(me)) {
|
if (isSignedIn(me)) {
|
||||||
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
|
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
|
||||||
|
// For some reason, if I don't setTimeout, an error occurs in the api function (probably caused by circular references)
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
api("i/registry/get-all", { scope: ["client", this.key] }).then(
|
api("i/registry/get-all", { scope: ["client", this.key] }).then(
|
||||||
(kvs) => {
|
(kvs) => {
|
||||||
|
@ -104,7 +106,7 @@ export class Storage<T extends StateDef> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
`${this.keyForLocalStorage}::cache::${me.id}`,
|
`${this.keyForLocalStorage}::cache::${me!.id}`,
|
||||||
JSON.stringify(cache),
|
JSON.stringify(cache),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -118,11 +120,12 @@ export class Storage<T extends StateDef> {
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
scope: string[];
|
scope?: string[];
|
||||||
key: keyof T;
|
key: keyof T;
|
||||||
value: T[typeof key]["default"];
|
value: T[typeof key]["default"];
|
||||||
}) => {
|
}) => {
|
||||||
if (
|
if (
|
||||||
|
scope == null ||
|
||||||
scope.length !== 2 ||
|
scope.length !== 2 ||
|
||||||
scope[0] !== "client" ||
|
scope[0] !== "client" ||
|
||||||
scope[1] !== this.key ||
|
scope[1] !== this.key ||
|
||||||
|
@ -135,13 +138,13 @@ export class Storage<T extends StateDef> {
|
||||||
|
|
||||||
const cache = JSON.parse(
|
const cache = JSON.parse(
|
||||||
localStorage.getItem(
|
localStorage.getItem(
|
||||||
`${this.keyForLocalStorage}::cache::${me.id}`,
|
`${this.keyForLocalStorage}::cache::${me!.id}`,
|
||||||
) || "{}",
|
) || "{}",
|
||||||
);
|
);
|
||||||
if (cache[key] !== value) {
|
if (cache[key] !== value) {
|
||||||
cache[key] = value;
|
cache[key] = value;
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
`${this.keyForLocalStorage}::cache::${me.id}`,
|
`${this.keyForLocalStorage}::cache::${me!.id}`,
|
||||||
JSON.stringify(cache),
|
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);
|
if (_DEV_) console.log("set", key, value);
|
||||||
|
|
||||||
this.state[key] = value;
|
this.state[key] = value;
|
||||||
|
@ -201,15 +204,15 @@ export class Storage<T extends StateDef> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public push<K extends keyof T>(
|
public push<K extends TypeUtils.PropertyOfType<T, { default: unknown[] }>>(
|
||||||
key: K,
|
key: K & string,
|
||||||
value: ArrayElement<T[K]["default"]>,
|
value: ArrayElement<T[K]["default"]>,
|
||||||
): void {
|
): void {
|
||||||
const currentState = this.state[key];
|
const currentState = this.state[key] as unknown[];
|
||||||
this.set(key, [...currentState, value]);
|
this.set(key, [...currentState, value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset(key: keyof T) {
|
public reset(key: keyof T & string) {
|
||||||
this.set(key, this.def[key].default);
|
this.set(key, this.def[key].default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,11 +221,11 @@ export class Storage<T extends StateDef> {
|
||||||
* 主にvue場で設定コントロールのmodelとして使う用
|
* 主にvue場で設定コントロールのmodelとして使う用
|
||||||
*/
|
*/
|
||||||
public makeGetterSetter<K extends keyof T>(
|
public makeGetterSetter<K extends keyof T>(
|
||||||
key: K,
|
key: K & string,
|
||||||
getter?: (v: T[K]) => unknown,
|
getter?: (oldV: T[K]["default"]) => T[K]["default"],
|
||||||
setter?: (v: unknown) => T[K],
|
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) => {
|
const stop = watch(this.reactiveState[key], (val) => {
|
||||||
valueRef.value = val;
|
valueRef.value = val;
|
||||||
|
@ -242,7 +245,7 @@ export class Storage<T extends StateDef> {
|
||||||
return valueRef.value;
|
return valueRef.value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set: (value: unknown) => {
|
set: (value: T[K]["default"]) => {
|
||||||
const val = setter ? setter(value) : value;
|
const val = setter ? setter(value) : value;
|
||||||
this.set(key, val);
|
this.set(key, val);
|
||||||
valueRef.value = val;
|
valueRef.value = val;
|
||||||
|
|
|
@ -42,7 +42,20 @@ export function install(plugin) {
|
||||||
aiscript.exec(parser.parse(plugin.src));
|
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>();
|
const config = new Map<string, values.Value>();
|
||||||
for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
|
for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
|
||||||
config.set(
|
config.set(
|
||||||
|
@ -172,7 +185,7 @@ function registerNoteAction({ pluginId, title, handler }) {
|
||||||
if (!pluginContext) {
|
if (!pluginContext) {
|
||||||
return;
|
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 {
|
function registerPageViewInterruptor({ pluginId, handler }): void {
|
||||||
pageViewInterruptors.push({
|
// pageViewInterruptors.push({
|
||||||
handler: async (page) => {
|
// handler: async (page) => {
|
||||||
const pluginContext = pluginContexts.get(pluginId);
|
// const pluginContext = pluginContexts.get(pluginId);
|
||||||
if (!pluginContext) {
|
// if (!pluginContext) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
return utils.valToJs(
|
// return utils.valToJs(
|
||||||
await pluginContext.execFn(handler, [utils.jsToVal(page)]),
|
// await pluginContext.execFn(handler, [utils.jsToVal(page)]),
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function byteify(string: string, encoding: "ascii" | "base64" | "hex") {
|
||||||
);
|
);
|
||||||
case "hex":
|
case "hex":
|
||||||
return new Uint8Array(
|
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
|
* 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
|
* Returns the longest prefix of elements that satisfy the predicate
|
||||||
*/
|
*/
|
||||||
export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
|
export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
|
||||||
const ys = [];
|
const ys: T[] = [];
|
||||||
for (const x of xs) {
|
for (const x of xs) {
|
||||||
if (f(x)) {
|
if (f(x)) {
|
||||||
ys.push(x);
|
ys.push(x);
|
||||||
|
|
|
@ -13,7 +13,7 @@ export class Autocomplete {
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
private textarea: HTMLInputElement | HTMLTextAreaElement;
|
private textarea: HTMLInputElement | HTMLTextAreaElement;
|
||||||
private currentType: string;
|
private currentType?: string;
|
||||||
private textRef: Ref<string>;
|
private textRef: Ref<string>;
|
||||||
private opening: boolean;
|
private opening: boolean;
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export class Autocomplete {
|
||||||
* テキスト入力時
|
* テキスト入力時
|
||||||
*/
|
*/
|
||||||
private onInput() {
|
private onInput() {
|
||||||
const caretPos = this.textarea.selectionStart;
|
const caretPos = this.textarea.selectionStart!;
|
||||||
const text = this.text.substring(0, caretPos).split("\n").pop()!;
|
const text = this.text.substring(0, caretPos).split("\n").pop()!;
|
||||||
|
|
||||||
const mentionIndex = text.lastIndexOf("@");
|
const mentionIndex = text.lastIndexOf("@");
|
||||||
|
@ -147,10 +147,10 @@ export class Autocomplete {
|
||||||
this.opening = true;
|
this.opening = true;
|
||||||
this.currentType = type;
|
this.currentType = type;
|
||||||
|
|
||||||
// #region サジェストを表示すべき位置を計算
|
// #region Calculate the position where suggestions should be displayed
|
||||||
const caretPosition = getCaretCoordinates(
|
const caretPosition = getCaretCoordinates(
|
||||||
this.textarea,
|
this.textarea,
|
||||||
this.textarea.selectionStart,
|
this.textarea.selectionStart!,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rect = this.textarea.getBoundingClientRect();
|
const rect = this.textarea.getBoundingClientRect();
|
||||||
|
@ -216,7 +216,7 @@ export class Autocomplete {
|
||||||
private complete({ type, value }) {
|
private complete({ type, value }) {
|
||||||
this.close();
|
this.close();
|
||||||
|
|
||||||
const caret = this.textarea.selectionStart;
|
const caret = this.textarea.selectionStart!;
|
||||||
|
|
||||||
if (type === "user") {
|
if (type === "user") {
|
||||||
const source = this.text;
|
const source = this.text;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { entities } from "firefish-js";
|
import type { entities } from "firefish-js";
|
||||||
|
import { detectLanguage, languageContains } from "./language-utils";
|
||||||
|
|
||||||
export interface Muted {
|
export interface Muted {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
@ -12,11 +13,12 @@ function checkLangMute(
|
||||||
note: entities.Note,
|
note: entities.Note,
|
||||||
mutedLangs: Array<string | string[]>,
|
mutedLangs: Array<string | string[]>,
|
||||||
): Muted {
|
): Muted {
|
||||||
const mutedLangList = new Set(
|
const mutedLangList = mutedLangs.flat();
|
||||||
mutedLangs.reduce((arr, x) => [...arr, ...(Array.isArray(x) ? x : [x])]),
|
const noteLang = note.lang ?? detectLanguage(note.text ?? "") ?? "no-lang";
|
||||||
);
|
for (const mutedLang of mutedLangList) {
|
||||||
if (mutedLangList.has((note.lang?.[0]?.lang || "").split("-")[0])) {
|
if (languageContains(mutedLang, noteLang)) {
|
||||||
return { muted: true, matched: [note.lang?.[0]?.lang] };
|
return { muted: true, matched: [noteLang] };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return NotMuted;
|
return NotMuted;
|
||||||
}
|
}
|
||||||
|
@ -32,7 +34,7 @@ function checkWordMute(
|
||||||
|
|
||||||
if (text === "") return NotMuted;
|
if (text === "") return NotMuted;
|
||||||
|
|
||||||
const result = { muted: false, matched: [] };
|
const result = { muted: false, matched: [] as string[] };
|
||||||
|
|
||||||
for (const mutePattern of mutedWords) {
|
for (const mutePattern of mutedWords) {
|
||||||
if (Array.isArray(mutePattern)) {
|
if (Array.isArray(mutePattern)) {
|
||||||
|
@ -74,7 +76,7 @@ function checkWordMute(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWordSoftMute(
|
export function getWordSoftMute(
|
||||||
note: firefish.entities.Note,
|
note: entities.Note,
|
||||||
meId: string | null | undefined,
|
meId: string | null | undefined,
|
||||||
mutedWords: Array<string | string[]>,
|
mutedWords: Array<string | string[]>,
|
||||||
mutedLangs: Array<string | string[]>,
|
mutedLangs: Array<string | string[]>,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
export function collectPageVars(content) {
|
import type { PageContent, PageVar } from "@/types/page";
|
||||||
const pageVars = [];
|
|
||||||
const collect = (xs: any[]) => {
|
export function collectPageVars(content: PageContent[]) {
|
||||||
|
const pageVars: PageVar[] = [];
|
||||||
|
const collect = (xs: PageContent[]) => {
|
||||||
for (const x of xs) {
|
for (const x of xs) {
|
||||||
if (x.type === "textInput") {
|
if (x.type === "textInput") {
|
||||||
pageVars.push({
|
pageVars.push({
|
||||||
|
@ -24,7 +26,7 @@ export function collectPageVars(content) {
|
||||||
pageVars.push({
|
pageVars.push({
|
||||||
name: x.name,
|
name: x.name,
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: x.default,
|
value: x.default!,
|
||||||
});
|
});
|
||||||
} else if (x.type === "counter") {
|
} else if (x.type === "counter") {
|
||||||
pageVars.push({
|
pageVars.push({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Clipboardに値をコピー(TODO: 文字列以外も対応)
|
* Clipboardに値をコピー(TODO: 文字列以外も対応)
|
||||||
*/
|
*/
|
||||||
export default (val) => {
|
function obsoleteCopyToClipboard(val: string) {
|
||||||
// 空div 生成
|
// 空div 生成
|
||||||
const tmp = document.createElement("div");
|
const tmp = document.createElement("div");
|
||||||
// 選択用のタグ生成
|
// 選択用のタグ生成
|
||||||
|
@ -21,7 +21,7 @@ export default (val) => {
|
||||||
// body に追加
|
// body に追加
|
||||||
document.body.appendChild(tmp);
|
document.body.appendChild(tmp);
|
||||||
// 要素を選択
|
// 要素を選択
|
||||||
document.getSelection().selectAllChildren(tmp);
|
document.getSelection()?.selectAllChildren(tmp);
|
||||||
|
|
||||||
// クリップボードにコピー
|
// クリップボードにコピー
|
||||||
const result = document.execCommand("copy");
|
const result = document.execCommand("copy");
|
||||||
|
@ -30,4 +30,20 @@ export default (val) => {
|
||||||
document.body.removeChild(tmp);
|
document.body.removeChild(tmp);
|
||||||
|
|
||||||
return result;
|
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[],
|
nodes: mfm.MfmNode[],
|
||||||
): mfm.MfmMention["props"][] {
|
): mfm.MfmMention["props"][] {
|
||||||
// TODO: 重複を削除
|
// 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);
|
const mentions = mentionNodes.map((x) => x.props);
|
||||||
|
|
||||||
return mentions;
|
return mentions;
|
||||||
|
|
|
@ -15,7 +15,8 @@ const animatedMfm = [
|
||||||
export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] {
|
export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] {
|
||||||
const mfmNodes = mfm.extract(nodes, (node) => {
|
const mfmNodes = mfm.extract(nodes, (node) => {
|
||||||
return node.type === "fn" && animatedMfm.includes(node.props.name);
|
return node.type === "fn" && animatedMfm.includes(node.props.name);
|
||||||
});
|
}) as mfm.MfmFn[];
|
||||||
|
// FIXME: mfm type error
|
||||||
const mfms = mfmNodes.map((x) => x.props.fn);
|
const mfms = mfmNodes.map((x) => x.props.fn);
|
||||||
|
|
||||||
return mfms;
|
return mfms;
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function extractUrlFromMfm(
|
||||||
node.type === "url" ||
|
node.type === "url" ||
|
||||||
(node.type === "link" && !(respectSilentFlag && node.props.silent))
|
(node.type === "link" && !(respectSilentFlag && node.props.silent))
|
||||||
);
|
);
|
||||||
});
|
}) as (mfm.MfmLink | mfm.MfmUrl)[];
|
||||||
const urls: string[] = unique(urlNodes.map((x) => x.props.url));
|
const urls: string[] = unique(urlNodes.map((x) => x.props.url));
|
||||||
|
|
||||||
return urls.reduce((array, url) => {
|
return urls.reduce((array, url) => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export function focusPrev(el: Element | null, self = false, scroll = true) {
|
export function focusPrev(el: Element | null, self = false, scroll = true) {
|
||||||
if (el == null) return;
|
if (el == null) return;
|
||||||
|
// biome-ignore lint/style/noParameterAssign: assign it intentionally
|
||||||
if (!self) el = el.previousElementSibling;
|
if (!self) el = el.previousElementSibling;
|
||||||
if (el) {
|
if (el) {
|
||||||
if (el.hasAttribute("tabindex")) {
|
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) {
|
export function focusNext(el: Element | null, self = false, scroll = true) {
|
||||||
if (el == null) return;
|
if (el == null) return;
|
||||||
|
// biome-ignore lint/style/noParameterAssign: assign it intentionally
|
||||||
if (!self) el = el.nextElementSibling;
|
if (!self) el = el.nextElementSibling;
|
||||||
if (el) {
|
if (el) {
|
||||||
if (el.hasAttribute("tabindex")) {
|
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 { notePage } from "@/filters/note";
|
||||||
import type { NoteTranslation } from "@/types/note";
|
import type { NoteTranslation } from "@/types/note";
|
||||||
import type { MenuItem } from "@/types/menu";
|
import type { MenuItem } from "@/types/menu";
|
||||||
|
import type { NoteDraft } from "@/types/post-form";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ export function getNoteMenu(props: {
|
||||||
});
|
});
|
||||||
|
|
||||||
os.post({
|
os.post({
|
||||||
initialNote: appearNote,
|
initialNote: appearNote as NoteDraft,
|
||||||
renote: appearNote.renote,
|
renote: appearNote.renote,
|
||||||
reply: appearNote.reply,
|
reply: appearNote.reply,
|
||||||
channel: appearNote.channel,
|
channel: appearNote.channel,
|
||||||
|
@ -83,7 +84,7 @@ export function getNoteMenu(props: {
|
||||||
|
|
||||||
async function edit() {
|
async function edit() {
|
||||||
os.post({
|
os.post({
|
||||||
initialNote: appearNote,
|
initialNote: appearNote as NoteDraft,
|
||||||
renote: appearNote.renote,
|
renote: appearNote.renote,
|
||||||
reply: appearNote.reply,
|
reply: appearNote.reply,
|
||||||
channel: appearNote.channel,
|
channel: appearNote.channel,
|
||||||
|
|
|
@ -1,385 +1,6 @@
|
||||||
// TODO: sharedに置いてバックエンドのと統合したい
|
import { langmap as _langmap } from "firefish-js";
|
||||||
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",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const iso639Langs3 = {
|
export const langmap = _langmap;
|
||||||
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);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://github.com/komodojp/tinyld/blob/develop/docs/langs.md
|
* @see https://github.com/komodojp/tinyld/blob/develop/docs/langs.md
|
||||||
|
|
|
@ -39,7 +39,7 @@ export function languageContains(
|
||||||
) {
|
) {
|
||||||
if (!langCode1 || !langCode2) return false;
|
if (!langCode1 || !langCode2) return false;
|
||||||
|
|
||||||
return parentLanguage(langCode2) === langCode1;
|
return langCode1 === langCode2 || parentLanguage(langCode2) === langCode1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parentLanguage(langCode: string | null) {
|
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 type { ApiTypes, entities } from "firefish-js";
|
||||||
import { isSignedIn, me } from "./me";
|
import { isSignedIn, me } from "./me";
|
||||||
import { Storage } from "./pizzax";
|
import { Storage } from "./pizzax";
|
||||||
import type { NoteVisibility } from "@/types/note";
|
import type { NoteVisibility } from "@/types/note";
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
type TODO = any;
|
||||||
|
|
||||||
export const postFormActions: {
|
export const postFormActions: {
|
||||||
title: string;
|
title: string;
|
||||||
handler: (from, update) => void | Promise<void>;
|
handler: (from, update) => void | Promise<void>;
|
||||||
|
@ -152,7 +155,7 @@ export const defaultStore = markRaw(
|
||||||
type: string;
|
type: string;
|
||||||
size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
|
size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
|
||||||
black: boolean;
|
black: boolean;
|
||||||
props: Record<string, any>;
|
props: Record<string, TODO>;
|
||||||
}[],
|
}[],
|
||||||
},
|
},
|
||||||
widgets: {
|
widgets: {
|
||||||
|
@ -161,7 +164,7 @@ export const defaultStore = markRaw(
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
place: string | null;
|
place: string | null;
|
||||||
data: Record<string, any>;
|
data: Record<string, TODO>;
|
||||||
}[],
|
}[],
|
||||||
},
|
},
|
||||||
tl: {
|
tl: {
|
||||||
|
@ -465,109 +468,6 @@ export const defaultStore = markRaw(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
import ColdStore from "./cold-store";
|
||||||
|
|
||||||
const PREFIX = "miux:";
|
export const ColdDeviceStorage = ColdStore;
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ export function reloadStream() {
|
||||||
|
|
||||||
isReloading = true;
|
isReloading = true;
|
||||||
stream.close();
|
stream.close();
|
||||||
|
// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
|
||||||
stream.once("_connected_", () => (isReloading = false));
|
stream.once("_connected_", () => (isReloading = false));
|
||||||
stream.stream.reconnect();
|
stream.stream.reconnect();
|
||||||
isReloading = false;
|
isReloading = false;
|
||||||
|
|
|
@ -17,7 +17,8 @@ export async function fetchThemes(): Promise<void> {
|
||||||
key: "themes",
|
key: "themes",
|
||||||
});
|
});
|
||||||
localStorage.setItem(lsCacheKey, JSON.stringify(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;
|
if (err.code === "NO_SUCH_KEY") return;
|
||||||
throw err;
|
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,
|
"minify": false,
|
||||||
"module": {
|
"module": {
|
||||||
"type": "commonjs",
|
"type": "es6",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"resolveFully": true
|
"resolveFully": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"description": "Firefish SDK for JavaScript",
|
"description": "Firefish SDK for JavaScript",
|
||||||
"homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/firefish-js",
|
"homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/firefish-js",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
"type": "module",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -35,7 +36,7 @@
|
||||||
"typescript": "5.4.5"
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"built"
|
"built", "src"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eventemitter3": "5.0.1",
|
"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();
|
const MK_API_ERROR = Symbol();
|
||||||
|
|
||||||
|
@ -7,10 +7,12 @@ export type APIError = {
|
||||||
code: string;
|
code: string;
|
||||||
message: string;
|
message: string;
|
||||||
kind: "client" | "server";
|
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;
|
return reason[MK_API_ERROR] === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ export type FetchLike = (
|
||||||
},
|
},
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
status: number;
|
status: number;
|
||||||
json(): Promise<any>;
|
json(): Promise<ExplicitlyUsedAny>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type IsNeverType<T> = [T] extends [never] ? true : false;
|
type IsNeverType<T> = [T] extends [never] ? true : false;
|
||||||
|
@ -36,7 +38,10 @@ type IsCaseMatched<
|
||||||
P extends Endpoints[E]["req"],
|
P extends Endpoints[E]["req"],
|
||||||
C extends number,
|
C extends number,
|
||||||
> = IsNeverType<
|
> = IsNeverType<
|
||||||
StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>
|
StrictExtract<
|
||||||
|
Endpoints[E]["res"]["$switch"]["$cases"][C],
|
||||||
|
[P, ExplicitlyUsedAny]
|
||||||
|
>
|
||||||
> extends false
|
> extends false
|
||||||
? true
|
? true
|
||||||
: false;
|
: false;
|
||||||
|
@ -45,7 +50,10 @@ type GetCaseResult<
|
||||||
E extends keyof Endpoints,
|
E extends keyof Endpoints,
|
||||||
P extends Endpoints[E]["req"],
|
P extends Endpoints[E]["req"],
|
||||||
C extends number,
|
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 {
|
export class APIClient {
|
||||||
public origin: string;
|
public origin: string;
|
||||||
|
@ -70,7 +78,7 @@ export class APIClient {
|
||||||
credential?: string | null | undefined,
|
credential?: string | null | undefined,
|
||||||
): Promise<
|
): Promise<
|
||||||
Endpoints[E]["res"] extends {
|
Endpoints[E]["res"] extends {
|
||||||
$switch: { $cases: [any, any][]; $default: any };
|
$switch: { $cases: [unknown, unknown][]; $default: unknown };
|
||||||
}
|
}
|
||||||
? IsCaseMatched<E, P, 0> extends true
|
? IsCaseMatched<E, P, 0> extends true
|
||||||
? GetCaseResult<E, P, 0>
|
? GetCaseResult<E, P, 0>
|
||||||
|
|
|
@ -38,7 +38,7 @@ import type {
|
||||||
UserList,
|
UserList,
|
||||||
UserLite,
|
UserLite,
|
||||||
UserSorting,
|
UserSorting,
|
||||||
} from "./entities";
|
} from "./entities.js";
|
||||||
|
|
||||||
import type * as consts from "./consts";
|
import type * as consts from "./consts";
|
||||||
|
|
||||||
|
@ -264,9 +264,9 @@ export type Endpoints = {
|
||||||
|
|
||||||
// clips
|
// clips
|
||||||
"clips/add-note": { req: TODO; res: TODO };
|
"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/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/notes": { req: TODO; res: TODO };
|
||||||
"clips/show": { req: TODO; res: TODO };
|
"clips/show": { req: TODO; res: TODO };
|
||||||
"clips/update": { req: TODO; res: TODO };
|
"clips/update": { req: TODO; res: TODO };
|
||||||
|
@ -363,6 +363,16 @@ export type Endpoints = {
|
||||||
res: DriveFile[];
|
res: DriveFile[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"email-address/available": {
|
||||||
|
req: {
|
||||||
|
emailAddress: string;
|
||||||
|
};
|
||||||
|
res: {
|
||||||
|
available?: boolean;
|
||||||
|
reason: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// endpoint
|
// endpoint
|
||||||
endpoint: {
|
endpoint: {
|
||||||
req: { endpoint: string };
|
req: { endpoint: string };
|
||||||
|
@ -739,6 +749,18 @@ export type Endpoints = {
|
||||||
};
|
};
|
||||||
res: Note[];
|
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": {
|
"notes/hybrid-timeline": {
|
||||||
req: {
|
req: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
@ -759,6 +781,12 @@ export type Endpoints = {
|
||||||
};
|
};
|
||||||
res: Note[];
|
res: Note[];
|
||||||
};
|
};
|
||||||
|
"notes/make-private": {
|
||||||
|
req: {
|
||||||
|
noteId: Note["id"];
|
||||||
|
};
|
||||||
|
res: null;
|
||||||
|
};
|
||||||
"notes/mentions": {
|
"notes/mentions": {
|
||||||
req: {
|
req: {
|
||||||
following?: boolean;
|
following?: boolean;
|
||||||
|
@ -900,6 +928,16 @@ export type Endpoints = {
|
||||||
// promo
|
// promo
|
||||||
"promo/read": { req: TODO; res: TODO };
|
"promo/read": { req: TODO; res: TODO };
|
||||||
|
|
||||||
|
// release
|
||||||
|
release: {
|
||||||
|
req: null;
|
||||||
|
res: {
|
||||||
|
version: string;
|
||||||
|
notes: string;
|
||||||
|
screenshots: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// request-reset-password
|
// request-reset-password
|
||||||
"request-reset-password": {
|
"request-reset-password": {
|
||||||
req: { username: string; email: string };
|
req: { username: string; email: string };
|
||||||
|
@ -922,8 +960,36 @@ export type Endpoints = {
|
||||||
// ck specific
|
// ck specific
|
||||||
"latest-version": { req: NoParams; res: TODO };
|
"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
|
||||||
"sw/register": { req: TODO; res: TODO };
|
"sw/register": { req: TODO; res: TODO };
|
||||||
|
"sw/unregister": {
|
||||||
|
req: {
|
||||||
|
endpoint: string;
|
||||||
|
};
|
||||||
|
res: null;
|
||||||
|
};
|
||||||
|
|
||||||
// username
|
// username
|
||||||
"username/available": {
|
"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 ID = string;
|
||||||
export type DateString = string;
|
export type DateString = string;
|
||||||
|
@ -116,6 +117,7 @@ export type MeDetailed = UserDetailed & {
|
||||||
preventAiLearning: boolean;
|
preventAiLearning: boolean;
|
||||||
receiveAnnouncementEmail: boolean;
|
receiveAnnouncementEmail: boolean;
|
||||||
usePasswordLessLogin: boolean;
|
usePasswordLessLogin: boolean;
|
||||||
|
token: string;
|
||||||
[other: string]: any;
|
[other: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -482,7 +484,7 @@ export type Announcement = {
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
isRead?: boolean;
|
isRead?: boolean;
|
||||||
isGoodNews: boolean;
|
isGoodNews: boolean;
|
||||||
showPopUp: boolean;
|
showPopup: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Antenna = {
|
export type Antenna = {
|
||||||
|
@ -513,7 +515,7 @@ export type AuthSession = {
|
||||||
|
|
||||||
export type Ad = TODO;
|
export type Ad = TODO;
|
||||||
|
|
||||||
export type Clip = TODO;
|
export type Clip = Packed<"Clip">;
|
||||||
|
|
||||||
export type NoteFavorite = {
|
export type NoteFavorite = {
|
||||||
id: ID;
|
id: ID;
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import * as acct from "./acct";
|
import * as acct from "./acct.js";
|
||||||
import type { Acct } from "./acct";
|
import type { Acct } from "./acct.js";
|
||||||
import { Endpoints } from "./api.types";
|
import type { Endpoints } from "./api.types.js";
|
||||||
import type * as ApiTypes from "./api.types";
|
import type * as ApiTypes from "./api.types.js";
|
||||||
import * as consts from "./consts";
|
import * as consts from "./consts.js";
|
||||||
import Stream, { Connection } from "./streaming";
|
import Stream, { Connection } from "./streaming.js";
|
||||||
import * as StreamTypes from "./streaming.types";
|
import * as StreamTypes from "./streaming.types.js";
|
||||||
import type * as TypeUtils from "./type-utils";
|
import type * as TypeUtils from "./type-utils.js";
|
||||||
|
|
||||||
|
import type * as SchemaTypes from "./misc/schema.js";
|
||||||
|
import * as Schema from "./misc/schema.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Endpoints,
|
type Endpoints,
|
||||||
type ApiTypes,
|
type ApiTypes,
|
||||||
Stream,
|
Stream,
|
||||||
Connection as ChannelConnection,
|
Connection as ChannelConnection,
|
||||||
|
@ -16,6 +19,8 @@ export {
|
||||||
acct,
|
acct,
|
||||||
type Acct,
|
type Acct,
|
||||||
type TypeUtils,
|
type TypeUtils,
|
||||||
|
Schema,
|
||||||
|
type SchemaTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const permissions = consts.permissions;
|
export const permissions = consts.permissions;
|
||||||
|
@ -26,9 +31,12 @@ export const languages = consts.languages;
|
||||||
export const ffVisibility = consts.ffVisibility;
|
export const ffVisibility = consts.ffVisibility;
|
||||||
export const instanceSortParam = consts.instanceSortParam;
|
export const instanceSortParam = consts.instanceSortParam;
|
||||||
|
|
||||||
|
import { langmap, type PostLanguage } from "./misc/langmap.js";
|
||||||
|
export { langmap, type PostLanguage };
|
||||||
|
|
||||||
// api extractor not supported yet
|
// api extractor not supported yet
|
||||||
//export * as api from './api';
|
//export * as api from './api';
|
||||||
//export * as entities from './entities';
|
//export * as entities from './entities';
|
||||||
import * as api from "./api";
|
import * as api from "./api.js";
|
||||||
import * as entities from "./entities";
|
import * as entities from "./entities.js";
|
||||||
export { api, entities };
|
export { api, entities };
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
// TODO: sharedに置いてバックエンドのと統合したい
|
|
||||||
export const iso639Langs1 = {
|
export const iso639Langs1 = {
|
||||||
af: {
|
af: {
|
||||||
nativeName: "Afrikaans",
|
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 = {
|
export const packedFederationInstanceSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -83,7 +81,7 @@ export const packedFederationInstanceSchema = {
|
||||||
type: "string",
|
type: "string",
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
example: config.version,
|
example: "20240424",
|
||||||
},
|
},
|
||||||
openRegistrations: {
|
openRegistrations: {
|
||||||
type: "boolean",
|
type: "boolean",
|
|
@ -1,4 +1,4 @@
|
||||||
import { langmap } from "@/misc/langmap.js";
|
import { langmap } from "../misc/langmap.js";
|
||||||
|
|
||||||
export const packedNoteSchema = {
|
export const packedNoteSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
|
@ -208,15 +208,5 @@ export const packedNoteSchema = {
|
||||||
optional: true,
|
optional: true,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
},
|
},
|
||||||
myRenoteCount: {
|
|
||||||
type: "number",
|
|
||||||
optional: true,
|
|
||||||
nullable: false,
|
|
||||||
},
|
|
||||||
quoteCount: {
|
|
||||||
type: "number",
|
|
||||||
optional: false,
|
|
||||||
nullable: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
|
@ -1,4 +1,4 @@
|
||||||
import { notificationTypes } from "@/types.js";
|
import { notificationTypes } from "../consts.js";
|
||||||
|
|
||||||
export const packedNotificationSchema = {
|
export const packedNotificationSchema = {
|
||||||
type: "object",
|
type: "object",
|
|
@ -1,6 +1,6 @@
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from "eventemitter3";
|
||||||
import ReconnectingWebsocket from "reconnecting";
|
import ReconnectingWebsocket from "reconnecting";
|
||||||
import type { BroadcastEvents, Channels } from "./streaming.types";
|
import type { BroadcastEvents, Channels } from "./streaming.types.js";
|
||||||
|
|
||||||
function autobind(instance: any): void {
|
function autobind(instance: any): void {
|
||||||
const prototype = Object.getPrototypeOf(instance);
|
const prototype = Object.getPrototypeOf(instance);
|
||||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
||||||
UserGroup,
|
UserGroup,
|
||||||
UserLite,
|
UserLite,
|
||||||
} from "./entities";
|
} from "./entities";
|
||||||
import type { Connection } from "./streaming";
|
import type { Connection } from "./streaming.js";
|
||||||
|
|
||||||
type FIXME = any;
|
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;
|
[K in keyof Type]: Type[K] extends U ? K : never;
|
||||||
}[keyof Type];
|
}[keyof Type];
|
||||||
|
|
||||||
export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>;
|
export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>;
|
||||||
|
|
||||||
|
export type NonUndefinedAble<T> = T extends undefined ? never : T;
|
||||||
|
|
|
@ -141,6 +141,9 @@ importers:
|
||||||
file-type:
|
file-type:
|
||||||
specifier: 19.0.0
|
specifier: 19.0.0
|
||||||
version: 19.0.0
|
version: 19.0.0
|
||||||
|
firefish-js:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../firefish-js
|
||||||
fluent-ffmpeg:
|
fluent-ffmpeg:
|
||||||
specifier: 2.1.2
|
specifier: 2.1.2
|
||||||
version: 2.1.2
|
version: 2.1.2
|
||||||
|
|
Loading…
Reference in a new issue