Merge remote-tracking branch 'origin/develop' into hk/edit-federation

This commit is contained in:
Kaity A 2023-05-02 18:24:54 +10:00
commit a7c9400a42
No known key found for this signature in database
GPG key ID: 5A797B97C2A490AD
109 changed files with 3012 additions and 916 deletions

View file

@ -1,7 +1,8 @@
pipeline: pipeline:
testCommit: testCommit:
image: node:latest image: node:alpine
commands: commands:
- apk add --no-cache cargo python3 make g++
- cp .config/ci.yml .config/default.yml - cp .config/ci.yml .config/default.yml
- corepack enable - corepack enable
- corepack prepare pnpm@latest --activate - corepack prepare pnpm@latest --activate

File diff suppressed because it is too large Load diff

View file

@ -71,8 +71,8 @@ import: "Import"
export: "Export" export: "Export"
files: "Files" files: "Files"
download: "Download" download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Posts\ driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? It\
\ with this file attached will also be deleted." \ will be removed from all posts that contain it as an attachment."
unfollowConfirm: "Are you sure that you want to unfollow {name}?" unfollowConfirm: "Are you sure that you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added\ exportRequested: "You've requested an export. This may take a while. It will be added\
\ to your Drive once completed." \ to your Drive once completed."
@ -200,6 +200,7 @@ perHour: "Per Hour"
perDay: "Per Day" perDay: "Per Day"
stopActivityDelivery: "Stop sending activities" stopActivityDelivery: "Stop sending activities"
blockThisInstance: "Block this instance" blockThisInstance: "Block this instance"
silenceThisInstance: "Silence this instance"
operations: "Operations" operations: "Operations"
software: "Software" software: "Software"
version: "Version" version: "Version"
@ -221,10 +222,13 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
blockedInstances: "Blocked Instances" blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances that you want to\ blockedInstancesDescription: "List the hostnames of the instances that you want to\
\ block. Listed instances will no longer be able to communicate with this instance." \ block. Listed instances will no longer be able to communicate with this instance."
silencedInstances: "Silenced Instances"
silencedInstancesDescription: "List the hostnames of the instances that you want to\
\ silence. Accounts in the listed instances are treated as \"Silenced\", can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
hiddenTags: "Hidden Hashtags" hiddenTags: "Hidden Hashtags"
hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\ hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\
\ to hide from trending and explore. Hidden hashtags are still discoverable via\ \ to hide from trending and explore. Hidden hashtags are still discoverable via\
\ other means." \ other means. Blocked instances are not affected even if listed here."
muteAndBlock: "Mutes and Blocks" muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users" mutedUsers: "Muted users"
blockedUsers: "Blocked users" blockedUsers: "Blocked users"
@ -243,6 +247,7 @@ noCustomEmojis: "There are no emoji"
noJobs: "There are no jobs" noJobs: "There are no jobs"
federating: "Federating" federating: "Federating"
blocked: "Blocked" blocked: "Blocked"
silenced: "Silenced"
suspended: "Suspended" suspended: "Suspended"
all: "All" all: "All"
subscribing: "Subscribing" subscribing: "Subscribing"
@ -832,7 +837,7 @@ active: "Active"
offline: "Offline" offline: "Offline"
notRecommended: "Not recommended" notRecommended: "Not recommended"
botProtection: "Bot Protection" botProtection: "Bot Protection"
instanceBlocking: "Blocked Instances" instanceBlocking: "Federation Block/Silence"
selectAccount: "Select account" selectAccount: "Select account"
switchAccount: "Switch account" switchAccount: "Switch account"
enabled: "Enabled" enabled: "Enabled"
@ -1200,7 +1205,7 @@ _mfm:
inlineMath: "Math (Inline)" inlineMath: "Math (Inline)"
inlineMathDescription: "Display math formulas (KaTeX) in-line" inlineMathDescription: "Display math formulas (KaTeX) in-line"
blockMath: "Math (Block)" blockMath: "Math (Block)"
blockMathDescription: "Display multi-line math formulas (KaTeX) in a block" blockMathDescription: "Display math formulas (KaTeX) in a block"
quote: "Quote" quote: "Quote"
quoteDescription: "Displays content as a quote." quoteDescription: "Displays content as a quote."
emoji: "Custom Emoji" emoji: "Custom Emoji"

View file

@ -1,3 +1,4 @@
_lang_: "Suomi"
username: Käyttäjänimi username: Käyttäjänimi
fetchingAsApObject: Hae Fedeversestä fetchingAsApObject: Hae Fedeversestä
gotIt: Selvä! gotIt: Selvä!
@ -220,4 +221,3 @@ clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jo
federoidu. Yleensä tätä toimintoa ei tarvita. federoidu. Yleensä tätä toimintoa ei tarvita.
blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää. blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa. Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.
_lang_: Suomi

View file

@ -183,6 +183,7 @@ perHour: "1時間ごと"
perDay: "1日ごと" perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送を停止" stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このインスタンスをブロック" blockThisInstance: "このインスタンスをブロック"
silenceThisInstance: "このインスタンスをサイレンス"
operations: "操作" operations: "操作"
software: "ソフトウェア" software: "ソフトウェア"
version: "バージョン" version: "バージョン"
@ -202,6 +203,8 @@ clearCachedFiles: "キャッシュをクリア"
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?" clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
blockedInstances: "ブロックしたインスタンス" blockedInstances: "ブロックしたインスタンス"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。" blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
silencedInstances: "サイレンスしたインスタンス"
silencedInstancesDescription: "サイレンスしたいインスタンスのホストを改行で区切って設定します。サイレンスされたインスタンスに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー" mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"
@ -220,6 +223,7 @@ noCustomEmojis: "絵文字はありません"
noJobs: "ジョブはありません" noJobs: "ジョブはありません"
federating: "連合中" federating: "連合中"
blocked: "ブロック中" blocked: "ブロック中"
silenced: "サイレンス中"
suspended: "配信停止" suspended: "配信停止"
all: "全て" all: "全て"
subscribing: "購読中" subscribing: "購読中"
@ -768,7 +772,7 @@ active: "アクティブ"
offline: "オフライン" offline: "オフライン"
notRecommended: "非推奨" notRecommended: "非推奨"
botProtection: "Botプロテクション" botProtection: "Botプロテクション"
instanceBlocking: "インスタンスブロック" instanceBlocking: "連合ブロック・サイレンス"
selectAccount: "アカウントを選択" selectAccount: "アカウントを選択"
switchAccount: "アカウントを切り替え" switchAccount: "アカウントを切り替え"
enabled: "有効" enabled: "有効"
@ -1079,7 +1083,7 @@ _mfm:
inlineMath: "数式(インライン)" inlineMath: "数式(インライン)"
inlineMathDescription: "数式(KaTeX)をインラインで表示します。" inlineMathDescription: "数式(KaTeX)をインラインで表示します。"
blockMath: "数式(ブロック)" blockMath: "数式(ブロック)"
blockMathDescription: "複数行の数式(KaTeX)をブロックで表示します。" blockMathDescription: "数式(KaTeX)をブロックで表示します。"
quote: "引用" quote: "引用"
quoteDescription: "内容が引用であることを示せます。" quoteDescription: "内容が引用であることを示せます。"
emoji: "カスタム絵文字" emoji: "カスタム絵文字"
@ -1120,6 +1124,7 @@ _mfm:
rotateDescription: "指定した角度で回転させます。" rotateDescription: "指定した角度で回転させます。"
plain: "プレーン" plain: "プレーン"
plainDescription: "内側の構文を全て無効にします。" plainDescription: "内側の構文を全て無効にします。"
position: 位置
_instanceTicker: _instanceTicker:
none: "表示しない" none: "表示しない"
remote: "リモートユーザーに表示" remote: "リモートユーザーに表示"
@ -1128,7 +1133,7 @@ _serverDisconnectedBehavior:
reload: "自動でリロード" reload: "自動でリロード"
dialog: "ダイアログで警告" dialog: "ダイアログで警告"
quiet: "控えめに警告" quiet: "控えめに警告"
nothing: "何も起こらない" nothing: "何もない"
_channel: _channel:
create: "チャンネルを作成" create: "チャンネルを作成"
edit: "チャンネルを編集" edit: "チャンネルを編集"

View file

@ -1009,9 +1009,9 @@ _mfm:
blockCode: "代码(块)" blockCode: "代码(块)"
blockCodeDescription: "语法高亮显示整块程序代码。" blockCodeDescription: "语法高亮显示整块程序代码。"
inlineMath: "数学公式(内嵌)" inlineMath: "数学公式(内嵌)"
inlineMathDescription: "显示内嵌的KaTex公式。" inlineMathDescription: "显示内嵌的KaTeX公式。"
blockMath: "数学公式(块)" blockMath: "数学公式(块)"
blockMathDescription: "显示整块的多行KaTex数学公式。" blockMathDescription: "显示整块的KaTeX数学公式。"
quote: "引用" quote: "引用"
quoteDescription: "可以用来表示引用的内容。" quoteDescription: "可以用来表示引用的内容。"
emoji: "自定义表情符号" emoji: "自定义表情符号"

View file

@ -1012,9 +1012,9 @@ _mfm:
blockCode: "程式碼(區塊)" blockCode: "程式碼(區塊)"
blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。" blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。"
inlineMath: "數學公式(內嵌)" inlineMath: "數學公式(內嵌)"
inlineMathDescription: "顯示內嵌的KaTex數學公式。" inlineMathDescription: "顯示內嵌的KaTeX數學公式。"
blockMath: "數學公式(方塊)" blockMath: "數學公式(方塊)"
blockMathDescription: "以區塊顯示複數行的KaTex數學式。" blockMathDescription: "以區塊顯示KaTeX數學式。"
quote: "引用" quote: "引用"
quoteDescription: "可以用來表示引用的内容。" quoteDescription: "可以用來表示引用的内容。"
emoji: "自訂表情符號" emoji: "自訂表情符號"

View file

@ -1,6 +1,6 @@
{ {
"name": "calckey", "name": "calckey",
"version": "13.2.0-dev41", "version": "14.0.0-dev",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
@ -40,6 +40,8 @@
"@bull-board/ui": "^4.10.2", "@bull-board/ui": "^4.10.2",
"@napi-rs/cli": "^2.15.0", "@napi-rs/cli": "^2.15.0",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^3.21.0",
"focus-trap": "^7.2.0",
"focus-trap-vue": "^4.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 473 KiB

View file

@ -0,0 +1,13 @@
export class InstanceSilence1682891890317 {
name = "InstanceSilence1682891890317";
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`,
);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
}
}

View file

@ -18,3 +18,21 @@ export async function shouldBlockInstance(
(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`), (blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
); );
} }
/**
* Returns whether a specific host (punycoded) should be limited.
*
* @param host punycoded instance host
* @param meta a resolved Meta table
* @returns whether the given host should be limited
*/
export async function shouldSilenceInstance(
host: Instance["host"],
meta?: Meta,
): Promise<boolean> {
const { silencedHosts } = meta ?? (await fetchMeta());
return silencedHosts.some(
(silencedHost) =>
host === silencedHost || host.endsWith(`.${silencedHost}`),
);
}

View file

@ -97,6 +97,11 @@ export class Meta {
}) })
public blockedHosts: string[]; public blockedHosts: string[];
@Column('varchar', {
length: 256, array: true, default: '{}',
})
public silencedHosts: string[];
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View file

