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 5bfe6c0730
109 changed files with 3014 additions and 918 deletions

View File

@ -1,7 +1,8 @@
pipeline:
testCommit:
image: node:latest
image: node:alpine
commands:
- apk add --no-cache cargo python3 make g++
- cp .config/ci.yml .config/default.yml
- corepack enable
- 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"
files: "Files"
download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Posts\
\ with this file attached will also be deleted."
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? It\
\ will be removed from all posts that contain it as an attachment."
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\
\ to your Drive once completed."
@ -200,6 +200,7 @@ perHour: "Per Hour"
perDay: "Per Day"
stopActivityDelivery: "Stop sending activities"
blockThisInstance: "Block this instance"
silenceThisInstance: "Silence this instance"
operations: "Operations"
software: "Software"
version: "Version"
@ -221,10 +222,13 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances that you want to\
\ 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"
hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\
\ 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"
mutedUsers: "Muted users"
blockedUsers: "Blocked users"
@ -243,6 +247,7 @@ noCustomEmojis: "There are no emoji"
noJobs: "There are no jobs"
federating: "Federating"
blocked: "Blocked"
silenced: "Silenced"
suspended: "Suspended"
all: "All"
subscribing: "Subscribing"
@ -832,7 +837,7 @@ active: "Active"
offline: "Offline"
notRecommended: "Not recommended"
botProtection: "Bot Protection"
instanceBlocking: "Blocked Instances"
instanceBlocking: "Federation Block/Silence"
selectAccount: "Select account"
switchAccount: "Switch account"
enabled: "Enabled"
@ -1200,7 +1205,7 @@ _mfm:
inlineMath: "Math (Inline)"
inlineMathDescription: "Display math formulas (KaTeX) in-line"
blockMath: "Math (Block)"
blockMathDescription: "Display multi-line math formulas (KaTeX) in a block"
blockMathDescription: "Display math formulas (KaTeX) in a block"
quote: "Quote"
quoteDescription: "Displays content as a quote."
emoji: "Custom Emoji"

View File

@ -1,3 +1,4 @@
_lang_: "Suomi"
username: Käyttäjänimi
fetchingAsApObject: Hae Fedeversestä
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.
blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.
_lang_: Suomi

View File

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

View File

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

View File

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

View File

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

BIN
packages/backend/assets/favicon.ico (Stored with Git LFS)