@ -1,12 +1,13 @@
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
import { Instance } from "@/models/entities/instance.js"; import { Instance } from "@/models/entities/instance.js";
import type { Packed } from "@/misc/schema.js"; import type { Packed } from "@/misc/schema.js";
import { fetchMeta } from "@/misc/fetch-meta.js"; import {
import { shouldBlockInstance } from "@/misc/should-block-instance.js"; shouldBlockInstance,
shouldSilenceInstance,
} from "@/misc/should-block-instance.js";
export const InstanceRepository = db.getRepository(Instance).extend({ export const InstanceRepository = db.getRepository(Instance).extend({
async pack(instance: Instance): Promise<Packed<"FederationInstance">> { async pack(instance: Instance): Promise<Packed<"FederationInstance">> {
const meta = await fetchMeta();
return { return {
id: instance.id, id: instance.id,
caughtAt: instance.caughtAt.toISOString(), caughtAt: instance.caughtAt.toISOString(),
@ -22,6 +23,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
isNotResponding: instance.isNotResponding, isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended, isSuspended: instance.isSuspended,
isBlocked: await shouldBlockInstance(instance.host), isBlocked: await shouldBlockInstance(instance.host),
isSilenced: await shouldSilenceInstance(instance.host),
softwareName: instance.softwareName, softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion, softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations, openRegistrations: instance.openRegistrations,

View file

@ -68,6 +68,11 @@ export const packedFederationInstanceSchema = {
optional: false, optional: false,
nullable: false, nullable: false,
}, },
isSilenced: {
type: "boolean",
optional: false,
nullable: false,
},
softwareName: { softwareName: {
type: "string", type: "string",
optional: false, optional: false,

View file

@ -10,7 +10,13 @@ import { renderPerson } from "@/remote/activitypub/renderer/person.js";
import renderEmoji from "@/remote/activitypub/renderer/emoji.js"; import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
import { inbox as processInbox } from "@/queue/index.js"; import { inbox as processInbox } from "@/queue/index.js";
import { isSelfHost, toPuny } from "@/misc/convert-host.js"; import { isSelfHost, toPuny } from "@/misc/convert-host.js";
import { Notes, Users, Emojis, NoteReactions } from "@/models/index.js"; import {
Notes,
Users,
Emojis,
NoteReactions,
FollowRequests,
} from "@/models/index.js";
import type { ILocalUser, User } from "@/models/entities/user.js"; import type { ILocalUser, User } from "@/models/entities/user.js";
import { renderLike } from "@/remote/activitypub/renderer/like.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js";
import { getUserKeypair } from "@/misc/keypair-store.js"; import { getUserKeypair } from "@/misc/keypair-store.js";
@ -330,22 +336,68 @@ router.get("/likes/:like", async (ctx) => {
}); });
// follow // follow
router.get("/follows/:follower/:followee", async (ctx) => { router.get(
"/follows/:follower/:followee",
async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify !== 200) {
ctx.status = verify;
return;
}
// This may be used before the follow is completed, so we do not
// check if the following exists.
const [follower, followee] = await Promise.all([
Users.findOneBy({
id: ctx.params.follower,
host: IsNull(),
}),
Users.findOneBy({
id: ctx.params.followee,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
ctx.status = 404;
return;
}
ctx.body = renderActivity(renderFollow(follower, followee));
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
ctx.set("Cache-Control", "public, max-age=180");
}
setResponseType(ctx);
},
);
// follow request
router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req); const verify = await checkFetch(ctx.req);
if (verify !== 200) { if (verify !== 200) {
ctx.status = verify; ctx.status = verify;
return; return;
} }
// This may be used before the follow is completed, so we do not
// check if the following exists. const followRequest = await FollowRequests.findOneBy({
id: ctx.params.followRequestId,
});
if (followRequest == null) {
ctx.status = 404;
return;
}
const [follower, followee] = await Promise.all([ const [follower, followee] = await Promise.all([
Users.findOneBy({ Users.findOneBy({
id: ctx.params.follower, id: followRequest.followerId,
host: IsNull(), host: IsNull(),
}), }),
Users.findOneBy({ Users.findOneBy({
id: ctx.params.followee, id: followRequest.followeeId,
host: Not(IsNull()), host: Not(IsNull()),
}), }),
]); ]);
@ -355,13 +407,13 @@ router.get("/follows/:follower/:followee", async (ctx) => {
return; return;
} }
ctx.body = renderActivity(renderFollow(follower, followee));
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) { if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else { } else {
ctx.set("Cache-Control", "public, max-age=180"); ctx.set("Cache-Control", "public, max-age=180");
} }
ctx.body = renderActivity(renderFollow(follower, followee));
setResponseType(ctx); setResponseType(ctx);
}); });

View file

@ -533,7 +533,7 @@ const eps = [
["i/export-following", ep___i_exportFollowing], ["i/export-following", ep___i_exportFollowing],
["i/export-mute", ep___i_exportMute], ["i/export-mute", ep___i_exportMute],
["i/export-notes", ep___i_exportNotes], ["i/export-notes", ep___i_exportNotes],
["i/import-posts", ep___i_importPosts], // ["i/import-posts", ep___i_importPosts],
["i/export-user-lists", ep___i_exportUserLists], ["i/export-user-lists", ep___i_exportUserLists],
["i/favorites", ep___i_favorites], ["i/favorites", ep___i_favorites],
["i/gallery/likes", ep___i_gallery_likes], ["i/gallery/likes", ep___i_gallery_likes],

View file

@ -259,6 +259,16 @@ export const meta = {
nullable: false, nullable: false,
}, },
}, },
silencedHosts: {
type: "array",
optional: true,
nullable: false,
items: {
type: "string",
optional: false,
nullable: false,
},
},
allowedHosts: { allowedHosts: {
type: "array", type: "array",
optional: true, optional: true,
@ -524,6 +534,7 @@ export default define(meta, paramDef, async (ps, me) => {
customSplashIcons: instance.customSplashIcons, customSplashIcons: instance.customSplashIcons,
hiddenTags: instance.hiddenTags, hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts, blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
allowedHosts: instance.allowedHosts, allowedHosts: instance.allowedHosts,
privateMode: instance.privateMode, privateMode: instance.privateMode,
secureMode: instance.secureMode, secureMode: instance.secureMode,

View file

@ -61,6 +61,13 @@ export const paramDef = {
type: "string", type: "string",
}, },
}, },
silencedHosts: {
type: "array",
nullable: true,
items: {
type: "string",
},
},
allowedHosts: { allowedHosts: {
type: "array", type: "array",
nullable: true, nullable: true,
@ -219,6 +226,15 @@ export default define(meta, paramDef, async (ps, me) => {
}); });
} }
if (Array.isArray(ps.silencedHosts)) {
let lastValue = "";
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
const lv = lastValue;
lastValue = h;
return h !== "" && h !== lv;
});
}
if (ps.themeColor !== undefined) { if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor; set.themeColor = ps.themeColor;
} }

View file

@ -34,6 +34,7 @@ export const paramDef = {
notResponding: { type: "boolean", nullable: true }, notResponding: { type: "boolean", nullable: true },
suspended: { type: "boolean", nullable: true }, suspended: { type: "boolean", nullable: true },
federating: { type: "boolean", nullable: true }, federating: { type: "boolean", nullable: true },
silenced: { type: "boolean", nullable: true },
subscribing: { type: "boolean", nullable: true }, subscribing: { type: "boolean", nullable: true },
publishing: { type: "boolean", nullable: true }, publishing: { type: "boolean", nullable: true },
limit: { type: "integer", minimum: 1, maximum: 100, default: 30 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 30 },
@ -115,6 +116,22 @@ export default define(meta, paramDef, async (ps, me) => {
} }
} }
if (typeof ps.silenced === "boolean") {
const meta = await fetchMeta(true);
if (ps.silenced) {
if (meta.silencedHosts.length === 0) {
return [];
}
query.andWhere("instance.host IN (:...silences)", {
silences: meta.silencedHosts,
});
} else if (meta.silencedHosts.length > 0) {
query.andWhere("instance.host NOT IN (:...silences)", {
silences: meta.silencedHosts,
});
}
}
if (typeof ps.notResponding === "boolean") { if (typeof ps.notResponding === "boolean") {
if (ps.notResponding) { if (ps.notResponding) {
query.andWhere("instance.isNotResponding = TRUE"); query.andWhere("instance.isNotResponding = TRUE");

View file

@ -8,8 +8,8 @@ export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
limit: { limit: {
duration: DAY, duration: DAY * 30,
max: 1, max: 2,
}, },
errors: { errors: {
noSuchFile: { noSuchFile: {

View file

@ -29,6 +29,7 @@ import {
convertId, convertId,
IdConvertType as IdType, IdConvertType as IdType,
} from "../../../native-utils/built/index.js"; } from "../../../native-utils/built/index.js";
import { convertAttachment } from "./mastodon/converters.js";
// re-export native rust id conversion (function and enum) // re-export native rust id conversion (function and enum)
export { IdType, convertId }; export { IdType, convertId };
@ -93,7 +94,7 @@ mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
return; return;
} }
const data = await client.uploadMedia(multipartData); const data = await client.uploadMedia(multipartData);
ctx.body = data.data; ctx.body = convertAttachment(data.data as Entity.Attachment);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -112,7 +113,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
return; return;
} }
const data = await client.uploadMedia(multipartData); const data = await client.uploadMedia(multipartData);
ctx.body = data.data; ctx.body = convertAttachment(data.data as Entity.Attachment);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;

View file

@ -8,6 +8,8 @@ import { apiTimelineMastodon } from "./endpoints/timeline.js";
import { apiNotificationsMastodon } from "./endpoints/notifications.js"; import { apiNotificationsMastodon } from "./endpoints/notifications.js";
import { apiSearchMastodon } from "./endpoints/search.js"; import { apiSearchMastodon } from "./endpoints/search.js";
import { getInstance } from "./endpoints/meta.js"; import { getInstance } from "./endpoints/meta.js";
import { convertAnnouncement, convertFilter } from "./converters.js";
import { convertId, IdType } from "../index.js";
export function getClient( export function getClient(
BASE_URL: string, BASE_URL: string,
@ -68,7 +70,9 @@ export function apiMastodonCompatible(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getInstanceAnnouncements(); const data = await client.getInstanceAnnouncements();
ctx.body = data.data; ctx.body = data.data.map((announcement) =>
convertAnnouncement(announcement),
);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -83,7 +87,9 @@ export function apiMastodonCompatible(router: Router): void {
const accessTokens = ctx.request.headers.authorization; const accessTokens = ctx.request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.dismissInstanceAnnouncement(ctx.params.id); const data = await client.dismissInstanceAnnouncement(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -100,7 +106,7 @@ export function apiMastodonCompatible(router: Router): void {
// displayed without being logged in // displayed without being logged in
try { try {
const data = await client.getFilters(); const data = await client.getFilters();
ctx.body = data.data; ctx.body = data.data.map((filter) => convertFilter(filter));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;

View file

@ -0,0 +1,61 @@
import { Entity } from "@calckey/megalodon";
import { convertId, IdType } from "../index.js";
function simpleConvert(data: any) {
data.id = convertId(data.id, IdType.MastodonId);
return data;
}
export function convertAccount(account: Entity.Account) {
return simpleConvert(account);
}
export function convertAnnouncement(announcement: Entity.Announcement) {
return simpleConvert(announcement);
}
export function convertAttachment(attachment: Entity.Attachment) {
return simpleConvert(attachment);
}
export function convertFilter(filter: Entity.Filter) {
return simpleConvert(filter);
}
export function convertList(list: Entity.List) {
return simpleConvert(list);
}
export function convertNotification(notification: Entity.Notification) {
notification.account = convertAccount(notification.account);
notification.id = convertId(notification.id, IdType.MastodonId);
if (notification.status)
notification.status = convertStatus(notification.status);
return notification;
}
export function convertPoll(poll: Entity.Poll) {
return simpleConvert(poll);
}
export function convertRelationship(relationship: Entity.Relationship) {
return simpleConvert(relationship);
}
export function convertStatus(status: Entity.Status) {
status.account = convertAccount(status.account);
status.id = convertId(status.id, IdType.MastodonId);
if (status.in_reply_to_account_id)
status.in_reply_to_account_id = convertId(
status.in_reply_to_account_id,
IdType.MastodonId,
);
if (status.in_reply_to_id)
status.in_reply_to_id = convertId(status.in_reply_to_id, IdType.MastodonId);
status.media_attachments = status.media_attachments.map((attachment) =>
convertAttachment(attachment),
);
status.mentions = status.mentions.map((mention) => ({
...mention,
id: convertId(mention.id, IdType.MastodonId),
}));
if (status.poll) status.poll = convertPoll(status.poll);
if (status.reblog) status.reblog = convertStatus(status.reblog);
return status;
}

View file

@ -3,8 +3,14 @@ import { resolveUser } from "@/remote/resolve-user.js";
import Router from "@koa/router"; import Router from "@koa/router";
import { FindOptionsWhere, IsNull } from "typeorm"; import { FindOptionsWhere, IsNull } from "typeorm";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { argsToBools, limitToInt } from "./timeline.js"; import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js";
import { convertId, IdType } from "../../index.js"; import { convertId, IdType } from "../../index.js";
import {
convertAccount,
convertList,
convertRelationship,
convertStatus,
} from "../converters.js";
const relationshipModel = { const relationshipModel = {
id: "", id: "",
@ -62,9 +68,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.updateCredentials( const data = await client.updateCredentials(
(ctx.request as any).body as any, (ctx.request as any).body as any,
); );
let resp = data.data; ctx.body = convertAccount(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -81,9 +85,7 @@ export function apiAccountMastodon(router: Router): void {
(ctx.request.query as any).acct, (ctx.request.query as any).acct,
"accounts", "accounts",
); );
let resp = data.data.accounts[0]; ctx.body = convertAccount(data.data.accounts[0]);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -115,11 +117,9 @@ export function apiAccountMastodon(router: Router): void {
} }
const data = await client.getRelationships(reqIds); const data = await client.getRelationships(reqIds);
let resp = data.data; ctx.body = data.data.map((relationship) =>
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { convertRelationship(relationship),
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); );
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
let data = e.response.data; let data = e.response.data;
@ -136,9 +136,7 @@ export function apiAccountMastodon(router: Router): void {
try { try {
const calcId = convertId(ctx.params.id, IdType.CalckeyId); const calcId = convertId(ctx.params.id, IdType.CalckeyId);
const data = await client.getAccount(calcId); const data = await client.getAccount(calcId);
let resp = data.data; ctx.body = convertAccount(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -155,27 +153,9 @@ export function apiAccountMastodon(router: Router): void {
try { try {
const data = await client.getAccountStatuses( const data = await client.getAccountStatuses(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
argsToBools(limitToInt(ctx.query as any)), convertTimelinesArgsId(argsToBools(limitToInt(ctx.query as any))),
); );
let resp = data.data; ctx.body = data.data.map((status) => convertStatus(status));
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx]
.in_reply_to_account_id
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
}
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -193,13 +173,9 @@ export function apiAccountMastodon(router: Router): void {
try { try {
const data = await client.getAccountFollowers( const data = await client.getAccountFollowers(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any), convertTimelinesArgsId(limitToInt(ctx.query as any)),
); );
let resp = data.data; ctx.body = data.data.map((account) => convertAccount(account));
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -217,13 +193,9 @@ export function apiAccountMastodon(router: Router): void {
try { try {
const data = await client.getAccountFollowing( const data = await client.getAccountFollowing(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any), convertTimelinesArgsId(limitToInt(ctx.query as any)),
); );
let resp = data.data; ctx.body = data.data.map((account) => convertAccount(account));
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -239,8 +211,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getAccountLists(ctx.params.id); const data = await client.getAccountLists(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data.map((list) => convertList(list));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -259,9 +233,8 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.followAccount( const data = await client.followAccount(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
); );
let acct = data.data; let acct = convertRelationship(data.data);
acct.following = true; acct.following = true;
acct.id = convertId(acct.id, IdType.MastodonId);
ctx.body = acct; ctx.body = acct;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -281,8 +254,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.unfollowAccount( const data = await client.unfollowAccount(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
); );
let acct = data.data; let acct = convertRelationship(data.data);
acct.id = convertId(acct.id, IdType.MastodonId);
acct.following = false; acct.following = false;
ctx.body = acct; ctx.body = acct;
} catch (e: any) { } catch (e: any) {
@ -303,9 +275,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.blockAccount( const data = await client.blockAccount(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
); );
let resp = data.data; ctx.body = convertRelationship(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -324,9 +294,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.unblockAccount( const data = await client.unblockAccount(
convertId(ctx.params.id, IdType.MastodonId), convertId(ctx.params.id, IdType.MastodonId),
); );
let resp = data.data; ctx.body = convertRelationship(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -346,9 +314,7 @@ export function apiAccountMastodon(router: Router): void {
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request as any).body as any, (ctx.request as any).body as any,
); );
let resp = data.data; ctx.body = convertRelationship(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -367,9 +333,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.unmuteAccount( const data = await client.unmuteAccount(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
); );
let resp = data.data; ctx.body = convertRelationship(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -383,28 +347,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = (await client.getBookmarks( const data = await client.getBookmarks(
limitToInt(ctx.query as any), convertTimelinesArgsId(limitToInt(ctx.query as any)),
)) as any; );
let resp = data.data; ctx.body = data.data.map((status) => convertStatus(status));
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx]
.in_reply_to_account_id
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
}
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -417,26 +363,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getFavourites(limitToInt(ctx.query as any)); const data = await client.getFavourites(
let resp = data.data; convertTimelinesArgsId(limitToInt(ctx.query as any)),
for (let statIdx = 0; statIdx < resp.length; statIdx++) { );
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); ctx.body = data.data.map((status) => convertStatus(status));
resp[statIdx].in_reply_to_account_id = resp[statIdx]
.in_reply_to_account_id
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
}
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -449,12 +379,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getMutes(limitToInt(ctx.query as any)); const data = await client.getMutes(
let resp = data.data; convertTimelinesArgsId(limitToInt(ctx.query as any)),
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { );
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); ctx.body = data.data.map((account) => convertAccount(account));
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -467,12 +395,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getBlocks(limitToInt(ctx.query as any)); const data = await client.getBlocks(
let resp = data.data; convertTimelinesArgsId(limitToInt(ctx.query as any)),
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { );
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); ctx.body = data.data.map((account) => convertAccount(account));
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -488,11 +414,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.getFollowRequests( const data = await client.getFollowRequests(
((ctx.query as any) || { limit: 20 }).limit, ((ctx.query as any) || { limit: 20 }).limit,
); );
let resp = data.data; ctx.body = data.data.map((account) => convertAccount(account));
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -510,9 +432,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.acceptFollowRequest( const data = await client.acceptFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
); );
let resp = data.data; ctx.body = convertRelationship(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -531,9 +451,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.rejectFollowRequest( const data = await client.rejectFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
); );
let resp = data.data; ctx.body = convertRelationship(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);

View file

@ -1,6 +1,8 @@
import megalodon, { MegalodonInterface } from "@calckey/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { IdType, convertId } from "../../index.js";
import { convertFilter } from "../converters.js";
export function apiFilterMastodon(router: Router): void { export function apiFilterMastodon(router: Router): void {
router.get("/v1/filters", async (ctx) => { router.get("/v1/filters", async (ctx) => {
@ -10,7 +12,7 @@ export function apiFilterMastodon(router: Router): void {
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.getFilters(); const data = await client.getFilters();
ctx.body = data.data; ctx.body = data.data.map((filter) => convertFilter(filter));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -24,8 +26,10 @@ export function apiFilterMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.getFilter(ctx.params.id); const data = await client.getFilter(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertFilter(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -40,7 +44,7 @@ export function apiFilterMastodon(router: Router): void {
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.createFilter(body.phrase, body.context, body); const data = await client.createFilter(body.phrase, body.context, body);
ctx.body = data.data; ctx.body = convertFilter(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -55,11 +59,11 @@ export function apiFilterMastodon(router: Router): void {
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.updateFilter( const data = await client.updateFilter(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
body.phrase, body.phrase,
body.context, body.context,
); );
ctx.body = data.data; ctx.body = convertFilter(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -73,7 +77,9 @@ export function apiFilterMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.deleteFilter(ctx.params.id); const data = await client.deleteFilter(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);

View file

@ -1,8 +1,10 @@
import megalodon, { MegalodonInterface } from "@calckey/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { koaBody } from "koa-body"; import { koaBody } from "koa-body";
import { convertId, IdType } from "../../index.js";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { toTextWithReaction } from "./timeline.js"; import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js";
import { convertNotification } from "../converters.js";
function toLimitToInt(q: any) { function toLimitToInt(q: any) {
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10); if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
return q; return q;
@ -15,9 +17,12 @@ export function apiNotificationsMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.getNotifications(toLimitToInt(ctx.query)); const data = await client.getNotifications(
convertTimelinesArgsId(toLimitToInt(ctx.query)),
);
const notfs = data.data; const notfs = data.data;
const ret = notfs.map((n) => { const ret = notfs.map((n) => {
n = convertNotification(n);
if (n.type !== "follow" && n.type !== "follow_request") { if (n.type !== "follow" && n.type !== "follow_request") {
if (n.type === "reaction") n.type = "favourite"; if (n.type === "reaction") n.type = "favourite";
n.status = toTextWithReaction( n.status = toTextWithReaction(
@ -43,8 +48,10 @@ export function apiNotificationsMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const dataRaw = await client.getNotification(ctx.params.id); const dataRaw = await client.getNotification(
const data = dataRaw.data; convertId(ctx.params.id, IdType.CalckeyId),
);
const data = convertNotification(dataRaw.data);
if (data.type !== "follow" && data.type !== "follow_request") { if (data.type !== "follow" && data.type !== "follow_request") {
if (data.type === "reaction") data.type = "favourite"; if (data.type === "reaction") data.type = "favourite";
ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0]; ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0];
@ -79,7 +86,9 @@ export function apiNotificationsMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.dismissNotification(ctx.params.id); const data = await client.dismissNotification(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);

View file

@ -3,7 +3,8 @@ import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import axios from "axios"; import axios from "axios";
import { Converter } from "@calckey/megalodon"; import { Converter } from "@calckey/megalodon";
import { limitToInt } from "./timeline.js"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
import { convertAccount, convertStatus } from "../converters.js";
export function apiSearchMastodon(router: Router): void { export function apiSearchMastodon(router: Router): void {
router.get("/v1/search", async (ctx) => { router.get("/v1/search", async (ctx) => {
@ -12,7 +13,7 @@ export function apiSearchMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const query: any = limitToInt(ctx.query); const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
const type = query.type || ""; const type = query.type || "";
const data = await client.search(query.q, type, query); const data = await client.search(query.q, type, query);
ctx.body = data.data; ctx.body = data.data;
@ -27,18 +28,20 @@ export function apiSearchMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const query: any = limitToInt(ctx.query); const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
const type = query.type; const type = query.type;
if (type) { if (type) {
const data = await client.search(query.q, type, query); const data = await client.search(query.q, type, query);
ctx.body = data.data; ctx.body = data.data.accounts.map((account) => convertAccount(account));
} else { } else {
const acct = await client.search(query.q, "accounts", query); const acct = await client.search(query.q, "accounts", query);
const stat = await client.search(query.q, "statuses", query); const stat = await client.search(query.q, "statuses", query);
const tags = await client.search(query.q, "hashtags", query); const tags = await client.search(query.q, "hashtags", query);
ctx.body = { ctx.body = {
accounts: acct.data.accounts, accounts: acct.data.accounts.map((account) =>
statuses: stat.data.statuses, convertAccount(account),
),
statuses: stat.data.statuses.map((status) => convertStatus(status)),
hashtags: tags.data.hashtags, hashtags: tags.data.hashtags,
}; };
} }
@ -57,7 +60,7 @@ export function apiSearchMastodon(router: Router): void {
ctx.request.hostname, ctx.request.hostname,
accessTokens, accessTokens,
); );
ctx.body = data; ctx.body = data.map((status) => convertStatus(status));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -69,12 +72,16 @@ export function apiSearchMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
try { try {
const query: any = ctx.query; const query: any = ctx.query;
const data = await getFeaturedUser( let data = await getFeaturedUser(
BASE_URL, BASE_URL,
ctx.request.hostname, ctx.request.hostname,
accessTokens, accessTokens,
query.limit || 20, query.limit || 20,
); );
data = data.map((suggestion) => {
suggestion.account = convertAccount(suggestion.account);
return suggestion;
});
console.log(data); console.log(data);
ctx.body = data; ctx.body = data;
} catch (e: any) { } catch (e: any) {

View file

@ -4,7 +4,14 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
import axios from "axios"; import axios from "axios";
import querystring from "node:querystring"; import querystring from "node:querystring";
import qs from "qs"; import qs from "qs";
import { limitToInt } from "./timeline.js"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
import { convertId, IdType } from "../../index.js";
import {
convertAccount,
convertAttachment,
convertPoll,
convertStatus,
} from "../converters.js";
function normalizeQuery(data: any) { function normalizeQuery(data: any) {
const str = querystring.stringify(data); const str = querystring.stringify(data);
@ -18,6 +25,8 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
let body: any = ctx.request.body; let body: any = ctx.request.body;
if (body.in_reply_to_id)
body.in_reply_to_id = convertId(body.in_reply_to_id, IdType.CalckeyId);
if ( if (
(!body.poll && body["poll[options][]"]) || (!body.poll && body["poll[options][]"]) ||
(!body.media_ids && body["media_ids[]"]) (!body.media_ids && body["media_ids[]"])
@ -54,7 +63,7 @@ export function apiStatusMastodon(router: Router): void {
body.sensitive = body.sensitive =
typeof sensitive === "string" ? sensitive === "true" : sensitive; typeof sensitive === "string" ? sensitive === "true" : sensitive;
const data = await client.postStatus(text, body); const data = await client.postStatus(text, body);
ctx.body = data.data; ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -66,8 +75,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getStatus(ctx.params.id); const data = await client.getStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -79,7 +90,9 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.deleteStatus(ctx.params.id); const data = await client.deleteStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e.response.data, request.params.id); console.error(e.response.data, request.params.id);
@ -100,10 +113,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const id = ctx.params.id; const id = convertId(ctx.params.id, IdType.CalckeyId);
const data = await client.getStatusContext( const data = await client.getStatusContext(
id, id,
limitToInt(ctx.query as any), convertTimelinesArgsId(limitToInt(ctx.query as any)),
); );
const status = await client.getStatus(id); const status = await client.getStatus(id);
let reqInstance = axios.create({ let reqInstance = axios.create({
@ -126,6 +139,12 @@ export function apiStatusMastodon(router: Router): void {
text, text,
), ),
); );
data.data.ancestors = data.data.ancestors.map((status) =>
convertStatus(status),
);
data.data.descendants = data.data.descendants.map((status) =>
convertStatus(status),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -141,8 +160,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getStatusRebloggedBy(ctx.params.id); const data = await client.getStatusRebloggedBy(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -165,11 +186,11 @@ export function apiStatusMastodon(router: Router): void {
const react = await getFirstReaction(BASE_URL, accessTokens); const react = await getFirstReaction(BASE_URL, accessTokens);
try { try {
const a = (await client.createEmojiReaction( const a = (await client.createEmojiReaction(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
react, react,
)) as any; )) as any;
//const data = await client.favouriteStatus(ctx.params.id) as any; //const data = await client.favouriteStatus(ctx.params.id) as any;
ctx.body = a.data; ctx.body = convertStatus(a.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -186,8 +207,11 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const react = await getFirstReaction(BASE_URL, accessTokens); const react = await getFirstReaction(BASE_URL, accessTokens);
try { try {
const data = await client.deleteEmojiReaction(ctx.params.id, react); const data = await client.deleteEmojiReaction(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
react,
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -203,8 +227,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.reblogStatus(ctx.params.id); const data = await client.reblogStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -220,8 +246,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.unreblogStatus(ctx.params.id); const data = await client.unreblogStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -237,8 +265,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.bookmarkStatus(ctx.params.id); const data = await client.bookmarkStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -254,8 +284,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = (await client.unbookmarkStatus(ctx.params.id)) as any; const data = await client.unbookmarkStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -271,8 +303,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.pinStatus(ctx.params.id); const data = await client.pinStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -288,8 +322,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.unpinStatus(ctx.params.id); const data = await client.unpinStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -302,8 +338,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getMedia(ctx.params.id); const data = await client.getMedia(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertAttachment(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -316,10 +354,10 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.updateMedia( const data = await client.updateMedia(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
ctx.request.body as any, ctx.request.body as any,
); );
ctx.body = data.data; ctx.body = convertAttachment(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -331,8 +369,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getPoll(ctx.params.id); const data = await client.getPoll(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertPoll(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -347,10 +387,10 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.votePoll( const data = await client.votePoll(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request.body as any).choices, (ctx.request.body as any).choices,
); );
ctx.body = data.data; ctx.body = convertPoll(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;

View file

@ -4,6 +4,8 @@ import { getClient } from "../ApiMastodonCompatibleService.js";
import { statusModel } from "./status.js"; import { statusModel } from "./status.js";
import Autolinker from "autolinker"; import Autolinker from "autolinker";
import { ParsedUrlQuery } from "querystring"; import { ParsedUrlQuery } from "querystring";
import { convertAccount, convertList, convertStatus } from "../converters.js";
import { convertId, IdType } from "../../index.js";
export function limitToInt(q: ParsedUrlQuery) { export function limitToInt(q: ParsedUrlQuery) {
let object: any = q; let object: any = q;
@ -29,6 +31,16 @@ export function argsToBools(q: ParsedUrlQuery) {
return q; return q;
} }
export function convertTimelinesArgsId(q: ParsedUrlQuery) {
if (typeof q.min_id === "string")
q.min_id = convertId(q.min_id, IdType.CalckeyId);
if (typeof q.max_id === "string")
q.max_id = convertId(q.max_id, IdType.CalckeyId);
if (typeof q.since_id === "string")
q.since_id = convertId(q.since_id, IdType.CalckeyId);
return q;
}
export function toTextWithReaction(status: Entity.Status[], host: string) { export function toTextWithReaction(status: Entity.Status[], host: string) {
return status.map((t) => { return status.map((t) => {
if (!t) return statusModel(null, null, [], "no content"); if (!t) return statusModel(null, null, [], "no content");
@ -97,9 +109,14 @@ export function apiTimelineMastodon(router: Router): void {
try { try {
const query: any = ctx.query; const query: any = ctx.query;
const data = query.local const data = query.local
? await client.getLocalTimeline(argsToBools(limitToInt(query))) ? await client.getLocalTimeline(
: await client.getPublicTimeline(argsToBools(limitToInt(query))); convertTimelinesArgsId(argsToBools(limitToInt(query))),
ctx.body = toTextWithReaction(data.data, ctx.hostname); )
: await client.getPublicTimeline(
convertTimelinesArgsId(argsToBools(limitToInt(query))),
);
let resp = data.data.map((status) => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -116,9 +133,10 @@ export function apiTimelineMastodon(router: Router): void {
try { try {
const data = await client.getTagTimeline( const data = await client.getTagTimeline(
ctx.params.hashtag, ctx.params.hashtag,
argsToBools(limitToInt(ctx.query)), convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))),
); );
ctx.body = toTextWithReaction(data.data, ctx.hostname); let resp = data.data.map((status) => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -132,8 +150,11 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getHomeTimeline(limitToInt(ctx.query)); const data = await client.getHomeTimeline(
ctx.body = toTextWithReaction(data.data, ctx.hostname); convertTimelinesArgsId(limitToInt(ctx.query)),
);
let resp = data.data.map((status) => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -149,10 +170,11 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getListTimeline( const data = await client.getListTimeline(
ctx.params.listId, convertId(ctx.params.listId, IdType.CalckeyId),
limitToInt(ctx.query), convertTimelinesArgsId(limitToInt(ctx.query)),
); );
ctx.body = toTextWithReaction(data.data, ctx.hostname); let resp = data.data.map((status) => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -166,7 +188,9 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getConversationTimeline(limitToInt(ctx.query)); const data = await client.getConversationTimeline(
convertTimelinesArgsId(limitToInt(ctx.query)),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -181,7 +205,7 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getLists(); const data = await client.getLists();
ctx.body = data.data; ctx.body = data.data.map((list) => convertList(list));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -196,8 +220,10 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getList(ctx.params.id); const data = await client.getList(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertList(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -212,7 +238,7 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.createList((ctx.request.body as any).title); const data = await client.createList((ctx.request.body as any).title);
ctx.body = data.data; ctx.body = convertList(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -227,8 +253,11 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.updateList(ctx.params.id, (ctx.request.body as any).title); const data = await client.updateList(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request.body as any).title,
);
ctx.body = convertList(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -244,7 +273,9 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.deleteList(ctx.params.id); const data = await client.deleteList(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -262,10 +293,10 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getAccountsInList( const data = await client.getAccountsInList(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
ctx.query as any, convertTimelinesArgsId(ctx.query as any),
); );
ctx.body = data.data; ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -282,8 +313,10 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.addAccountsToList( const data = await client.addAccountsToList(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
(ctx.query as any).account_ids, (ctx.query.account_ids as string[]).map((id) =>
convertId(id, IdType.CalckeyId),
),
); );
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
@ -302,8 +335,10 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.deleteAccountsFromList( const data = await client.deleteAccountsFromList(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
(ctx.query as any).account_ids, (ctx.query.account_ids as string[]).map((id) =>
convertId(id, IdType.CalckeyId),
),
); );
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {

View file

@ -55,7 +55,7 @@ export default async (ctx: Koa.Context) => {
return; return;
} }
const available = await validateEmailForAccount(emailAddress); const { available } = await validateEmailForAccount(emailAddress);
if (!available) { if (!available) {
ctx.status = 400; ctx.status = 400;
return; return;

View file

@ -399,28 +399,33 @@ router.get("/notes/:note", async (ctx, next) => {
visibility: In(["public", "home"]), visibility: In(["public", "home"]),
}); });
if (note) { try {
const _note = await Notes.pack(note); if (note) {
const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); const _note = await Notes.pack(note);
const meta = await fetchMeta();
await ctx.render("note", {
note: _note,
profile,
avatarUrl: await Users.getAvatarUrl(
await Users.findOneByOrFail({ id: note.userId }),
),
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
instanceName: meta.name || "Calckey",
icon: meta.iconUrl,
privateMode: meta.privateMode,
themeColor: meta.themeColor,
});
ctx.set("Cache-Control", "public, max-age=15"); const profile = await UserProfiles.findOneByOrFail({
userId: note.userId,
});
const meta = await fetchMeta();
await ctx.render("note", {
note: _note,
profile,
avatarUrl: await Users.getAvatarUrl(
await Users.findOneByOrFail({ id: note.userId }),
),
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
instanceName: meta.name || "Calckey",
icon: meta.iconUrl,
privateMode: meta.privateMode,
themeColor: meta.themeColor,
});
return; ctx.set("Cache-Control", "public, max-age=15");
}
return;
}
} catch {}
await next(); await next();
}); });

View file

@ -6,11 +6,13 @@ import {
NoteThreadMutings, NoteThreadMutings,
UserProfiles, UserProfiles,
Users, Users,
Followings,
} from "@/models/index.js"; } from "@/models/index.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import type { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js"; import type { Notification } from "@/models/entities/notification.js";
import { sendEmailNotification } from "./send-email-notification.js"; import { sendEmailNotification } from "./send-email-notification.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
export async function createNotification( export async function createNotification(
notifieeId: User["id"], notifieeId: User["id"],
@ -21,6 +23,26 @@ export async function createNotification(
return null; return null;
} }
if (
data.notifierId &&
["mention", "reply", "renote", "quote", "reaction"].includes(type)
) {
const notifier = await Users.findOneBy({ id: data.notifierId });
// suppress if the notifier does not exist or is silenced.
if (!notifier) return null;
// suppress if the notifier is silenced or in a silenced instance, and not followed by the notifiee.
if (
(notifier.isSilenced ||
(Users.isRemoteUser(notifier) &&
(await shouldSilenceInstance(notifier.host)))) &&
!(await Followings.exist({
where: { followerId: notifieeId, followeeId: data.notifierId },
}))
)
return null;
}
const profile = await UserProfiles.findOneBy({ userId: notifieeId }); const profile = await UserProfiles.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type); const isMuted = profile?.mutingNotificationTypes.includes(type);

View file

@ -27,6 +27,7 @@ import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js
import type { Packed } from "@/misc/schema.js"; import type { Packed } from "@/misc/schema.js";
import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { webhookDeliver } from "@/queue/index.js"; import { webhookDeliver } from "@/queue/index.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
const logger = new Logger("following/create"); const logger = new Logger("following/create");
@ -226,13 +227,19 @@ export default async function (
}); });
// フォロー対象が鍵アカウントである or // フォロー対象が鍵アカウントである or
// The follower is silenced, or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
// The follower is remote, the followee is local, and the follower is in a silenced instance.
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if ( if (
followee.isLocked || followee.isLocked ||
follower.isSilenced ||
(followeeProfile.carefulBot && follower.isBot) || (followeeProfile.carefulBot && follower.isBot) ||
(Users.isLocalUser(follower) && Users.isRemoteUser(followee)) (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) ||
(Users.isRemoteUser(follower) &&
Users.isLocalUser(followee) &&
(await shouldSilenceInstance(follower.host)))
) { ) {
let autoAccept = false; let autoAccept = false;

View file

@ -6,6 +6,7 @@ import type { User } from "@/models/entities/user.js";
import { Blockings, FollowRequests, Users } from "@/models/index.js"; import { Blockings, FollowRequests, Users } from "@/models/index.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { createNotification } from "../../create-notification.js"; import { createNotification } from "../../create-notification.js";
import config from "@/config/index.js";
export default async function ( export default async function (
follower: { follower: {
@ -79,7 +80,13 @@ export default async function (
} }
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderFollow(follower, followee)); const content = renderActivity(
renderFollow(
follower,
followee,
requestId ?? `${config.url}/follows/${followRequest.id}`,
),
);
deliver(follower, content, followee.inbox); deliver(follower, content, followee.inbox);
} }
} }

View file

@ -39,7 +39,7 @@ import {
} from "@/models/index.js"; } from "@/models/index.js";
import type { DriveFile } from "@/models/entities/drive-file.js"; import type { DriveFile } from "@/models/entities/drive-file.js";
import type { App } from "@/models/entities/app.js"; import type { App } from "@/models/entities/app.js";
import { Not, In } from "typeorm"; import { Not, In, IsNull } from "typeorm";
import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { import {
@ -66,6 +66,7 @@ import { Cache } from "@/misc/cache.js";
import type { UserProfile } from "@/models/entities/user-profile.js"; import type { UserProfile } from "@/models/entities/user-profile.js";
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
const mutedWordsCache = new Cache< const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@ -166,6 +167,7 @@ export default async (
data: Option, data: Option,
silent = false, silent = false,
) => ) =>
// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
new Promise<Note>(async (res, rej) => { new Promise<Note>(async (res, rej) => {
// If you reply outside the channel, match the scope of the target. // If you reply outside the channel, match the scope of the target.
// TODO (I think it's a process that could be done on the client side, but it's server side for now.) // TODO (I think it's a process that could be done on the client side, but it's server side for now.)
@ -203,6 +205,15 @@ export default async (
data.visibility = "home"; data.visibility = "home";
} }
// Enforce home visibility if the user is in a silenced instance.
if (
data.visibility === "public" &&
Users.isRemoteUser(user) &&
(await shouldSilenceInstance(user.host))
) {
data.visibility = "home";
}
// Reject if the target of the renote is a public range other than "Home or Entire". // Reject if the target of the renote is a public range other than "Home or Entire".
if ( if (
data.renote && data.renote &&

View file

@ -118,7 +118,7 @@ export default async (
userId: user.id, userId: user.id,
}); });
// リアクションされたユーザーがローカルユーザーなら通知を作成 // Create notification if the reaction target is a local user.
if (note.userHost === null) { if (note.userHost === null) {
createNotification(note.userId, "reaction", { createNotification(note.userId, "reaction", {
notifierId: user.id, notifierId: user.id,
@ -143,7 +143,7 @@ export default async (
} }
}); });
//#region 配信 //#region deliver
if (Users.isLocalUser(user) && !note.localOnly) { if (Users.isLocalUser(user) && !note.localOnly) {
const content = renderActivity(await renderLike(record, note)); const content = renderActivity(await renderLike(record, note));
const dm = new DeliverManager(user, content); const dm = new DeliverManager(user, content);

View file

@ -55,6 +55,7 @@ export type Endpoints = {
"admin/get-table-stats": { req: TODO; res: TODO }; "admin/get-table-stats": { req: TODO; res: TODO };
"admin/invite": { req: TODO; res: TODO }; "admin/invite": { req: TODO; res: TODO };
"admin/logs": { req: TODO; res: TODO }; "admin/logs": { req: TODO; res: TODO };
"admin/meta": { req: TODO; res: TODO };
"admin/reset-password": { req: TODO; res: TODO }; "admin/reset-password": { req: TODO; res: TODO };
"admin/resolve-abuse-user-report": { req: TODO; res: TODO }; "admin/resolve-abuse-user-report": { req: TODO; res: TODO };
"admin/resync-chart": { req: TODO; res: TODO }; "admin/resync-chart": { req: TODO; res: TODO };

View file

@ -32,7 +32,7 @@
"autosize": "5.0.2", "autosize": "5.0.2",
"blurhash": "1.1.5", "blurhash": "1.1.5",
"broadcast-channel": "4.19.1", "broadcast-channel": "4.19.1",
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git", "browser-image-resizer": "github:misskey-dev/browser-image-resizer",
"calckey-js": "workspace:*", "calckey-js": "workspace:*",
"chart.js": "4.1.1", "chart.js": "4.1.1",
"chartjs-adapter-date-fns": "2.0.1", "chartjs-adapter-date-fns": "2.0.1",

View file

@ -195,8 +195,7 @@ function onMousedown(evt: MouseEvent): void {
} }
&:focus-visible { &:focus-visible {
outline: solid 2px var(--focus); outline: auto;
outline-offset: 2px;
} }
&.inline { &.inline {

View file

@ -1,5 +1,6 @@
<template> <template>
<button <button
ref="el"
class="_button" class="_button"
:class="{ showLess: modelValue, fade: !modelValue }" :class="{ showLess: modelValue, fade: !modelValue }"
@click.stop="toggle" @click.stop="toggle"
@ -12,7 +13,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed, ref } from "vue";
import { length } from "stringz"; import { length } from "stringz";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { concat } from "@/scripts/array"; import { concat } from "@/scripts/array";
@ -27,6 +28,8 @@ const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void; (ev: "update:modelValue", v: boolean): void;
}>(); }>();
const el = ref<HTMLElement>();
const label = computed(() => { const label = computed(() => {
return concat([ return concat([
props.note.text props.note.text
@ -43,6 +46,14 @@ const label = computed(() => {
const toggle = () => { const toggle = () => {
emit("update:modelValue", !props.modelValue); emit("update:modelValue", !props.modelValue);
}; };
function focus() {
el.value.focus();
}
defineExpose({
focus,
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -62,9 +73,46 @@ const toggle = () => {
} }
} }
} }
&:hover > span { &:hover > span,
&:focus > span {
background: var(--cwFg) !important; background: var(--cwFg) !important;
color: var(--cwBg) !important; color: var(--cwBg) !important;
} }
&.fade {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
&.showLess {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
}
} }
</style> </style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div ref="thumbnail" class="zdjebgpv"> <button ref="thumbnail" class="zdjebgpv">
<ImgWithBlurhash <ImgWithBlurhash
v-if="isThumbnailAvailable" v-if="isThumbnailAvailable"
:hash="file.blurhash" :hash="file.blurhash"
@ -36,7 +36,7 @@
v-if="isThumbnailAvailable && is === 'video'" v-if="isThumbnailAvailable && is === 'video'"
class="ph-file-video ph-bold ph-lg icon-sub" class="ph-file-video ph-bold ph-lg icon-sub"
></i> ></i>
</div> </button>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => {
background: var(--panel); background: var(--panel);
border-radius: 8px; border-radius: 8px;
overflow: clip; overflow: clip;
border: 0;
padding: 0;
cursor: pointer;
> .icon-sub { > .icon-sub {
position: absolute; position: absolute;

View file

@ -1,157 +1,160 @@
<template> <template>
<div <FocusTrap v-bind:active="isActive">
class="omfetrab" <div
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" class="omfetrab"
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
> :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
<input tabindex="-1"
ref="search" >
v-model.trim="q" <input
class="search" ref="search"
data-prevent-emoji-insert v-model.trim="q"
:class="{ filled: q != null && q != '' }" class="search"
:placeholder="i18n.ts.search" data-prevent-emoji-insert
type="search" :class="{ filled: q != null && q != '' }"
@paste.stop="paste" :placeholder="i18n.ts.search"
@keyup.enter="done()" type="search"
/> @paste.stop="paste"
<div ref="emojis" class="emojis"> @keyup.enter="done()"
<section class="result"> />
<div v-if="searchResultCustom.length > 0" class="body"> <div ref="emojis" class="emojis">
<button <section class="result">
v-for="emoji in searchResultCustom" <div v-if="searchResultCustom.length > 0" class="body">
:key="emoji.id"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img
class="emoji"
:src="
disableShowingAnimatedImages
? getStaticImageUrl(emoji.url)
: emoji.url
"
/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji.char" />
</button>
</div>
</section>
<div v-if="tab === 'index'" class="group index">
<section v-if="showPinned">
<div class="body">
<button <button
v-for="emoji in pinned" v-for="emoji in searchResultCustom"
:key="emoji" :key="emoji.id"
class="_button item" class="_button item"
:title="emoji.name"
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<MkEmoji <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img
class="emoji" class="emoji"
:emoji="emoji" :src="
:normal="true" disableShowingAnimatedImages
? getStaticImageUrl(emoji.url)
: emoji.url
"
/> />
</button> </button>
</div> </div>
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji.char" />
</button>
</div>
</section> </section>
<section> <div v-if="tab === 'index'" class="group index">
<header class="_acrylic"> <section v-if="showPinned">
<i class="ph-alarm ph-bold ph-fw ph-lg"></i> <div class="body">
{{ i18n.ts.recentUsed }} <button
</header> v-for="emoji in pinned"
<div class="body"> :key="emoji"
<button class="_button item"
v-for="emoji in recentlyUsedEmojis" tabindex="0"
:key="emoji" @click="chosen(emoji, $event)"
class="_button item" >
@click="chosen(emoji, $event)" <MkEmoji
> class="emoji"
<MkEmoji :emoji="emoji"
class="emoji" :normal="true"
:emoji="emoji" />
:normal="true" </button>
/> </div>
</button> </section>
</div>
</section> <section>
<header class="_acrylic">
<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
{{ i18n.ts.recentUsed }}
</header>
<div class="body">
<button
v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button item"
@click="chosen(emoji, $event)"
>
<MkEmoji
class="emoji"
:emoji="emoji"
:normal="true"
/>
</button>
</div>
</section>
</div>
<div v-once class="group">
<header>{{ i18n.ts.customEmojis }}</header>
<XSection
v-for="category in customEmojiCategories"
:key="'custom:' + category"
:initial-shown="false"
:emojis="
customEmojis
.filter((e) => e.category === category)
.map((e) => ':' + e.name + ':')
"
@chosen="chosen"
>{{ category || i18n.ts.other }}</XSection
>
</div>
<div v-once class="group">
<header>{{ i18n.ts.emoji }}</header>
<XSection
v-for="category in categories"
:key="category"
:emojis="
emojilist
.filter((e) => e.category === category)
.map((e) => e.char)
"
@chosen="chosen"
>{{ category }}</XSection
>
</div>
</div> </div>
<div v-once class="group"> <div class="tabs">
<header>{{ i18n.ts.customEmojis }}</header> <button
<XSection class="_button tab"
v-for="category in customEmojiCategories" :class="{ active: tab === 'index' }"
:key="'custom:' + category" @click="tab = 'index'"
:initial-shown="false"
:emojis="
customEmojis
.filter((e) => e.category === category)
.map((e) => ':' + e.name + ':')
"
@chosen="chosen"
>{{ category || i18n.ts.other }}</XSection
> >
</div> <i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
<div v-once class="group"> </button>
<header>{{ i18n.ts.emoji }}</header> <button
<XSection class="_button tab"
v-for="category in categories" :class="{ active: tab === 'custom' }"
:key="category" @click="tab = 'custom'"
:emojis="
emojilist
.filter((e) => e.category === category)
.map((e) => e.char)
"
@chosen="chosen"
>{{ category }}</XSection
> >
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'unicode' }"
@click="tab = 'unicode'"
>
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'tags' }"
@click="tab = 'tags'"
>
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
</button>
</div> </div>
</div> </div>
<div class="tabs"> </FocusTrap>
<button
class="_button tab"
:class="{ active: tab === 'index' }"
@click="tab = 'index'"
>
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'custom' }"
@click="tab = 'custom'"
>
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'unicode' }"
@click="tab = 'unicode'"
>
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'tags' }"
@click="tab = 'tags'"
>
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
</button>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -171,6 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance"; import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { FocusTrap } from "focus-trap-vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View file

@ -5,6 +5,7 @@
{ {
yellow: instance.isNotResponding, yellow: instance.isNotResponding,
red: instance.isBlocked, red: instance.isBlocked,
purple: instance.isSilenced,
gray: instance.isSuspended, gray: instance.isSuspended,
}, },
]" ]"
@ -23,13 +24,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as misskey from "calckey-js"; import * as calckey from "calckey-js";
import MkMiniChart from "@/components/MkMiniChart.vue"; import MkMiniChart from "@/components/MkMiniChart.vue";
import * as os from "@/os"; import * as os from "@/os";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
const props = defineProps<{ const props = defineProps<{
instance: misskey.entities.Instance; instance: calckey.entities.Instance;
}>(); }>();
let chartValues = $ref<number[] | null>(null); let chartValues = $ref<number[] | null>(null);
@ -135,6 +136,21 @@ function getInstanceIcon(instance): string {
background-size: 16px 16px; background-size: 16px 16px;
} }
&:global(.purple) {
--c: rgba(196, 0, 255, 0.15);
background-image: linear-gradient(
45deg,
var(--c) 16.67%,
transparent 16.67%,
transparent 50%,
var(--c) 50%,
var(--c) 66.67%,
transparent 66.67%,
transparent 100%
);
background-size: 16px 16px;
}
&:global(.gray) { &:global(.gray) {
--c: var(--bg); --c: var(--bg);
background-image: linear-gradient( background-image: linear-gradient(

View file

@ -139,7 +139,8 @@ function close() {
height: 100px; height: 100px;
border-radius: 10px; border-radius: 10px;
&:hover { &:hover,
&:focus-visible {
color: var(--accent); color: var(--accent);
background: var(--accentedBg); background: var(--accentedBg);
text-decoration: none; text-decoration: none;

View file

@ -138,6 +138,10 @@ watch(
background-position: center; background-position: center;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
box-sizing: border-box;
&:focus-visible {
border: 2px solid var(--accent);
}
> .gif { > .gif {
background-color: var(--fg); background-color: var(--fg);

View file

@ -1,5 +1,5 @@
<template> <template>
<div ref="el" class="sfhdhdhr"> <div ref="el" class="sfhdhdhr" tabindex="-1">
<MkMenu <MkMenu
ref="menu" ref="menu"
:items="items" :items="items"
@ -23,7 +23,6 @@ import {
} from "vue"; } from "vue";
import MkMenu from "./MkMenu.vue"; import MkMenu from "./MkMenu.vue";
import { MenuItem } from "@/types/menu"; import { MenuItem } from "@/types/menu";
import * as os from "@/os";
const props = defineProps<{ const props = defineProps<{
items: MenuItem[]; items: MenuItem[];

View file

@ -1,191 +1,202 @@
<template> <template>
<div> <FocusTrap :active="false" ref="focusTrap">
<div <div tabindex="-1">
ref="itemsEl" <div
v-hotkey="keymap" ref="itemsEl"
class="rrevdjwt _popup _shadow" class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }" :class="{ center: align === 'center', asDrawer }"
:style="{ :style="{
width: width && !asDrawer ? width + 'px' : '', width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '',
}" }"
@contextmenu.self="(e) => e.preventDefault()" @contextmenu.self="(e) => e.preventDefault()"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div> <div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item"> <span v-else-if="item.type === 'label'" class="label item">
<span :style="item.textStyle || ''">{{ item.text }}</span> <span :style="item.textStyle || ''">{{
</span> item.text
<span }}</span>
v-else-if="item.type === 'pending'"
:tabindex="i"
class="pending item"
>
<span><MkEllipsis /></span>
</span>
<MkA
v-else-if="item.type === 'link'"
:to="item.to"
:tabindex="i"
class="_button item"
@click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span> </span>
<MkAvatar <span
v-if="item.avatar" v-else-if="item.type === 'pending'"
:user="item.avatar" class="pending item"
class="avatar"
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</MkA>
<a
v-else-if="item.type === 'a'"
:href="item.href"
:target="item.target"
:download="item.download"
:tabindex="i"
class="_button item"
@click="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</a>
<button
v-else-if="item.type === 'user' && !items.hidden"
:tabindex="i"
class="_button item"
:class="{ active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<MkAvatar :user="item.user" class="avatar" /><MkUserName
:user="item.user"
/>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
<span
v-else-if="item.type === 'switch'"
:tabindex="i"
class="item"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<FormSwitch
v-model="item.ref"
:disabled="item.disabled"
class="form-switch"
:style="item.textStyle || ''"
>{{ item.text }}</FormSwitch
> >
<span><MkEllipsis /></span>
</span>
<MkA
v-else-if="item.type === 'link'"
:to="item.to"
class="_button item"
@click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
disableLink
/>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</MkA>
<a
v-else-if="item.type === 'a'"
:href="item.href"
:target="item.target"
:download="item.download"
class="_button item"
@click="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</a>
<button
v-else-if="item.type === 'user' && !items.hidden"
class="_button item"
:class="{ active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<MkAvatar
:user="item.user"
class="avatar"
disableLink
/><MkUserName :user="item.user" />
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
<span
v-else-if="item.type === 'switch'"
class="item"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<FormSwitch
v-model="item.ref"
:disabled="item.disabled"
class="form-switch"
:style="item.textStyle || ''"
>{{ item.text }}</FormSwitch
>
</span>
<button
v-else-if="item.type === 'parent'"
class="_button item parent"
:class="{ childShowing: childShowingItem === item }"
@mouseenter="showChildren(item, $event)"
@click="showChildren(item, $event)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<span class="caret"
><i
class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"
></i
></span>
</button>
<button
v-else-if="!item.hidden"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
disableLink
/>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span>{{ i18n.ts.none }}</span>
</span> </span>
<button </div>
v-else-if="item.type === 'parent'" <div v-if="childMenu" class="child">
:tabindex="i" <XChild
class="_button item parent" ref="child"
:class="{ childShowing: childShowingItem === item }" :items="childMenu"
@mouseenter="showChildren(item, $event)" :target-element="childTarget"
> :root-element="itemsEl"
<i showing
v-if="item.icon" @actioned="childActioned"
class="ph-fw ph-lg" />
:class="item.icon" </div>
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span class="caret"
><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
></span>
</button>
<button
v-else-if="!item.hidden"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span>{{ i18n.ts.none }}</span>
</span>
</div> </div>
<div v-if="childMenu" class="child"> </FocusTrap>
<XChild
ref="child"
:items="childMenu"
:target-element="childTarget"
:root-element="itemsEl"
showing
@actioned="childActioned"
/>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -206,8 +217,10 @@ import FormSwitch from "@/components/form/switch.vue";
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu"; import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { FocusTrap } from "focus-trap-vue";
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue")); const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
const focusTrap = ref();
const props = defineProps<{ const props = defineProps<{
items: MenuItem[]; items: MenuItem[];
@ -228,12 +241,6 @@ let items2: InnerMenuItem[] = $ref([]);
let child = $ref<InstanceType<typeof XChild>>(); let child = $ref<InstanceType<typeof XChild>>();
let keymap = computed(() => ({
"up|k|shift+tab": focusUp,
"down|j|tab": focusDown,
esc: close,
}));
let childShowingItem = $ref<MenuItem | null>(); let childShowingItem = $ref<MenuItem | null>();
watch( watch(
@ -324,6 +331,8 @@ function focusDown() {
} }
onMounted(() => { onMounted(() => {
focusTrap.value.activate();
if (props.viaKeyboard) { if (props.viaKeyboard) {
nextTick(() => { nextTick(() => {
focusNext(itemsEl.children[0], true, false); focusNext(itemsEl.children[0], true, false);
@ -364,8 +373,7 @@ onBeforeUnmount(() => {
font-size: 0.9em; font-size: 0.9em;
line-height: 20px; line-height: 20px;
text-align: left; text-align: left;
overflow: hidden; outline: none;
text-overflow: ellipsis;
&:before { &:before {
content: ""; content: "";
@ -389,7 +397,8 @@ onBeforeUnmount(() => {
transform: translateY(0em); transform: translateY(0em);
} }
&:not(:disabled):hover { &:not(:disabled):hover,
&:focus-visible {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
@ -397,6 +406,9 @@ onBeforeUnmount(() => {
background: var(--accentedBg); background: var(--accentedBg);
} }
} }
&:focus-visible:before {
outline: auto;
}
&.danger { &.danger {
color: #eb6f92; color: #eb6f92;

View file

@ -14,54 +14,62 @@
:duration="transitionDuration" :duration="transitionDuration"
appear appear
@after-leave="emit('closed')" @after-leave="emit('closed')"
@keyup.esc="emit('click')"
@enter="emit('opening')" @enter="emit('opening')"
@after-enter="onOpened" @after-enter="onOpened"
> >
<div <FocusTrap v-model:active="isActive">
v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap"
:class="[
$style.root,
{
[$style.drawer]: type === 'drawer',
[$style.dialog]: type === 'dialog' || type === 'dialog:top',
[$style.popup]: type === 'popup',
},
]"
:style="{
zIndex,
pointerEvents: (manualShowing != null ? manualShowing : showing)
? 'auto'
: 'none',
'--transformOrigin': transformOrigin,
}"
>
<div <div
class="_modalBg data-cy-bg" v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap"
:class="[ :class="[
$style.bg, $style.root,
{ {
[$style.bgTransparent]: isEnableBgTransparent, [$style.drawer]: type === 'drawer',
'data-cy-transparent': isEnableBgTransparent, [$style.dialog]:
type === 'dialog' || type === 'dialog:top',
[$style.popup]: type === 'popup',
}, },
]" ]"
:style="{ zIndex }" :style="{
@click="onBgClick" zIndex,
@mousedown="onBgClick" pointerEvents: (
@contextmenu.prevent.stop="() => {}" manualShowing != null ? manualShowing : showing
></div> )
<div ? 'auto'
ref="content" : 'none',
:class="[ '--transformOrigin': transformOrigin,
$style.content, }"
{ [$style.fixed]: fixed, top: type === 'dialog:top' }, tabindex="-1"
]" v-focus
:style="{ zIndex }"
@click.self="onBgClick"
> >
<slot :max-height="maxHeight" :type="type"></slot> <div
class="_modalBg data-cy-bg"
:class="[
$style.bg,
{
[$style.bgTransparent]: isEnableBgTransparent,
'data-cy-transparent': isEnableBgTransparent,
},
]"
:style="{ zIndex }"
@click="onBgClick"
@mousedown="onBgClick"
@contextmenu.prevent.stop="() => {}"
></div>
<div
ref="content"
:class="[
$style.content,
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
]"
:style="{ zIndex }"
@click.self="onBgClick"
>
<slot :max-height="maxHeight" :type="type"></slot>
</div>
</div> </div>
</div> </FocusTrap>
</Transition> </Transition>
</template> </template>
@ -71,6 +79,7 @@ import * as os from "@/os";
import { isTouchUsing } from "@/scripts/touch"; import { isTouchUsing } from "@/scripts/touch";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind"; import { deviceKind } from "@/scripts/device-kind";
import { FocusTrap } from "focus-trap-vue";
function getFixedContainer(el: Element | null): Element | null { function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === "BODY") return null; if (el == null || el.tagName === "BODY") return null;
@ -166,6 +175,7 @@ let transitionDuration = $computed(() =>
let contentClicking = false; let contentClicking = false;
const focusedElement = document.activeElement;
function close(opts: { useSendAnimation?: boolean } = {}) { function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) { if (opts.useSendAnimation) {
useSendAnime = true; useSendAnime = true;
@ -175,10 +185,12 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
if (props.src) props.src.style.pointerEvents = "auto"; if (props.src) props.src.style.pointerEvents = "auto";
showing = false; showing = false;
emit("close"); emit("close");
focusedElement.focus();
} }
function onBgClick() { function onBgClick() {
if (contentClicking) return; if (contentClicking) return;
focusedElement.focus();
emit("click"); emit("click");
} }
@ -481,6 +493,7 @@ defineExpose({
} }
.root { .root {
outline: none;
&.dialog { &.dialog {
> .content { > .content {
position: fixed; position: fixed;

View file

@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) {
flex-direction: column; flex-direction: column;
contain: content; contain: content;
border-radius: var(--radius); border-radius: var(--radius);
margin: auto;
--root-margin: 24px; --root-margin: 24px;

View file

@ -3,59 +3,64 @@
ref="modal" ref="modal"
:prefer-type="'dialog'" :prefer-type="'dialog'"
@click="onBgClick" @click="onBgClick"
@keyup.esc="$emit('close')"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<div <FocusTrap v-model:active="isActive">
ref="rootEl" <div
class="ebkgoccj" ref="rootEl"
:style="{ class="ebkgoccj"
width: `${width}px`, :style="{
height: scroll width: `${width}px`,
? height height: scroll
? `${height}px` ? height
: null ? `${height}px`
: height : null
? `min(${height}px, 100%)` : height
: '100%', ? `min(${height}px, 100%)`
}" : '100%',
@keydown="onKeydown" }"
> @keydown="onKeydown"
<div ref="headerEl" class="header"> tabindex="-1"
<button >
v-if="withOkButton" <div ref="headerEl" class="header">
class="_button" <button
@click="$emit('close')" v-if="withOkButton"
> class="_button"
<i class="ph-x ph-bold ph-lg"></i> @click="$emit('close')"
</button> >
<span class="title"> <i class="ph-x ph-bold ph-lg"></i>
<slot name="header"></slot> </button>
</span> <span class="title">
<button <slot name="header"></slot>
v-if="!withOkButton" </span>
class="_button" <button
@click="$emit('close')" v-if="!withOkButton"
> class="_button"
<i class="ph-x ph-bold ph-lg"></i> @click="$emit('close')"
</button> >
<button <i class="ph-x ph-bold ph-lg"></i>
v-if="withOkButton" </button>
class="_button" <button
:disabled="okButtonDisabled" v-if="withOkButton"
@click="$emit('ok')" class="_button"
> :disabled="okButtonDisabled"
<i class="ph-check ph-bold ph-lg"></i> @click="$emit('ok')"
</button> >
<i class="ph-check ph-bold ph-lg"></i>
</button>
</div>
<div class="body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div> </div>
<div class="body"> </FocusTrap>
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
</MkModal> </MkModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
import { FocusTrap } from "focus-trap-vue";
import MkModal from "./MkModal.vue"; import MkModal from "./MkModal.vue";
const props = withDefaults( const props = withDefaults(

View file

@ -84,6 +84,7 @@
:detailedView="detailedView" :detailedView="detailedView"
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
@push="(e) => router.push(notePage(e))" @push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()"
></MkSubNoteContent> ></MkSubNoteContent>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -117,7 +118,7 @@
<MkTime :time="appearNote.createdAt" mode="absolute" /> <MkTime :time="appearNote.createdAt" mode="absolute" />
</MkA> </MkA>
</div> </div>
<footer ref="el" class="footer" @click.stop> <footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -278,6 +279,7 @@ const isRenote =
note.poll == null; note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
@ -298,8 +300,8 @@ const keymap = {
r: () => reply(true), r: () => reply(true),
"e|a|plus": () => react(true), "e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true), q: () => renoteButton.value.renote(true),
"up|k|shift+tab": focusBefore, "up|k": focusBefore,
"down|j|tab": focusAfter, "down|j": focusAfter,
esc: blur, esc: blur,
"m|o": () => menu(true), "m|o": () => menu(true),
s: () => showContent.value !== showContent.value, s: () => showContent.value !== showContent.value,

View file

@ -1,6 +1,6 @@
<template> <template>
<div v-size="{ min: [350, 500] }" class="fefdfafb"> <div v-size="{ min: [350, 500] }" class="fefdfafb">
<MkAvatar class="avatar" :user="$i" /> <MkAvatar class="avatar" :user="$i" disableLink />
<div class="main"> <div class="main">
<div class="header"> <div class="header">
<MkUserName :user="$i" /> <MkUserName :user="$i" />

View file

@ -26,6 +26,7 @@
:note="note" :note="note"
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
:conversation="conversation" :conversation="conversation"
@focusfooter="footerEl.focus()"
/> />
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -46,7 +47,7 @@
</div> </div>
</div> </div>
</div> </div>
<footer class="footer" @click.stop> <footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -211,6 +212,7 @@ const isRenote =
note.poll == null; note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();

View file

@ -7,6 +7,8 @@
:transparent-bg="true" :transparent-bg="true"
@click="modal.close()" @click="modal.close()"
@closed="emit('closed')" @closed="emit('closed')"
tabindex="-1"
v-focus
> >
<MkMenu <MkMenu
:items="items" :items="items"

View file

@ -462,15 +462,26 @@ if (
props.reply && props.reply &&
["home", "followers", "specified"].includes(props.reply.visibility) ["home", "followers", "specified"].includes(props.reply.visibility)
) { ) {
visibility = props.reply.visibility; if (props.reply.visibility === "home" && visibility === "followers") {
if (props.reply.visibility === "specified") { visibility = "followers";
os.api("users/show", { } else if (
userIds: props.reply.visibleUserIds.filter( ["home", "followers"].includes(props.reply.visibility) &&
(uid) => uid !== $i.id && uid !== props.reply.userId visibility === "specified"
), ) {
}).then((users) => { visibility = "specified";
users.forEach(pushVisibleUser); } else {
}); visibility = props.reply.visibility;
}
if (visibility === "specified") {
if (props.reply.visibleUserIds) {
os.api("users/show", {
userIds: props.reply.visibleUserIds.filter(
(uid) => uid !== $i.id && uid !== props.reply.userId
),
}).then((users) => {
users.forEach(pushVisibleUser);
});
}
if (props.reply.userId !== $i.id) { if (props.reply.userId !== $i.id) {
os.api("users/show", { userId: props.reply.userId }).then( os.api("users/show", { userId: props.reply.userId }).then(

View file

@ -198,7 +198,6 @@ export default defineComponent({
height: 64px; height: 64px;
margin-right: 4px; margin-right: 4px;
border-radius: 4px; border-radius: 4px;
overflow: hidden;
cursor: move; cursor: move;
&:hover > .remove { &:hover > .remove {

View file

@ -0,0 +1,60 @@
<template>
<button v-if="modelValue" class="fade _button" @click.stop="toggle">
<span>{{ i18n.ts.showMore }}</span>
</button>
<button v-if="!modelValue" class="showLess _button" @click.stop="toggle">
<span>{{ i18n.ts.showLess }}</span>
</button>
</template>
<script lang="ts" setup>
import { i18n } from "@/i18n";
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void;
}>();
const toggle = () => {
emit("update:modelValue", !props.modelValue);
};
</script>
<style lang="scss" scoped>
.fade {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
.showLess {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
}
</style>

View file

@ -35,7 +35,20 @@
class="content" class="content"
:class="{ collapsed, isLong, showContent: note.cw && !showContent }" :class="{ collapsed, isLong, showContent: note.cw && !showContent }"
> >
<div class="body"> <XCwButton
ref="cwButton"
v-if="note.cw && !showContent"
v-model="showContent"
:note="note"
v-on:keydown="focusFooter"
/>
<div
class="body"
v-bind="{
'aria-label': !showContent ? '' : null,
tabindex: !showContent ? '-1' : null,
}"
>
<span v-if="note.deletedAt" style="opacity: 0.5" <span v-if="note.deletedAt" style="opacity: 0.5"
>({{ i18n.ts.deleted }})</span >({{ i18n.ts.deleted }})</span
> >
@ -96,34 +109,30 @@
<XNoteSimple :note="note.renote" /> <XNoteSimple :note="note.renote" />
</div> </div>
</template> </template>
<div
v-if="note.cw && !showContent"
tabindex="0"
v-on:focus="cwButton?.focus()"
></div>
</div> </div>
<button <XShowMoreButton
v-if="isLong && collapsed" v-if="isLong"
class="fade _button" v-model="collapsed"
@click.stop="collapsed = false" ></XShowMoreButton>
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-if="isLong && !collapsed"
class="showLess _button"
@click.stop="collapsed = true"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
<XCwButton v-if="note.cw" v-model="showContent" :note="note" /> <XCwButton v-if="note.cw" v-model="showContent" :note="note" />
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue"; import { ref } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MkNoteSimple.vue";
import XMediaList from "@/components/MkMediaList.vue"; import XMediaList from "@/components/MkMediaList.vue";
import XPoll from "@/components/MkPoll.vue"; import XPoll from "@/components/MkPoll.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue"; import MkUrlPreview from "@/components/MkUrlPreview.vue";
import XShowMoreButton from "@/components/MkShowMoreButton.vue";
import XCwButton from "@/components/MkCwButton.vue"; import XCwButton from "@/components/MkCwButton.vue";
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm"; import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -138,19 +147,28 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "push", v): void; (ev: "push", v): void;
(ev: "focusfooter"): void;
}>(); }>();
const cwButton = ref<HTMLElement>();
const isLong = const isLong =
!props.detailedView && !props.detailedView &&
props.note.cw == null && props.note.cw == null &&
props.note.text != null && props.note.text != null &&
(props.note.text.split("\n").length > 9 || props.note.text.length > 500); (props.note.text.split("\n").length > 9 || props.note.text.length > 500);
const collapsed = $ref(props.note.cw == null && isLong); const collapsed = $ref(props.note.cw == null && isLong);
const urls = props.note.text const urls = props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5) ? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
: null; : null;
let showContent = $ref(false); let showContent = $ref(false);
function focusFooter(ev) {
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
emit("focusfooter");
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -242,6 +260,9 @@ let showContent = $ref(false);
margin-top: -50px; margin-top: -50px;
padding-top: 50px; padding-top: 50px;
overflow: hidden; overflow: hidden;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
} }
&.collapsed > .body { &.collapsed > .body {
box-sizing: border-box; box-sizing: border-box;
@ -264,43 +285,6 @@ let showContent = $ref(false);
top: 40px; top: 40px;
} }
} }
:deep(.fade) {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
}
:deep(.showLess) {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
} }
} }
} }

View file

@ -9,7 +9,6 @@
v-if="item.type === 'a'" v-if="item.type === 'a'"
:href="item.href" :href="item.href"
:target="item.target" :target="item.target"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
> >
@ -22,7 +21,6 @@
</a> </a>
<button <button
v-else-if="item.type === 'button'" v-else-if="item.type === 'button'"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
:disabled="item.active" :disabled="item.active"
@ -38,7 +36,6 @@
<MkA <MkA
v-else v-else
:to="item.to" :to="item.to"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
> >
@ -99,7 +96,8 @@ export default defineComponent({
font-size: 0.9em; font-size: 0.9em;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
&:hover { &:hover,
&:focus-visible {
text-decoration: none; text-decoration: none;
background: var(--panelHighlight); background: var(--panelHighlight);
} }

View file

@ -46,7 +46,10 @@
/></MkA> /></MkA>
<p class="username"><MkAcct :user="user" /></p> <p class="username"><MkAcct :user="user" /></p>
</div> </div>
<div class="description" :class="{ collapsed: isLong && collapsed }"> <div
class="description"
:class="{ collapsed: isLong && collapsed }"
>
<Mfm <Mfm
v-if="user.description" v-if="user.description"
:text="user.description" :text="user.description"
@ -55,20 +58,10 @@
:custom-emojis="user.emojis" :custom-emojis="user.emojis"
/> />
</div> </div>
<button <XShowMoreButton
v-if="isLong && collapsed" v-if="isLong"
class="fade _button" v-model="collapsed"
@click.stop="collapsed = false" ></XShowMoreButton>
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-if="isLong && !collapsed"
class="showLess _button"
@click.stop="collapsed = true"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
<div v-if="user.fields.length > 0" class="fields"> <div v-if="user.fields.length > 0" class="fields">
<dl <dl
v-for="(field, i) in user.fields" v-for="(field, i) in user.fields"
@ -128,6 +121,7 @@ import * as Acct from "calckey-js/built/acct";
import type * as misskey from "calckey-js"; import type * as misskey from "calckey-js";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import XShowMoreButton from "@/components/MkShowMoreButton.vue";
import * as os from "@/os"; import * as os from "@/os";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -149,14 +143,15 @@ let user = $ref<misskey.entities.UserDetailed | null>(null);
let top = $ref(0); let top = $ref(0);
let left = $ref(0); let left = $ref(0);
let isLong = $ref(false); let isLong = $ref(false);
let collapsed = $ref(!isLong); let collapsed = $ref(!isLong);
onMounted(() => { onMounted(() => {
if (typeof props.q === "object") { if (typeof props.q === "object") {
user = props.q; user = props.q;
isLong = (user.description.split("\n").length > 9 || user.description.length > 400); isLong =
user.description.split("\n").length > 9 ||
user.description.length > 400;
} else { } else {
const query = props.q.startsWith("@") const query = props.q.startsWith("@")
? Acct.parse(props.q.substr(1)) ? Acct.parse(props.q.substr(1))
@ -165,11 +160,12 @@ onMounted(() => {
os.api("users/show", query).then((res) => { os.api("users/show", query).then((res) => {
if (!props.showing) return; if (!props.showing) return;
user = res; user = res;
isLong = (user.description.split("\n").length > 9 || user.description.length > 400); isLong =
user.description.split("\n").length > 9 ||
user.description.length > 400;
}); });
} }
const rect = props.source.getBoundingClientRect(); const rect = props.source.getBoundingClientRect();
const x = const x =
rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.pageXOffset; rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.pageXOffset;
@ -313,7 +309,7 @@ onMounted(() => {
> .fields { > .fields {
padding: 0 16px; padding: 0 16px;
font-size: .8em; font-size: 0.8em;
margin-top: 1em; margin-top: 1em;
> .field { > .field {

View file

@ -46,6 +46,7 @@
:user="user" :user="user"
class="avatar" class="avatar"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<MkUserName :user="user" class="name" /> <MkUserName :user="user" class="name" />
@ -73,6 +74,7 @@
:user="user" :user="user"
class="avatar" class="avatar"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<MkUserName :user="user" class="name" /> <MkUserName :user="user" class="name" />

View file

@ -7,7 +7,7 @@
> >
<div class="beaffaef"> <div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user"> <div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u" /> <MkAvatar class="avatar" :user="u" disableLink />
<MkUserName class="name" :user="u" :nowrap="true" /> <MkUserName class="name" :user="u" :nowrap="true" />
</div> </div>
<div v-if="users.length < count" class="omitted"> <div v-if="users.length < count" class="omitted">

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="vjoppmmu"> <div class="vjoppmmu">
<template v-if="edit"> <template v-if="edit">
<header> <header tabindex="-1" v-focus>
<MkSelect <MkSelect
v-model="widgetAdderSelected" v-model="widgetAdderSelected"
style="margin-bottom: var(--margin)" style="margin-bottom: var(--margin)"

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="dwzlatin" :class="{ opened }"> <div class="dwzlatin" :class="{ opened }">
<div class="header _button" @click="toggle"> <button class="header _button" @click="toggle">
<span class="icon"><slot name="icon"></slot></span> <span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot name="label"></slot></span> <span class="text"><slot name="label"></slot></span>
<span class="right"> <span class="right">
@ -8,7 +8,7 @@
<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i> <i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
<i v-else class="ph-caret-down ph-bold ph-lg icon"></i> <i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
</span> </span>
</div> </button>
<KeepAlive> <KeepAlive>
<div v-if="openedAtLeastOnce" v-show="opened" class="body"> <div v-if="openedAtLeastOnce" v-show="opened" class="body">
<MkSpacer :margin-min="14" :margin-max="22"> <MkSpacer :margin-min="14" :margin-max="22">

View file

@ -66,6 +66,9 @@ function toggle(): void {
&:hover { &:hover {
border-color: var(--inputBorderHover) !important; border-color: var(--inputBorderHover) !important;
} }
&:focus-within {
outline: auto;
}
&.checked { &.checked {
background-color: var(--accentedBg) !important; background-color: var(--accentedBg) !important;

View file

@ -99,6 +99,9 @@ const toggle = () => {
border-color: var(--inputBorderHover) !important; border-color: var(--inputBorderHover) !important;
} }
} }
&:focus-within > .button {
outline: auto;
}
> .label { > .label {
margin-left: 12px; margin-left: 12px;

View file

@ -19,6 +19,7 @@
class="avatar" class="avatar"
:user="$i" :user="$i"
:disable-preview="true" :disable-preview="true"
disableLink
/> />
</div> </div>
<template v-if="metadata"> <template v-if="metadata">
@ -33,6 +34,7 @@
:user="metadata.avatar" :user="metadata.avatar"
:disable-preview="true" :disable-preview="true"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<i <i
v-else-if="metadata.icon && !narrow" v-else-if="metadata.icon && !narrow"

View file

@ -5,6 +5,9 @@
:is="currentPageComponent" :is="currentPageComponent"
:key="key" :key="key"
v-bind="Object.fromEntries(currentPageProps)" v-bind="Object.fromEntries(currentPageProps)"
tabindex="-1"
v-focus
style="outline: none"
/> />
<template #fallback> <template #fallback>

View file

@ -0,0 +1,3 @@
export default {
mounted: (el) => el.focus(),
};

View file

@ -11,6 +11,7 @@ import anim from "./anim";
import clickAnime from "./click-anime"; import clickAnime from "./click-anime";
import panel from "./panel"; import panel from "./panel";
import adaptiveBorder from "./adaptive-border"; import adaptiveBorder from "./adaptive-border";
import focus from "./focus";
export default function (app: App) { export default function (app: App) {
app.directive("userPreview", userPreview); app.directive("userPreview", userPreview);
@ -25,4 +26,5 @@ export default function (app: App) {
app.directive("click-anime", clickAnime); app.directive("click-anime", clickAnime);
app.directive("panel", panel); app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder); app.directive("adaptive-border", adaptiveBorder);
app.directive("focus", focus);
} }

View file

@ -76,25 +76,22 @@ export default {
ev.preventDefault(); ev.preventDefault();
}); });
el.addEventListener( function showTooltip() {
start, window.clearTimeout(self.showTimer);
() => { window.clearTimeout(self.hideTimer);
window.clearTimeout(self.showTimer); self.showTimer = window.setTimeout(self.show, delay);
window.clearTimeout(self.hideTimer); }
self.showTimer = window.setTimeout(self.show, delay); function hideTooltip() {
}, window.clearTimeout(self.showTimer);
{ passive: true }, window.clearTimeout(self.hideTimer);
); self.hideTimer = window.setTimeout(self.close, delay);
}
el.addEventListener( el.addEventListener(start, showTooltip, { passive: true });
end, el.addEventListener("focusin", showTooltip, { passive: true });
() => {
window.clearTimeout(self.showTimer); el.addEventListener(end, hideTooltip, { passive: true });
window.clearTimeout(self.hideTimer); el.addEventListener("focusout", hideTooltip, { passive: true });
self.hideTimer = window.setTimeout(self.close, delay);
},
{ passive: true },
);
el.addEventListener("click", () => { el.addEventListener("click", () => {
window.clearTimeout(self.showTimer); window.clearTimeout(self.showTimer);

View file

@ -18,6 +18,7 @@
<option value="publishing">{{ i18n.ts.publishing }}</option> <option value="publishing">{{ i18n.ts.publishing }}</option>
<option value="suspended">{{ i18n.ts.suspended }}</option> <option value="suspended">{{ i18n.ts.suspended }}</option>
<option value="blocked">{{ i18n.ts.blocked }}</option> <option value="blocked">{{ i18n.ts.blocked }}</option>
<option value="silenced">{{ i18n.ts.silenced }}</option>
<option value="notResponding"> <option value="notResponding">
{{ i18n.ts.notResponding }} {{ i18n.ts.notResponding }}
</option> </option>
@ -105,13 +106,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/form/input.vue"; import MkInput from "@/components/form/input.vue";
import MkSelect from "@/components/form/select.vue"; import MkSelect from "@/components/form/select.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import MkInstanceCardMini from "@/components/MkInstanceCardMini.vue"; import MkInstanceCardMini from "@/components/MkInstanceCardMini.vue";
import FormSplit from "@/components/form/split.vue"; import FormSplit from "@/components/form/split.vue";
import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
let host = $ref(""); let host = $ref("");
@ -134,6 +133,8 @@ const pagination = {
? { suspended: true } ? { suspended: true }
: state === "blocked" : state === "blocked"
? { blocked: true } ? { blocked: true }
: state === "silenced"
? { silenced: true }
: state === "notResponding" : state === "notResponding"
? { notResponding: true } ? { notResponding: true }
: {}), : {}),
@ -143,6 +144,7 @@ const pagination = {
function getStatus(instance) { function getStatus(instance) {
if (instance.isSuspended) return "Suspended"; if (instance.isSuspended) return "Suspended";
if (instance.isBlocked) return "Blocked"; if (instance.isBlocked) return "Blocked";
if (instance.isSilenced) return "Silenced";
if (instance.isNotResponding) return "Error"; if (instance.isNotResponding) return "Error";
return "Alive"; return "Alive";
} }

View file

@ -313,10 +313,8 @@ onUnmounted(() => {
font-weight: normal; font-weight: normal;
opacity: 0.7; opacity: 0.7;
&:hover { &:hover,
opacity: 1; &:focus-visible,
}
&.active { &.active {
opacity: 1; opacity: 1;
} }

View file

@ -3,7 +3,6 @@
<MkStickyContainer> <MkStickyContainer>
<template #header <template #header
><MkPageHeader ><MkPageHeader
v-model:tab="tab"
:actions="headerActions" :actions="headerActions"
:tabs="headerTabs" :tabs="headerTabs"
:display-back-button="true" :display-back-button="true"

View file

@ -7,13 +7,31 @@
:display-back-button="true" :display-back-button="true"
/></template> /></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<MkTab v-model="tab" class="_formBlock">
<option value="block">{{ i18n.ts.blockedInstances }}</option>
<option value="silence">{{ i18n.ts.silencedInstances }}</option>
</MkTab>
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormTextarea v-model="blockedHosts" class="_formBlock"> <FormTextarea
v-if="tab === 'block'"
v-model="blockedHosts"
class="_formBlock"
>
<span>{{ i18n.ts.blockedInstances }}</span> <span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ <template #caption>{{
i18n.ts.blockedInstancesDescription i18n.ts.blockedInstancesDescription
}}</template> }}</template>
</FormTextarea> </FormTextarea>
<FormTextarea
v-else-if="tab === 'silence'"
v-model="silencedHosts"
class="_formBlock"
>
<span>{{ i18n.ts.silencedInstances }}</span>
<template #caption>{{
i18n.ts.silencedInstancesDescription
}}</template>
</FormTextarea>
<FormButton primary class="_formBlock" @click="save" <FormButton primary class="_formBlock" @click="save"
><i class="ph-floppy-disk-back ph-bold ph-lg"></i> ><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
@ -29,21 +47,28 @@ import {} from "vue";
import FormButton from "@/components/MkButton.vue"; import FormButton from "@/components/MkButton.vue";
import FormTextarea from "@/components/form/textarea.vue"; import FormTextarea from "@/components/form/textarea.vue";
import FormSuspense from "@/components/form/suspense.vue"; import FormSuspense from "@/components/form/suspense.vue";
import MkTab from "@/components/MkTab.vue";
import * as os from "@/os"; import * as os from "@/os";
import { fetchInstance } from "@/instance"; import { fetchInstance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
let blockedHosts: string = $ref(""); let blockedHosts: string = $ref("");
let silencedHosts: string = $ref("");
let tab = $ref("block");
async function init() { async function init() {
const meta = await os.api("admin/meta"); const meta = await os.api("admin/meta");
blockedHosts = meta.blockedHosts.join("\n"); if (meta) {
blockedHosts = meta.blockedHosts.join("\n");
silencedHosts = meta.silencedHosts.join("\n");
}
} }
function save() { function save() {
os.apiWithDialog("admin/update-meta", { os.apiWithDialog("admin/update-meta", {
blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [], blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [],
silencedHosts: silencedHosts.split("\n").map((h) => h.trim()) || [],
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();
}); });

View file

@ -12,7 +12,12 @@
class="user" class="user"
:to="`/user-info/${user.id}`" :to="`/user-info/${user.id}`"
> >
<MkAvatar :user="user" class="avatar" indicator /> <MkAvatar
:user="user"
class="avatar"
indicator
disableLink
/>
</MkA> </MkA>
</div> </div>
</Transition> </Transition>

View file

@ -22,10 +22,10 @@
@slide-change="onSlideChange" @slide-change="onSlideChange"
> >
<swiper-slide> <swiper-slide>
<XFeatured /> <XUsers />
</swiper-slide> </swiper-slide>
<swiper-slide> <swiper-slide>
<XUsers /> <XFeatured />
</swiper-slide> </swiper-slide>
</swiper> </swiper>
</MkSpacer> </MkSpacer>
@ -53,16 +53,16 @@ watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
const headerTabs = $computed(() => [ const headerTabs = $computed(() => [
{
key: "featured",
icon: "ph-lightning ph-bold ph-lg",
title: i18n.ts.featured,
},
{ {
key: "users", key: "users",
icon: "ph-users ph-bold ph-lg", icon: "ph-users ph-bold ph-lg",
title: i18n.ts.users, title: i18n.ts.users,
}, },
{
key: "featured",
icon: "ph-lightning ph-bold ph-lg",
title: i18n.ts.featured,
},
]); ]);
definePageMetadata( definePageMetadata(

View file

@ -23,6 +23,7 @@
class="avatar" class="avatar"
:user="req.follower" :user="req.follower"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<div class="name"> <div class="name">

View file

@ -98,6 +98,14 @@
@update:modelValue="toggleBlock" @update:modelValue="toggleBlock"
>{{ i18n.ts.blockThisInstance }}</FormSwitch >{{ i18n.ts.blockThisInstance }}</FormSwitch
> >
<FormSwitch
v-model="isSilenced"
class="_formBlock"
@update:modelValue="toggleSilence"
>{{
i18n.ts.silenceThisInstance
}}</FormSwitch
>
</FormSuspense> </FormSuspense>
<MkButton @click="refreshMetadata" <MkButton @click="refreshMetadata"
><i ><i
@ -329,7 +337,7 @@
import { watch } from "vue"; import { watch } from "vue";
import { Virtual } from "swiper"; import { Virtual } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue"; import { Swiper, SwiperSlide } from "swiper/vue";
import type * as misskey from "calckey-js"; import type * as calckey from "calckey-js";
import MkChart from "@/components/MkChart.vue"; import MkChart from "@/components/MkChart.vue";
import MkObjectView from "@/components/MkObjectView.vue"; import MkObjectView from "@/components/MkObjectView.vue";
import FormLink from "@/components/form/link.vue"; import FormLink from "@/components/form/link.vue";
@ -352,11 +360,13 @@ import "swiper/scss";
import "swiper/scss/virtual"; import "swiper/scss/virtual";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & { type AugmentedInstanceMetadata = calckey.entities.DetailedInstanceMetadata & {
blockedHosts: string[]; blockedHosts: string[];
silencedHosts: string[];
}; };
type AugmentedInstance = misskey.entities.Instance & { type AugmentedInstance = calckey.entities.Instance & {
isBlocked: boolean; isBlocked: boolean;
isSilenced: boolean;
}; };
const props = defineProps<{ const props = defineProps<{
@ -373,6 +383,7 @@ let meta = $ref<AugmentedInstanceMetadata | null>(null);
let instance = $ref<AugmentedInstance | null>(null); let instance = $ref<AugmentedInstance | null>(null);
let suspended = $ref(false); let suspended = $ref(false);
let isBlocked = $ref(false); let isBlocked = $ref(false);
let isSilenced = $ref(false);
let faviconUrl = $ref(null); let faviconUrl = $ref(null);
const usersPagination = { const usersPagination = {
@ -386,16 +397,14 @@ const usersPagination = {
offsetMode: true, offsetMode: true,
}; };
async function init() {
meta = await os.api("admin/meta");
}
async function fetch() { async function fetch() {
meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
instance = (await os.api("federation/show-instance", { instance = (await os.api("federation/show-instance", {
host: props.host, host: props.host,
})) as AugmentedInstance; })) as AugmentedInstance;
suspended = instance.isSuspended; suspended = instance.isSuspended;
isBlocked = instance.isBlocked; isBlocked = instance.isBlocked;
isSilenced = instance.isSilenced;
faviconUrl = faviconUrl =
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ?? getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
getProxiedImageUrlNullable(instance.iconUrl, "preview"); getProxiedImageUrlNullable(instance.iconUrl, "preview");
@ -417,6 +426,22 @@ async function toggleBlock() {
}); });
} }
async function toggleSilence() {
if (meta == null) return;
if (!instance) {
throw new Error(`Instance info not loaded`);
}
let silencedHosts: string[];
if (isSilenced) {
silencedHosts = meta.silencedHosts.concat([instance.host]);
} else {
silencedHosts = meta.silencedHosts.filter((x) => x !== instance!.host);
}
await os.api("admin/update-meta", {
silencedHosts,
});
}
async function toggleSuspend(v) { async function toggleSuspend(v) {
await os.api("admin/federation/update-instance", { await os.api("admin/federation/update-instance", {
host: instance.host, host: instance.host,

View file

@ -2,7 +2,7 @@
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader /></template> <template #header><MkPageHeader /></template>
<MkSpacer :content-max="800"> <MkSpacer :content-max="800">
<div class="mwysmxbg"> <div :class="$style.root">
<div>{{ i18n.ts._mfm.intro }}</div> <div>{{ i18n.ts._mfm.intro }}</div>
<br /> <br />
<div class="section _block"> <div class="section _block">
@ -137,6 +137,18 @@
</div> </div>
</div> </div>
</div> </div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.blockMath }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.blockMathDescription }}</p>
<div class="preview">
<Mfm :text="preview_blockMath" />
<MkTextarea v-model="preview_blockMath"
><template #label>MFM</template></MkTextarea
>
</div>
</div>
</div>
<!-- deprecated <!-- deprecated
<div class="section _block"> <div class="section _block">
<div class="title">{{ i18n.ts._mfm.search }}</div> <div class="title">{{ i18n.ts._mfm.search }}</div>
@ -427,8 +439,11 @@ let preview_blockCode = $ref(
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```' '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```'
); );
let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)"); let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`); let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`); let preview_search = $ref(
`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`
);
let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]"); let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");
let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]"); let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]"); let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");
@ -450,9 +465,15 @@ let preview_x4 = $ref("$[x4 🍮]");
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`); let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]"); let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
let preview_sparkle = $ref("$[sparkle 🍮]"); let preview_sparkle = $ref("$[sparkle 🍮]");
let preview_rotate = $ref("$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]"); let preview_rotate = $ref(
let preview_position = $ref("$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]"); "$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]"
let preview_scale = $ref("$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]"); );
let preview_position = $ref(
"$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]"
);
let preview_scale = $ref(
"$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]"
);
let preview_fg = $ref("$[fg.color=ff0000 Text color]"); let preview_fg = $ref("$[fg.color=ff0000 Text color]");
let preview_bg = $ref("$[bg.color=ff0000 Background color]"); let preview_bg = $ref("$[bg.color=ff0000 Background color]");
let preview_plain = $ref( let preview_plain = $ref(
@ -465,8 +486,8 @@ definePageMetadata({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.mwysmxbg { .root {
background: var(--bg); background: var(--bg);
> .section { > .section {

View file

@ -6,14 +6,14 @@
{{ i18n.ts.addAccount }}</FormButton {{ i18n.ts.addAccount }}</FormButton
> >
<div <button
v-for="account in accounts" v-for="account in accounts"
:key="account.id" :key="account.id"
class="_panel _button lcjjdxlm" class="_panel _button lcjjdxlm"
@click="menu(account, $event)" @click="menu(account, $event)"
> >
<div class="avatar"> <div class="avatar">
<MkAvatar :user="account" class="avatar" /> <MkAvatar :user="account" class="avatar" disableLink />
</div> </div>
<div class="body"> <div class="body">
<div class="name"> <div class="name">
@ -23,7 +23,7 @@
<MkAcct :user="account" /> <MkAcct :user="account" />
</div> </div>
</div> </div>
</div> </button>
</FormSuspense> </FormSuspense>
</div> </div>
</template> </template>
@ -158,6 +158,8 @@ definePageMetadata({
.lcjjdxlm { .lcjjdxlm {
display: flex; display: flex;
padding: 16px; padding: 16px;
width: 100%;
text-align: unset;
> .avatar { > .avatar {
display: block; display: block;

View file

@ -16,23 +16,20 @@
{{ i18n.ts.export }}</MkButton {{ i18n.ts.export }}</MkButton
> >
</FormFolder> </FormFolder>
<FormFolder class="_formBlock"> <!-- <FormFolder class="_formBlock">
<template #label>{{ i18n.ts.import }}</template> <template #label>{{ i18n.ts.import }}</template>
<template #icon <template #icon
><i class="ph-upload-simple ph-bold ph-lg"></i ><i class="ph-upload-simple ph-bold ph-lg"></i
></template> ></template>
<!-- <FormSwitch v-model="signatureCheck" class="_formBlock">
Mastodon import? (not Akkoma!)
</FormSwitch> -->
<FormRadios v-model="importType" class="_formBlock"> <FormRadios v-model="importType" class="_formBlock">
<option value="calckey">Calckey/Misskey</option> <option value="calckey">Calckey/Misskey</option>
<option value="mastodon">Mastodon</option> <option value="mastodon">Mastodon</option>
<!-- <option :disabled="true" value="akkoma"> <option :disabled="true" value="akkoma">
Pleroma/Akkoma (soon) Pleroma/Akkoma (soon)
</option> </option>
<option :disabled="true" value="twitter"> <option :disabled="true" value="twitter">
Twitter (soon) Twitter (soon)
</option> --> </option>
</FormRadios> </FormRadios>
<MkButton <MkButton
primary primary
@ -42,7 +39,7 @@
><i class="ph-upload-simple ph-bold ph-lg"></i> ><i class="ph-upload-simple ph-bold ph-lg"></i>
{{ i18n.ts.import }}</MkButton {{ i18n.ts.import }}</MkButton
> >
</FormFolder> </FormFolder> -->
</FormSection> </FormSection>
<FormSection> <FormSection>
<template #label>{{ <template #label>{{

View file

@ -204,10 +204,6 @@ hr {
pointer-events: none; pointer-events: none;
} }
&:focus-visible {
outline: none;
}
&:disabled { &:disabled {
opacity: 0.5; opacity: 0.5;
cursor: default; cursor: default;

View file

@ -1,7 +1,7 @@
{ {
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea', id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
base: 'dark', base: 'dark',
name: 'Mi Astro Dark', name: 'Astro Dark',
author: 'syuilo', author: 'syuilo',
props: { props: {
bg: '#232125', bg: '#232125',

View file

@ -1,7 +1,7 @@
{ {
id: '504debaf-4912-6a4c-5059-1db08a76b737', id: '504debaf-4912-6a4c-5059-1db08a76b737',
name: 'Mi Botanical Dark', name: 'Botanical Dark',
author: 'syuilo', author: 'syuilo',
base: 'dark', base: 'dark',

View file

@ -1,7 +1,7 @@
{ {
id: 'ffcd3328-5c57-4ca3-9dac-4580cbf7742f', id: 'ffcd3328-5c57-4ca3-9dac-4580cbf7742f',
base: 'dark', base: 'dark',
name: 'Catppuccin frappe', name: 'Catppuccin Frappe',
props: { props: {
X2: ':darken<2<@panel', X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)', X3: 'rgba(255, 255, 255, 0.05)',

View file

@ -1,7 +1,7 @@
{ {
id: 'd413f41f-a489-48be-9e20-3532ffbb4363', id: 'd413f41f-a489-48be-9e20-3532ffbb4363',
base: 'dark', base: 'dark',
name: 'Catppuccin mocha', name: 'Catppuccin Mocha',
props: { props: {
X2: ':darken<2<@panel', X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)', X3: 'rgba(255, 255, 255, 0.05)',

View file

@ -1,7 +1,7 @@
{ {
id: '679b3b87-a4e9-4789-8696-b56c15cc33b0', id: '679b3b87-a4e9-4789-8696-b56c15cc33b0',
name: 'Mi Cherry Dark', name: 'Cherry Dark',
author: 'syuilo', author: 'syuilo',
base: 'dark', base: 'dark',

View file

@ -1,7 +1,7 @@
{ {
id: '32a637ef-b47a-4775-bb7b-bacbb823f865', id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
name: 'Mi Future Dark', name: 'Future Dark',
author: 'syuilo', author: 'syuilo',
base: 'dark', base: 'dark',

View file

@ -1,7 +1,7 @@
{ {
id: '02816013-8107-440f-877e-865083ffe194', id: '02816013-8107-440f-877e-865083ffe194',
name: 'Mi Green+Lime Dark', name: 'Mi Dark',
author: 'syuilo', author: 'syuilo',
base: 'dark', base: 'dark',

View file

@ -1,24 +0,0 @@
{
id: 'dc489603-27b5-424a-9b25-1ff6aec9824a',
name: 'Mi Green+Orange Dark',
author: 'syuilo',
base: 'dark',
props: {
accent: '#e97f00',
bg: '#0C1210',
fg: '#dee7e4',
fgHighlighted: '#fff',
fgOnAccent: '#192320',
divider: '#e7fffb24',
panel: '#192320',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
popup: '#293330',
renote: '@accent',
mentionMe: '#b4e900',
link: '#24d7ce',
},
}

View file

@ -1,7 +1,7 @@
{ {
id: '66e7e5a9-cd43-42cd-837d-12f47841fa34', id: '66e7e5a9-cd43-42cd-837d-12f47841fa34',
name: 'Mi Ice Dark', name: 'Ice Dark',
author: 'syuilo', author: 'syuilo',
base: 'dark', base: 'dark',

View file

@ -1,7 +1,7 @@
{ {
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d', id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
name: 'Mi Persimmon Dark', name: 'Persimmon Dark',
author: 'syuilo', author: 'syuilo',
base: 'dark', base: 'dark',

View file

@ -1,7 +1,7 @@
{ {
id: '7a5bc13b-df8f-4d44-8e94-4452f0c634bb', id: '7a5bc13b-df8f-4d44-8e94-4452f0c634bb',
base: 'dark', base: 'dark',
name: 'Mi U0 Dark', name: 'U0 Dark',
props: { props: {
X2: ':darken<2<@panel', X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)', X3: 'rgba(255, 255, 255, 0.05)',

View file

@ -1,7 +1,7 @@
{ {
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
name: 'Mi Apricot Light', name: 'Apricot Light',
author: 'syuilo', author: 'syuilo',
base: 'light', base: 'light',

View file

@ -0,0 +1,94 @@
{
id: "169661d2-5a17-4dfc-b71b-9938cbbbed3e",
base: "light",
name: "Catppuccin Latte",
props: {
X2: ":darken<2<@panel",
X3: "rgba(255, 255, 255, 0.05)",
X4: "rgba(255, 255, 255, 0.1)",
X5: "rgba(255, 255, 255, 0.05)",
X6: "rgba(255, 255, 255, 0.15)",
X7: "rgba(255, 255, 255, 0.05)",
X8: ":lighten<5<@accent",
X9: ":darken<5<@accent",
bg: "#dce0e8",
fg: "#4c4f69",
X10: ":alpha<0.4<@accent",
X11: "rgba(0, 0, 0, 0.3)",
X12: "rgba(255, 255, 255, 0.1)",
X13: "rgba(255, 255, 255, 0.15)",
X14: ":alpha<0.5<@navBg",
X15: ":alpha<0<@panel",
X16: ":alpha<0.7<@panel",
X17: ":alpha<0.8<@bg",
cwBg: "#bcc0cc",
cwFg: "#5c5f77",
link: "#1e66f5",
warn: "#fe640b",
badge: "#1e66f5",
error: "#d20f39",
focus: ":alpha<0.3<@accent",
navBg: "@panel",
navFg: "@fg",
panel: ":lighten<3<@bg",
popup: ":lighten<3<@panel",
accent: "#8839ef",
header: ":alpha<0.7<@panel",
infoBg: "#ccd0da",
infoFg: "#6c6f85",
renote: "#1e66f5",
shadow: "rgba(0, 0, 0, 0.3)",
divider: "rgba(255, 255, 255, 0.1)",
hashtag: "#209fb5",
mention: "@accent",
modalBg: "rgba(0, 0, 0, 0.5)",
success: "#40a02b",
buttonBg: "rgba(255, 255, 255, 0.05)",
switchBg: "rgba(255, 255, 255, 0.15)",
acrylicBg: ":alpha<0.5<@bg",
cwHoverBg: "#acb0be",
indicator: "@accent",
mentionMe: "@mention",
messageBg: "@bg",
navActive: "@accent",
accentedBg: ":alpha<0.15<@accent",
codeNumber: "#40a02b",
codeString: "#fe640b",
fgOnAccent: "#eff1f5",
infoWarnBg: "#ccd0da",
infoWarnFg: "#5c5f77",
navHoverFg: ":lighten<17<@fg",
swutchOnBg: "@accentedBg",
swutchOnFg: "@accent",
codeBoolean: "@accent",
dateLabelFg: "@fg",
deckDivider: "#9ca0b0",
inputBorder: "rgba(255, 255, 255, 0.1)",
panelBorder: "solid 1px var(--divider)",
swutchOffBg: "rgba(255, 255, 255, 0.1)",
swutchOffFg: "@fg",
accentDarken: ":darken<10<@accent",
acrylicPanel: ":alpha<0.5<@panel",
navIndicator: "@indicator",
windowHeader: ":alpha<0.85<@panel",
accentLighten: ":lighten<10<@accent",
buttonHoverBg: "rgba(255, 255, 255, 0.1)",
driveFolderBg: ":alpha<0.3<@accent",
fgHighlighted: ":lighten<3<@fg",
fgTransparent: ":alpha<0.5<@fg",
panelHeaderBg: ":lighten<3<@panel",
panelHeaderFg: "@fg",
buttonGradateA: "@accent",
buttonGradateB: ":hue<20<@accent",
htmlThemeColor: "@bg",
panelHighlight: ":lighten<3<@panel",
listItemHoverBg: "rgba(255, 255, 255, 0.03)",
scrollbarHandle: "rgba(255, 255, 255, 0.2)",
inputBorderHover: "rgba(255, 255, 255, 0.2)",
wallpaperOverlay: "rgba(0, 0, 0, 0.5)",
fgTransparentWeak: ":alpha<0.75<@fg",
panelHeaderDivider: "rgba(0, 0, 0, 0)",
scrollbarHandleHover: "rgba(255, 255, 255, 0.4)",
},
author: "somebody ¯_(ツ)_/¯",
}

View file

@ -1,7 +1,7 @@
{ {
id: 'ac168876-f737-4074-a3fc-a370c732ef48', id: 'ac168876-f737-4074-a3fc-a370c732ef48',
name: 'Mi Cherry Light', name: 'Cherry Light',
author: 'syuilo', author: 'syuilo',
base: 'light', base: 'light',

View file

@ -1,7 +1,7 @@
{ {
id: '6ed80faa-74f0-42c2-98e4-a64d9e138eab', id: '6ed80faa-74f0-42c2-98e4-a64d9e138eab',
name: 'Mi Coffee Light', name: 'Coffee Light',
author: 'syuilo', author: 'syuilo',
base: 'light', base: 'light',

View file

@ -1,7 +1,7 @@
{ {
id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96', id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
name: 'Mi Rainy Light', name: 'Rainy Light',
author: 'syuilo', author: 'syuilo',
base: 'light', base: 'light',

View file

@ -1,7 +1,7 @@
{ {
id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c', id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c',
name: 'Mi Sushi Light', name: 'Sushi Light',
author: 'syuilo', author: 'syuilo',
base: 'light', base: 'light',

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