Binary file not shown.

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}`),
);
}
/**
* 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[];
@Column('varchar', {
length: 256, array: true, default: '{}',
})
public silencedHosts: string[];
@Column('boolean', {
default: false,
})

View File

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

View File

@ -68,6 +68,11 @@ export const packedFederationInstanceSchema = {
optional: false,
nullable: false,
},
isSilenced: {
type: "boolean",
optional: false,
nullable: false,
},
softwareName: {
type: "string",
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 { inbox as processInbox } from "@/queue/index.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 { renderLike } from "@/remote/activitypub/renderer/like.js";
import { getUserKeypair } from "@/misc/keypair-store.js";
@ -330,22 +336,68 @@ router.get("/likes/:like", async (ctx) => {
});
// 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);
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 followRequest = await FollowRequests.findOneBy({
id: ctx.params.followRequestId,
});
if (followRequest == null) {
ctx.status = 404;
return;
}
const [follower, followee] = await Promise.all([
Users.findOneBy({
id: ctx.params.follower,
id: followRequest.followerId,
host: IsNull(),
}),
Users.findOneBy({
id: ctx.params.followee,
id: followRequest.followeeId,
host: Not(IsNull()),
}),
]);
@ -355,13 +407,13 @@ router.get("/follows/:follower/:followee", async (ctx) => {
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");
}
ctx.body = renderActivity(renderFollow(follower, followee));
setResponseType(ctx);
});

View File

@ -533,7 +533,7 @@ const eps = [
["i/export-following", ep___i_exportFollowing],
["i/export-mute", ep___i_exportMute],
["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/favorites", ep___i_favorites],
["i/gallery/likes", ep___i_gallery_likes],

View File

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

View File

@ -61,6 +61,13 @@ export const paramDef = {
type: "string",
},
},
silencedHosts: {
type: "array",
nullable: true,
items: {
type: "string",
},
},
allowedHosts: {
type: "array",
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) {
set.themeColor = ps.themeColor;
}

View File

@ -34,6 +34,7 @@ export const paramDef = {
notResponding: { type: "boolean", nullable: true },
suspended: { type: "boolean", nullable: true },
federating: { type: "boolean", nullable: true },
silenced: { type: "boolean", nullable: true },
subscribing: { type: "boolean", nullable: true },
publishing: { type: "boolean", nullable: true },
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 (ps.notResponding) {
query.andWhere("instance.isNotResponding = TRUE");

View File

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

View File

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

View File

@ -8,6 +8,8 @@ import { apiTimelineMastodon } from "./endpoints/timeline.js";
import { apiNotificationsMastodon } from "./endpoints/notifications.js";
import { apiSearchMastodon } from "./endpoints/search.js";
import { getInstance } from "./endpoints/meta.js";
import { convertAnnouncement, convertFilter } from "./converters.js";
import { convertId, IdType } from "../index.js";
export function getClient(
BASE_URL: string,
@ -68,7 +70,9 @@ export function apiMastodonCompatible(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getInstanceAnnouncements();
ctx.body = data.data;
ctx.body = data.data.map((announcement) =>
convertAnnouncement(announcement),
);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -83,7 +87,9 @@ export function apiMastodonCompatible(router: Router): void {
const accessTokens = ctx.request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.dismissInstanceAnnouncement(ctx.params.id);
const data = await client.dismissInstanceAnnouncement(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
@ -100,7 +106,7 @@ export function apiMastodonCompatible(router: Router): void {
// displayed without being logged in
try {
const data = await client.getFilters();
ctx.body = data.data;
ctx.body = data.data.map((filter) => convertFilter(filter));
} catch (e: any) {
console.error(e);
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 { FindOptionsWhere, IsNull } from "typeorm";
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 {
convertAccount,
convertList,
convertRelationship,
convertStatus,
} from "../converters.js";
const relationshipModel = {
id: "",
@ -62,9 +68,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.updateCredentials(
(ctx.request as any).body as any,
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertAccount(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -81,9 +85,7 @@ export function apiAccountMastodon(router: Router): void {
(ctx.request.query as any).acct,
"accounts",
);
let resp = data.data.accounts[0];
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertAccount(data.data.accounts[0]);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -115,11 +117,9 @@ export function apiAccountMastodon(router: Router): void {
}
const data = await client.getRelationships(reqIds);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
ctx.body = data.data.map((relationship) =>
convertRelationship(relationship),
);
} catch (e: any) {
console.error(e);
let data = e.response.data;
@ -136,9 +136,7 @@ export function apiAccountMastodon(router: Router): void {
try {
const calcId = convertId(ctx.params.id, IdType.CalckeyId);
const data = await client.getAccount(calcId);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertAccount(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -155,27 +153,9 @@ export function apiAccountMastodon(router: Router): void {
try {
const data = await client.getAccountStatuses(
convertId(ctx.params.id, IdType.CalckeyId),
argsToBools(limitToInt(ctx.query as any)),
convertTimelinesArgsId(argsToBools(limitToInt(ctx.query as any))),
);
let resp = data.data;
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;
ctx.body = data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -193,13 +173,9 @@ export function apiAccountMastodon(router: Router): void {
try {
const data = await client.getAccountFollowers(
convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any),
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -217,13 +193,9 @@ export function apiAccountMastodon(router: Router): void {
try {
const data = await client.getAccountFollowing(
convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any),
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -239,8 +211,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountLists(ctx.params.id);
ctx.body = data.data;
const data = await client.getAccountLists(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data.map((list) => convertList(list));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -259,9 +233,8 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.followAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = data.data;
let acct = convertRelationship(data.data);
acct.following = true;
acct.id = convertId(acct.id, IdType.MastodonId);
ctx.body = acct;
} catch (e: any) {
console.error(e);
@ -281,8 +254,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.unfollowAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = data.data;
acct.id = convertId(acct.id, IdType.MastodonId);
let acct = convertRelationship(data.data);
acct.following = false;
ctx.body = acct;
} catch (e: any) {
@ -303,9 +275,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.blockAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -324,9 +294,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.unblockAccount(
convertId(ctx.params.id, IdType.MastodonId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -346,9 +314,7 @@ export function apiAccountMastodon(router: Router): void {
convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request as any).body as any,
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -367,9 +333,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.unmuteAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -383,28 +347,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = (await client.getBookmarks(
limitToInt(ctx.query as any),
)) as any;
let resp = data.data;
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;
const data = await client.getBookmarks(
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -417,26 +363,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getFavourites(limitToInt(ctx.query as any));
let resp = data.data;
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;
const data = await client.getFavourites(
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -449,12 +379,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getMutes(limitToInt(ctx.query as any));
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
const data = await client.getMutes(
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -467,12 +395,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getBlocks(limitToInt(ctx.query as any));
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
const data = await client.getBlocks(
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -488,11 +414,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.getFollowRequests(
((ctx.query as any) || { limit: 20 }).limit,
);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -510,9 +432,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.acceptFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -531,9 +451,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.rejectFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -399,28 +399,33 @@ router.get("/notes/:note", async (ctx, next) => {
visibility: In(["public", "home"]),
});
if (note) {
const _note = await Notes.pack(note);
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,
});
try {
if (note) {
const _note = await Notes.pack(note);
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();
});

View File

@ -6,11 +6,13 @@ import {
NoteThreadMutings,
UserProfiles,
Users,
Followings,
} from "@/models/index.js";
import { genId } from "@/misc/gen-id.js";
import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js";
import { sendEmailNotification } from "./send-email-notification.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
export async function createNotification(
notifieeId: User["id"],
@ -21,6 +23,26 @@ export async function createNotification(
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 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 { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { webhookDeliver } from "@/queue/index.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
const logger = new Logger("following/create");
@ -226,13 +227,19 @@ export default async function (
});
// フォロー対象が鍵アカウントである or
// The follower is silenced, or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
// The follower is remote, the followee is local, and the follower is in a silenced instance.
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if (
followee.isLocked ||
follower.isSilenced ||
(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;

View File

@ -6,6 +6,7 @@ import type { User } from "@/models/entities/user.js";
import { Blockings, FollowRequests, Users } from "@/models/index.js";
import { genId } from "@/misc/gen-id.js";
import { createNotification } from "../../create-notification.js";
import config from "@/config/index.js";
export default async function (
follower: {
@ -79,7 +80,13 @@ export default async function (
}
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);
}
}

View File

@ -39,7 +39,7 @@ import {
} from "@/models/index.js";
import type { DriveFile } from "@/models/entities/drive-file.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 { genId } from "@/misc/gen-id.js";
import {
@ -66,6 +66,7 @@ import { Cache } from "@/misc/cache.js";
import type { UserProfile } from "@/models/entities/user-profile.js";
import { db } from "@/db/postgre.js";
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@ -166,6 +167,7 @@ export default async (
data: Option,
silent = false,
) =>
// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
new Promise<Note>(async (res, rej) => {
// 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.)
@ -203,6 +205,15 @@ export default async (
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".
if (
data.renote &&

View File

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

View File

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

View File

@ -32,7 +32,7 @@
"autosize": "5.0.2",
"blurhash": "1.1.5",
"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:*",
"chart.js": "4.1.1",
"chartjs-adapter-date-fns": "2.0.1",

View File

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

View File

@ -1,5 +1,6 @@
<template>
<button
ref="el"
class="_button"
:class="{ showLess: modelValue, fade: !modelValue }"
@click.stop="toggle"
@ -12,7 +13,7 @@
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { computed, ref } from "vue";
import { length } from "stringz";
import * as misskey from "calckey-js";
import { concat } from "@/scripts/array";
@ -27,6 +28,8 @@ const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void;
}>();
const el = ref<HTMLElement>();
const label = computed(() => {
return concat([
props.note.text
@ -43,6 +46,14 @@ const label = computed(() => {
const toggle = () => {
emit("update:modelValue", !props.modelValue);
};
function focus() {
el.value.focus();
}
defineExpose({
focus,
});
</script>
<style lang="scss" scoped>
@ -62,9 +73,46 @@ const toggle = () => {
}
}
}
&:hover > span {
&:hover > span,
&:focus > span {
background: var(--cwFg) !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>

View File

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

View File

@ -1,157 +1,160 @@
<template>
<div
class="omfetrab"
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
>
<input
ref="search"
v-model.trim="q"
class="search"
data-prevent-emoji-insert
:class="{ filled: q != null && q != '' }"
:placeholder="i18n.ts.search"
type="search"
@paste.stop="paste"
@keyup.enter="done()"
/>
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
v-for="emoji in searchResultCustom"
: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">
<FocusTrap v-bind:active="isActive">
<div
class="omfetrab"
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
tabindex="-1"
>
<input
ref="search"
v-model.trim="q"
class="search"
data-prevent-emoji-insert
:class="{ filled: q != null && q != '' }"
:placeholder="i18n.ts.search"
type="search"
@paste.stop="paste"
@keyup.enter="done()"
/>
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
v-for="emoji in pinned"
:key="emoji"
v-for="emoji in searchResultCustom"
:key="emoji.id"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img
class="emoji"
:emoji="emoji"
:normal="true"
: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>
<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 v-if="tab === 'index'" class="group index">
<section v-if="showPinned">
<div class="body">
<button
v-for="emoji in pinned"
:key="emoji"
class="_button item"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji
class="emoji"
:emoji="emoji"
:normal="true"
/>
</button>
</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 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 class="tabs">
<button
class="_button tab"
:class="{ active: tab === 'index' }"
@click="tab = 'index'"
>
</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
<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>
<div class="tabs">
<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>
</FocusTrap>
</template>
<script lang="ts" setup>
@ -171,6 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import { FocusTrap } from "focus-trap-vue";
const props = withDefaults(
defineProps<{

View File

@ -5,6 +5,7 @@
{
yellow: instance.isNotResponding,
red: instance.isBlocked,
purple: instance.isSilenced,
gray: instance.isSuspended,
},
]"
@ -23,13 +24,13 @@
</template>
<script lang="ts" setup>
import * as misskey from "calckey-js";
import * as calckey from "calckey-js";
import MkMiniChart from "@/components/MkMiniChart.vue";
import * as os from "@/os";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
const props = defineProps<{
instance: misskey.entities.Instance;
instance: calckey.entities.Instance;
}>();
let chartValues = $ref<number[] | null>(null);
@ -135,6 +136,21 @@ function getInstanceIcon(instance): string {
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) {
--c: var(--bg);
background-image: linear-gradient(

View File

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

View File

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

View File

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

View File

@ -1,191 +1,202 @@
<template>
<div>
<div
ref="itemsEl"
v-hotkey="keymap"
class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }"
:style="{
width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '',
}"
@contextmenu.self="(e) => e.preventDefault()"
>
<template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item">
<span :style="item.textStyle || ''">{{ item.text }}</span>
</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>
<FocusTrap :active="false" ref="focusTrap">
<div tabindex="-1">
<div
ref="itemsEl"
class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }"
:style="{
width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '',
}"
@contextmenu.self="(e) => e.preventDefault()"
>
<template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item">
<span :style="item.textStyle || ''">{{
item.text
}}</span>
</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>
</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
v-else-if="item.type === 'pending'"
class="pending item"
>
<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>
<button
v-else-if="item.type === 'parent'"
:tabindex="i"
class="_button item parent"
:class="{ childShowing: childShowingItem === item }"
@mouseenter="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"
: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 v-if="childMenu" class="child">
<XChild
ref="child"
:items="childMenu"
:target-element="childTarget"
:root-element="itemsEl"
showing
@actioned="childActioned"
/>
</div>
</div>
<div v-if="childMenu" class="child">
<XChild
ref="child"
:items="childMenu"
:target-element="childTarget"
:root-element="itemsEl"
showing
@actioned="childActioned"
/>
</div>
</div>
</FocusTrap>
</template>
<script lang="ts" setup>
@ -206,8 +217,10 @@ import FormSwitch from "@/components/form/switch.vue";
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { FocusTrap } from "focus-trap-vue";
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
const focusTrap = ref();
const props = defineProps<{
items: MenuItem[];
@ -228,12 +241,6 @@ let items2: InnerMenuItem[] = $ref([]);
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>();
watch(
@ -324,6 +331,8 @@ function focusDown() {
}
onMounted(() => {
focusTrap.value.activate();
if (props.viaKeyboard) {
nextTick(() => {
focusNext(itemsEl.children[0], true, false);
@ -364,8 +373,7 @@ onBeforeUnmount(() => {
font-size: 0.9em;
line-height: 20px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
outline: none;
&:before {
content: "";
@ -389,7 +397,8 @@ onBeforeUnmount(() => {
transform: translateY(0em);
}
&:not(:disabled):hover {
&:not(:disabled):hover,
&:focus-visible {
color: var(--accent);
text-decoration: none;
@ -397,6 +406,9 @@ onBeforeUnmount(() => {
background: var(--accentedBg);
}
}
&:focus-visible:before {
outline: auto;
}
&.danger {
color: #eb6f92;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -462,15 +462,26 @@ if (
props.reply &&
["home", "followers", "specified"].includes(props.reply.visibility)
) {
visibility = props.reply.visibility;
if (props.reply.visibility === "specified") {
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.visibility === "home" && visibility === "followers") {
visibility = "followers";
} else if (
["home", "followers"].includes(props.reply.visibility) &&
visibility === "specified"
) {
visibility = "specified";
} 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) {
os.api("users/show", { userId: props.reply.userId }).then(

View File

@ -198,7 +198,6 @@ export default defineComponent({
height: 64px;
margin-right: 4px;
border-radius: 4px;
overflow: hidden;
cursor: move;
&: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="{ 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"
>({{ i18n.ts.deleted }})</span
>
@ -96,34 +109,30 @@
<XNoteSimple :note="note.renote" />
</div>
</template>
<div
v-if="note.cw && !showContent"
tabindex="0"
v-on:focus="cwButton?.focus()"
></div>
</div>
<button
v-if="isLong && collapsed"
class="fade _button"
@click.stop="collapsed = false"
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-if="isLong && !collapsed"
class="showLess _button"
@click.stop="collapsed = true"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
<XShowMoreButton
v-if="isLong"
v-model="collapsed"
></XShowMoreButton>
<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
</div>
</div>
</template>
<script lang="ts" setup>
import {} from "vue";
import { ref } from "vue";
import * as misskey from "calckey-js";
import * as mfm from "mfm-js";
import XNoteSimple from "@/components/MkNoteSimple.vue";
import XMediaList from "@/components/MkMediaList.vue";
import XPoll from "@/components/MkPoll.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue";
import XShowMoreButton from "@/components/MkShowMoreButton.vue";
import XCwButton from "@/components/MkCwButton.vue";
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
import { i18n } from "@/i18n";
@ -138,19 +147,28 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: "push", v): void;
(ev: "focusfooter"): void;
}>();
const cwButton = ref<HTMLElement>();
const isLong =
!props.detailedView &&
props.note.cw == null &&
props.note.text != null &&
(props.note.text.split("\n").length > 9 || props.note.text.length > 500);
const collapsed = $ref(props.note.cw == null && isLong);
const urls = props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
: null;
let showContent = $ref(false);
function focusFooter(ev) {
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
emit("focusfooter");
}
}
</script>
<style lang="scss" scoped>
@ -242,6 +260,9 @@ let showContent = $ref(false);
margin-top: -50px;
padding-top: 50px;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
&.collapsed > .body {
box-sizing: border-box;
@ -264,43 +285,6 @@ let showContent = $ref(false);
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'"
:href="item.href"
:target="item.target"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
>
@ -22,7 +21,6 @@
</a>
<button
v-else-if="item.type === 'button'"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
:disabled="item.active"
@ -38,7 +36,6 @@
<MkA
v-else
:to="item.to"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
>
@ -99,7 +96,8 @@ export default defineComponent({
font-size: 0.9em;
margin-bottom: 0.3rem;
&:hover {
&:hover,
&:focus-visible {
text-decoration: none;
background: var(--panelHighlight);
}

View File

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

View File

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

View File

@ -7,7 +7,7 @@
>
<div class="beaffaef">
<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" />
</div>
<div v-if="users.length < count" class="omitted">

View File

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

View File

@ -1,6 +1,6 @@
<template>
<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="text"><slot name="label"></slot></span>
<span class="right">
@ -8,7 +8,7 @@
<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>
</span>
</div>
</button>
<KeepAlive>
<div v-if="openedAtLeastOnce" v-show="opened" class="body">
<MkSpacer :margin-min="14" :margin-max="22">

View File

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

View File

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

View File

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

View File

@ -5,6 +5,9 @@
:is="currentPageComponent"
:key="key"
v-bind="Object.fromEntries(currentPageProps)"
tabindex="-1"
v-focus
style="outline: none"
/>
<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 panel from "./panel";
import adaptiveBorder from "./adaptive-border";
import focus from "./focus";
export default function (app: App) {
app.directive("userPreview", userPreview);
@ -25,4 +26,5 @@ export default function (app: App) {
app.directive("click-anime", clickAnime);
app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder);
app.directive("focus", focus);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -7,13 +7,31 @@
:display-back-button="true"
/></template>
<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">
<FormTextarea v-model="blockedHosts" class="_formBlock">
<FormTextarea
v-if="tab === 'block'"
v-model="blockedHosts"
class="_formBlock"
>
<span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{
i18n.ts.blockedInstancesDescription
}}</template>
</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"
><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 FormTextarea from "@/components/form/textarea.vue";
import FormSuspense from "@/components/form/suspense.vue";
import MkTab from "@/components/MkTab.vue";
import * as os from "@/os";
import { fetchInstance } from "@/instance";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
let blockedHosts: string = $ref("");
let silencedHosts: string = $ref("");
let tab = $ref("block");
async function init() {
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() {
os.apiWithDialog("admin/update-meta", {
blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [],
silencedHosts: silencedHosts.split("\n").map((h) => h.trim()) || [],
}).then(() => {
fetchInstance();
});

View File

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

View File

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

View File

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

View File

@ -98,6 +98,14 @@
@update:modelValue="toggleBlock"
>{{ i18n.ts.blockThisInstance }}</FormSwitch
>
<FormSwitch
v-model="isSilenced"
class="_formBlock"
@update:modelValue="toggleSilence"
>{{
i18n.ts.silenceThisInstance
}}</FormSwitch
>
</FormSuspense>
<MkButton @click="refreshMetadata"
><i
@ -329,7 +337,7 @@
import { watch } from "vue";
import { Virtual } from "swiper";
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 MkObjectView from "@/components/MkObjectView.vue";
import FormLink from "@/components/form/link.vue";
@ -352,11 +360,13 @@ import "swiper/scss";
import "swiper/scss/virtual";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
type AugmentedInstanceMetadata = calckey.entities.DetailedInstanceMetadata & {
blockedHosts: string[];
silencedHosts: string[];
};
type AugmentedInstance = misskey.entities.Instance & {
type AugmentedInstance = calckey.entities.Instance & {
isBlocked: boolean;
isSilenced: boolean;
};
const props = defineProps<{
@ -373,6 +383,7 @@ let meta = $ref<AugmentedInstanceMetadata | null>(null);
let instance = $ref<AugmentedInstance | null>(null);
let suspended = $ref(false);
let isBlocked = $ref(false);
let isSilenced = $ref(false);
let faviconUrl = $ref(null);
const usersPagination = {
@ -386,16 +397,14 @@ const usersPagination = {
offsetMode: true,
};
async function init() {
meta = await os.api("admin/meta");
}
async function fetch() {
meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
instance = (await os.api("federation/show-instance", {
host: props.host,
})) as AugmentedInstance;
suspended = instance.isSuspended;
isBlocked = instance.isBlocked;
isSilenced = instance.isSilenced;
faviconUrl =
getProxiedImageUrlNullable(instance.faviconUrl, "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) {
await os.api("admin/federation/update-instance", {
host: instance.host,

View File

@ -2,7 +2,7 @@
<MkStickyContainer>
<template #header><MkPageHeader /></template>
<MkSpacer :content-max="800">
<div class="mwysmxbg">
<div :class="$style.root">
<div>{{ i18n.ts._mfm.intro }}</div>
<br />
<div class="section _block">
@ -137,6 +137,18 @@
</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
<div class="section _block">
<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```'
);
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_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_tada = $ref("$[tada 🍮] $[tada.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_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
let preview_sparkle = $ref("$[sparkle 🍮]");
let preview_rotate = $ref("$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]");
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_rotate = $ref(
"$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]"
);
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_bg = $ref("$[bg.color=ff0000 Background color]");
let preview_plain = $ref(
@ -465,8 +486,8 @@ definePageMetadata({
});
</script>
<style lang="scss" scoped>
.mwysmxbg {
<style lang="scss" module>
.root {
background: var(--bg);
> .section {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
id: '02816013-8107-440f-877e-865083ffe194',
name: 'Mi Green+Lime Dark',
name: 'Mi Dark',
author: 'syuilo',
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',
name: 'Mi Ice Dark',
name: 'Ice Dark',
author: 'syuilo',
base: 'dark',

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
name: 'Mi Apricot Light',
name: 'Apricot Light',
author: 'syuilo',
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',
name: 'Mi Cherry Light',
name: 'Cherry Light',
author: 'syuilo',
base: 'light',

View File

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

View File

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

View File

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

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