diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 6903f447d0..6c4854f26a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1,6 +1,6 @@ _lang_: "简体中文" -headlineMisskey: "通过帖子连接在一起的网络" -introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n\ +headlineMisskey: "一个开源、去中心化的社交媒体平台,永远免费!🚀" +introMisskey: "欢迎!Calckey 是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n\ 来探索新的世界吧!🚀" monthAndDay: "{month}月 {day}日" search: "搜索" @@ -10,7 +10,7 @@ password: "密码" forgotPassword: "忘记密码" fetchingAsApObject: "正在联邦宇宙查询中" ok: "OK" -gotIt: "我明白了" +gotIt: "知道了!" cancel: "取消" enterUsername: "输入用户名" renotedBy: "转发自 {user}" @@ -78,7 +78,7 @@ followsYou: "正在关注你" createList: "创建列表" manageLists: "管理列表" error: "错误" -somethingHappened: "出现了一些问题!" +somethingHappened: "发生了一个错误" retry: "重试" pageLoadError: "页面加载失败。" pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。" @@ -202,7 +202,7 @@ noUsers: "无用户" editProfile: "编辑资料" noteDeleteConfirm: "要删除该帖子吗?" pinLimitExceeded: "无法置顶更多帖子了" -intro: "Misskey的部署结束啦!填写管理员账号吧!" +intro: "Calckey安装完成!请创建一个管理员用户。" done: "完成" processing: "正在处理" preview: "预览" @@ -222,10 +222,10 @@ instanceFollowers: "服务器的关注者" instanceUsers: "此服务器的用户" changePassword: "修改密码" security: "安全" -retypedNotMatch: "两次输入不一致!" +retypedNotMatch: "两次输入不匹配。" currentPassword: "现在的密码" newPassword: "新密码" -newPasswordRetype: "重新输入密码:" +newPasswordRetype: "重新输入新密码" attachFile: "插入附件" more: "更多!" featured: "热门" @@ -359,7 +359,7 @@ antennaSource: "接收来源" antennaKeywords: "包含关键字" antennaExcludeKeywords: "排除关键字" antennaKeywordsDescription: "使用空格分隔会产生AND规范,并且使用换行符分隔会产生OR规范" -notifyAntenna: "开启通知" +notifyAntenna: "新帖子通知" withFileAntenna: "仅带有附件的帖子" enableServiceworker: "启用ServiceWorker" antennaUsersDescription: "指定用户名,用换行符分隔" @@ -391,7 +391,7 @@ nUsersMentioned: "{n} 被提到" securityKey: "安全密钥" securityKeyName: "密钥名称" registerSecurityKey: "注册硬件安全密钥" -lastUsed: "最后使用:" +lastUsed: "上次使用" unregister: "删除账户" passwordLessLogin: "无密码登录" resetPassword: "重置密码" @@ -535,7 +535,7 @@ updateRemoteUser: "更新远程用户信息" deleteAllFiles: "删除所有文件" deleteAllFilesConfirm: "要删除所有文件吗?" removeAllFollowing: "取消所有关注" -removeAllFollowingDescription: "取消{host}的所有关注者。当实例不存在时执行。" +removeAllFollowingDescription: "取消 {host} 的所有关注者。如果服务器已不存在,请执行它。" userSuspended: "该用户已被冻结。" userSilenced: "该用户已被禁言。" yourAccountSuspendedTitle: "账户已被冻结" @@ -626,20 +626,20 @@ sample: "示例" abuseReports: "举报" reportAbuse: "举报" reportAbuseOf: "举报{name}" -fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写URL地址。" +fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写 URL 地址。" abuseReported: "内容已发送。感谢您提交信息。" reporter: "举报者" reporteeOrigin: "举报来源" reporterOrigin: "举报者来源" -forwardReport: "将该举报信息转发给远程实例" -forwardReportIsAnonymous: "勾选则在远程实例上显示的举报者是匿名的系统账号,而不是您的账号。" +forwardReport: "将该举报信息转发给远程服务器" +forwardReportIsAnonymous: "勾选则在远程服务器上显示的举报者是匿名的系统账号,而不是您的账号。" send: "发送" abuseMarkAsResolved: "处理完毕" openInNewTab: "在新标签页中打开" openInSideView: "在侧边栏中打开" defaultNavigationBehaviour: "默认导航" editTheseSettingsMayBreakAccount: "编辑这些设置可以会损坏您的账号" -instanceTicker: "帖子的实例信息" +instanceTicker: "帖子的服务器信息" waitingFor: "等待{x}" random: "随机" system: "系统" @@ -650,16 +650,16 @@ createNew: "新建" optional: "可选" createNewClip: "新建便签" unclip: "移除便签" -confirmToUnclipAlreadyClippedNote: "本帖已包含在便签\"{name}\"里。您想要将本帖从该便签中移除吗?" +confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?" public: "公开" i18nInfo: "Calckey已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过{link}帮助翻译。" manageAccessTokens: "管理 Access Tokens" accountInfo: "账户信息" notesCount: "帖子数量" repliesCount: "回复数量" -renotesCount: "转帖数量" +renotesCount: "推贴数量" repliedCount: "回复数" -renotedCount: "转发数" +renotedCount: "收到的推贴数" followingCount: "正在关注数量" followersCount: "关注者数量" sentReactionsCount: "发送回应数" @@ -703,7 +703,7 @@ onlineUsersCount: "{n}人在线" nUsers: "{n}用户" nNotes: "{n} 帖子" sendErrorReports: "发送错误报告" -sendErrorReportsDescription: "启用后,如果出现问题,可以与Misskey共享详细的错误信息,从而帮助提高软件的质量。" +sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。" myTheme: "我的主题" backgroundColor: "背景" accentColor: "强调色" @@ -727,7 +727,7 @@ capacity: "容量" inUse: "已使用" editCode: "编辑代码" apply: "应用" -receiveAnnouncementFromInstance: "从实例接收通知" +receiveAnnouncementFromInstance: "从服务器接收通知" emailNotification: "邮件通知" publish: "发布" inChannelSearch: "频道内搜索" @@ -759,7 +759,7 @@ instanceBlocking: "联邦管理" selectAccount: "选择账户" switchAccount: "切换账户" enabled: "已启用" -disabled: "已禁用 " +disabled: "已禁用" quickAction: "快捷操作" user: "用户" administration: "管理" @@ -839,8 +839,8 @@ themeColor: "服务器滚动条颜色" size: "大小" numberOfColumn: "列数" searchByGoogle: "Google" -instanceDefaultLightTheme: "实例默认浅色主题" -instanceDefaultDarkTheme: "实例默认深色主题" +instanceDefaultLightTheme: "服务器默认浅色主题" +instanceDefaultDarkTheme: "服务器默认深色主题" instanceDefaultThemeDescription: "以对象格式键入主题代码" mutePeriod: "屏蔽期限" indefinitely: "永久" @@ -863,7 +863,7 @@ check: "检查" driveCapOverrideLabel: "變更此用戶的雲端硬碟容量上限" driveCapOverrideCaption: "设定为 0 以下则会解除此限制。" requireAdminForView: "需要使用管理员账户登录才能查看。" -isSystemAccount: "该账号由系统自动创建和管理。" +isSystemAccount: "该账号由系统自动创建和管理。请不要修改、编辑、删除或以其他方式篡改这个账户,否则可能会破坏你的服务器。" typeToConfirm: "输入 {x} 以确认操作。" deleteAccount: "删除账户" document: "文档" @@ -875,7 +875,7 @@ statusbar: "状态栏" pleaseSelect: "请选择" reverse: "翻转" colored: "彩色" -refreshInterval: "刷新间隔" +refreshInterval: "更新间隔 " label: "标签" type: "类型" speed: "速度" @@ -889,7 +889,7 @@ cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法 cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。" beta: "测试" enableAutoSensitive: "自动 NSFW 识别" -enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据实例自动设置。" +enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据服务器自动设置。" activeEmailValidationDescription: "积极地验证用户的电子邮件地址,判断它是一次性的电子邮件地址,还是可以实际通信的地址。关闭时,则只检查字符串是否正确。" navbar: "导航栏" shuffle: "随机" @@ -935,8 +935,8 @@ _ad: reduceFrequencyOfThisAd: "减少此广告的频率" _forgotPassword: enterEmail: "请输入您验证账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。" - ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系管理员。" - contactAdmin: "该实例不支持发送电子邮件。如果您想重设密码,请联系管理员。" + ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系服务器管理员。" + contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。" _gallery: my: "我的图库" liked: "喜欢的图片" @@ -1109,10 +1109,10 @@ _wordMute: hard: "硬屏蔽" mutedNotes: "已静音的帖子" _instanceMute: - instanceMuteDescription: "屏蔽配置实例中的所有帖子和转帖,包括实例的用户回复。" + instanceMuteDescription: "屏蔽列出服务器中的所有帖子和转帖,包括服务器的用户回复。" instanceMuteDescription2: "设置时用换行符来分隔" - title: "隐藏实例已设置的帖子。" - heading: "屏蔽实例" + title: "隐藏服务器已设置的帖子。" + heading: "要静音的服务器列表" _theme: explore: "寻找主题" install: "安装主题" @@ -1215,22 +1215,22 @@ _tutorial: step1_1: "欢迎!" step1_2: "让我们把你安排好。你很快就会启动并运行!" step2_1: "首先,请完成您的个人资料。" - step2_2: "通过提供一些关于你自己的信息,其他人会更容易了解他们是否想看到你的帖子或关注你。" - step3_1: "现在是时候跟随一些人了!" + step2_2: "提供一些关于你的信息,让其他人更容易知道他们是否想看你的帖子或关注你。" + step3_1: "现在是时候关注一些人了!" step3_2: "你的主页和社交馈送是基于你所关注的人,所以试着先关注几个账户。{n点击个人资料右上角的加号圈就可以关注它。" step4_1: "让我们出去找你。" - step4_2: "对于他们的第一条信息,有些人喜欢做{introduction}或一个简单的 \"hello world!\"" - step5_1: "时间限制,到处是时间限制!" - step5_2: "您的实例已启用各种时间线的{timelines}。" - step5_3: "主{icon}时间线是你可以看到你的订阅者的帖子的时间线。" - step5_4: "本地{icon}时间线是你可以看到实例中所有其他用户的信息的时间线。" - step5_5: "推荐的{icon}时间线 - 是时间轴,你可以看到管理员推荐的实例的信息" - step5_6: "社交{icon}时间线显示来自你的订阅者朋友的信息。" - step5_7: "全球{icon}时间线是你可以看到来自所有其他连接的实例的消息。" + step4_2: "对于第一条帖子,可以做一个 {introduction} 或一个简单的 \"hello world!\"" + step5_1: "时间线,无处不在的时间线!" + step5_2: "您的服务器已启用{timelines}种不同的时间线。" + step5_3: "主页{icon}时间线是你可以看到你关注账户的帖子的时间线。" + step5_4: "本地{icon}时间线是你可以看到此服务器上其它用户的帖子的时间线。" + step5_5: "社交{icon}时间线是主页和本地时间线的结合。" + step5_6: "推荐{icon}时间线是你可以看到管理员推荐服务器的帖子的时间线。" + step5_7: "全球{icon}时间线是你可以看到来自其它所有互联服务器的帖子的时间线。" step6_1: "那么,这里是什么地方?" - step6_2: "好吧,你不只是加入卡尔基。你已经加入了Fediverse的一个门户,这是一个由成千上万台服务器组成的互联网络,被称为 \"实例\"" + step6_2: "好吧,你不只是加入Calckey。你已经加入了Fediverse的一个门户,这是一个由成千上万台服务器组成的互联网络。" step6_3: "每个服务器的工作方式不同,并不是所有的服务器都运行Calckey。但这个人确实如此! 这有点复杂,但你很快就会明白的。" - step6_4: "现在去学习并享受乐趣!" + step6_4: "现在,去吧,去探索,去享受乐趣吧!" _2fa: alreadyRegistered: "此设备已被注册" registerTOTP: "注册设备" @@ -1292,7 +1292,7 @@ _permissions: _auth: shareAccess: "您要授权允许“{name}”访问您的帐户吗?" shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?" - permissionAsk: "这个应用程序需要以下权限" + permissionAsk: "此应用程序请求以下权限:" pleaseGoBack: "请返回到应用程序" callback: "回到应用程序" denied: "拒绝访问" @@ -1372,11 +1372,11 @@ _poll: remainingSeconds: "{s}秒后截止" _visibility: public: "公开" - publicDescription: "您的帖子将出现在全局时间线上" + publicDescription: "您的帖子将出现在公共时间线上" home: "不公开" homeDescription: "仅发送至首页的时间线" followers: "仅关注者" - followersDescription: "仅发送至关注者" + followersDescription: "仅对你的关注者和提及的用户可见" specified: "指定用户" specifiedDescription: "仅发送至指定用户" localOnly: "仅限本地" @@ -1433,7 +1433,7 @@ _instanceCharts: usersTotal: "用户总计" notes: "帖子:增加/减少" notesTotal: "帖子总计" - ff: "关注/被关注:数量变化" + ff: "被关注用户/关注者的数量差异 " ffTotal: "关注/被关注者总计" cacheSize: "缓存大小:增加/减少" cacheSizeTotal: "缓存大小总计" @@ -1522,7 +1522,7 @@ _pages: note: "嵌入的帖子" _note: id: "帖子ID" - idDescription: "您也可以通过粘贴帖子的URL来进行设置。" + idDescription: "你也可以将帖子 URL 粘贴到此处。" detailed: "显示详细信息" switch: "开关" _switch: @@ -1939,3 +1939,5 @@ isPatron: Calckey 赞助 _dialog: charactersExceeded: 超出了最大字符数!当前:{current} / 限制:{max} charactersBelow: 没有足够的字符!当前:{current} / 限制:{min} +enableIdenticonGeneration: 启用Identicon生成 +enableServerMachineStats: 启用服务器硬件统计 diff --git a/packages/backend/package.json b/packages/backend/package.json index b584a56910..6f63441023 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -34,6 +34,7 @@ "@koa/cors": "3.4.3", "@koa/multer": "3.0.2", "@koa/router": "9.0.1", + "@msgpack/msgpack": "3.0.0-beta2", "@peertube/http-signature": "1.7.0", "@redocly/openapi-core": "1.0.0-beta.120", "@sinonjs/fake-timers": "9.1.2", @@ -43,7 +44,6 @@ "ajv": "8.12.0", "archiver": "5.3.1", "argon2": "^0.30.3", - "async-mutex": "^0.4.0", "autobind-decorator": "2.4.0", "autolinker": "4.0.0", "autwh": "0.1.0", diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 9abebc91cb..fe68908e57 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,43 +1,85 @@ +import { redisClient } from "@/db/redis.js"; +import { encode, decode } from "@msgpack/msgpack"; +import { ChainableCommander } from "ioredis"; + export class Cache<T> { - public cache: Map<string | null, { date: number; value: T }>; - private lifetime: number; + private ttl: number; + private prefix: string; - constructor(lifetime: Cache<never>["lifetime"]) { - this.cache = new Map(); - this.lifetime = lifetime; + constructor(name: string, ttlSeconds: number) { + this.ttl = ttlSeconds; + this.prefix = `cache:${name}`; } - public set(key: string | null, value: T): void { - this.cache.set(key, { - date: Date.now(), - value, - }); + private prefixedKey(key: string | null): string { + return key ? `${this.prefix}:${key}` : this.prefix; } - public get(key: string | null): T | undefined { - const cached = this.cache.get(key); - if (cached == null) return undefined; - if (Date.now() - cached.date > this.lifetime) { - this.cache.delete(key); - return undefined; + public async set( + key: string | null, + value: T, + transaction?: ChainableCommander, + ): Promise<void> { + const _key = this.prefixedKey(key); + const _value = Buffer.from(encode(value)); + const commander = transaction ?? redisClient; + await commander.set(_key, _value, "EX", this.ttl); + } + + public async get(key: string | null, renew = false): Promise<T | undefined> { + const _key = this.prefixedKey(key); + const cached = await redisClient.getBuffer(_key); + if (cached === null) return undefined; + + if (renew) await redisClient.expire(_key, this.ttl); + + return decode(cached) as T; + } + + public async getAll(renew = false): Promise<Map<string, T>> { + const keys = await redisClient.keys(`${this.prefix}*`); + const map = new Map<string, T>(); + if (keys.length === 0) { + return map; } - return cached.value; + const values = await redisClient.mgetBuffer(keys); + + for (const [i, key] of keys.entries()) { + const val = values[i]; + if (val !== null) { + map.set(key, decode(val) as T); + } + } + + if (renew) { + const trans = redisClient.multi(); + for (const key of map.keys()) { + trans.expire(key, this.ttl); + } + await trans.exec(); + } + + return map; } - public delete(key: string | null) { - this.cache.delete(key); + public async delete(...keys: (string | null)[]): Promise<void> { + if (keys.length > 0) { + const _keys = keys.map(this.prefixedKey); + await redisClient.del(_keys); + } } /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * Returns if cached value exists. Otherwise, calls fetcher and caches. + * Overwrites cached value if invalidated by the optional validator. */ public async fetch( key: string | null, fetcher: () => Promise<T>, + renew = false, validator?: (cachedValue: T) => boolean, ): Promise<T> { - const cachedValue = this.get(key); + const cachedValue = await this.get(key, renew); if (cachedValue !== undefined) { if (validator) { if (validator(cachedValue)) { @@ -52,20 +94,21 @@ export class Cache<T> { // Cache MISS const value = await fetcher(); - this.set(key, value); + await this.set(key, value); return value; } /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * Returns if cached value exists. Otherwise, calls fetcher and caches if the fetcher returns a value. + * Overwrites cached value if invalidated by the optional validator. */ public async fetchMaybe( key: string | null, fetcher: () => Promise<T | undefined>, + renew = false, validator?: (cachedValue: T) => boolean, ): Promise<T | undefined> { - const cachedValue = this.get(key); + const cachedValue = await this.get(key, renew); if (cachedValue !== undefined) { if (validator) { if (validator(cachedValue)) { @@ -81,7 +124,7 @@ export class Cache<T> { // Cache MISS const value = await fetcher(); if (value !== undefined) { - this.set(key, value); + await this.set(key, value); } return value; } diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts index 358fba0f37..1ff09d6299 100644 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ b/packages/backend/src/misc/check-hit-antenna.ts @@ -11,7 +11,7 @@ import * as Acct from "@/misc/acct.js"; import type { Packed } from "./schema.js"; import { Cache } from "./cache.js"; -const blockingCache = new Cache<User["id"][]>(1000 * 60 * 5); +const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5); // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている diff --git a/packages/backend/src/misc/emoji-meta.ts b/packages/backend/src/misc/emoji-meta.ts index fd9d9baa5c..2b9365b826 100644 --- a/packages/backend/src/misc/emoji-meta.ts +++ b/packages/backend/src/misc/emoji-meta.ts @@ -1,33 +1,41 @@ import probeImageSize from "probe-image-size"; -import { Mutex, withTimeout } from "async-mutex"; +import { Mutex } from "redis-semaphore"; import { FILE_TYPE_BROWSERSAFE } from "@/const.js"; import Logger from "@/services/logger.js"; import { Cache } from "./cache.js"; +import { redisClient } from "@/db/redis.js"; export type Size = { width: number; height: number; }; -const cache = new Cache<boolean>(1000 * 60 * 10); // once every 10 minutes for the same url -const mutex = withTimeout(new Mutex(), 1000); +const cache = new Cache<boolean>("emojiMeta", 60 * 10); // once every 10 minutes for the same url +const logger = new Logger("emoji"); export async function getEmojiSize(url: string): Promise<Size> { - const logger = new Logger("emoji"); + let attempted = true; - await mutex.runExclusive(() => { - const attempted = cache.get(url); - if (!attempted) { - cache.set(url, true); - } else { - logger.warn(`Attempt limit exceeded: ${url}`); - throw new Error("Too many attempts"); - } - }); + const lock = new Mutex(redisClient, "getEmojiSize"); + await lock.acquire(); try { - logger.info(`Retrieving emoji size from ${url}`); + attempted = (await cache.get(url)) === true; + if (!attempted) { + await cache.set(url, true); + } + } finally { + await lock.release(); + } + + if (attempted) { + logger.warn(`Attempt limit exceeded: ${url}`); + throw new Error("Too many attempts"); + } + + try { + logger.debug(`Retrieving emoji size from ${url}`); const { width, height, mime } = await probeImageSize(url, { timeout: 5000, }); diff --git a/packages/backend/src/misc/keypair-store.ts b/packages/backend/src/misc/keypair-store.ts index 4551bfd988..6255773599 100644 --- a/packages/backend/src/misc/keypair-store.ts +++ b/packages/backend/src/misc/keypair-store.ts @@ -3,10 +3,12 @@ import type { User } from "@/models/entities/user.js"; import type { UserKeypair } from "@/models/entities/user-keypair.js"; import { Cache } from "./cache.js"; -const cache = new Cache<UserKeypair>(Infinity); +const cache = new Cache<UserKeypair>("keypairStore", 60 * 30); export async function getUserKeypair(userId: User["id"]): Promise<UserKeypair> { - return await cache.fetch(userId, () => - UserKeypairs.findOneByOrFail({ userId: userId }), + return await cache.fetch( + userId, + () => UserKeypairs.findOneByOrFail({ userId: userId }), + true, ); } diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts index 7aee4ec253..795a267f91 100644 --- a/packages/backend/src/misc/populate-emojis.ts +++ b/packages/backend/src/misc/populate-emojis.ts @@ -7,8 +7,9 @@ import { isSelfHost, toPunyNullable } from "./convert-host.js"; import { decodeReaction } from "./reaction-lib.js"; import config from "@/config/index.js"; import { query } from "@/prelude/url.js"; +import { redisClient } from "@/db/redis.js"; -const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12); +const cache = new Cache<Emoji | null>("populateEmojis", 60 * 60 * 12); /** * 添付用絵文字情報 @@ -75,7 +76,7 @@ export async function populateEmoji( if (emoji && !(emoji.width && emoji.height)) { emoji = await queryOrNull(); - cache.set(cacheKey, emoji); + await cache.set(cacheKey, emoji); } if (emoji == null) return null; @@ -150,7 +151,7 @@ export async function prefetchEmojis( emojis: { name: string; host: string | null }[], ): Promise<void> { const notCachedEmojis = emojis.filter( - (emoji) => cache.get(`${emoji.name} ${emoji.host}`) == null, + async (emoji) => !(await cache.get(`${emoji.name} ${emoji.host}`)), ); const emojisQuery: any[] = []; const hosts = new Set(notCachedEmojis.map((e) => e.host)); @@ -169,7 +170,9 @@ export async function prefetchEmojis( select: ["name", "host", "originalUrl", "publicUrl"], }) : []; + const trans = redisClient.multi(); for (const emoji of _emojis) { - cache.set(`${emoji.name} ${emoji.host}`, emoji); + cache.set(`${emoji.name} ${emoji.host}`, emoji, trans); } + await trans.exec(); } diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 48c8d75b3b..5ca36e3d31 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -1,4 +1,3 @@ -import { URL } from "url"; import { In, Not } from "typeorm"; import Ajv from "ajv"; import type { ILocalUser, IRemoteUser } from "@/models/entities/user.js"; @@ -40,7 +39,10 @@ import { } from "../index.js"; import type { Instance } from "../entities/instance.js"; -const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3); +const userInstanceCache = new Cache<Instance | null>( + "userInstance", + 60 * 60 * 3, +); type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<"UserDetailed"> diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index 6e448d4b17..a710b9f115 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -5,7 +5,6 @@ import type { CacheableRemoteUser, CacheableUser, } from "@/models/entities/user.js"; -import { User, IRemoteUser } from "@/models/entities/user.js"; import type { UserPublickey } from "@/models/entities/user-publickey.js"; import type { MessagingMessage } from "@/models/entities/messaging-message.js"; import { @@ -20,8 +19,11 @@ import type { IObject } from "./type.js"; import { getApId } from "./type.js"; import { resolvePerson } from "./models/person.js"; -const publicKeyCache = new Cache<UserPublickey | null>(Infinity); -const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity); +const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30); +const publicKeyByUserIdCache = new Cache<UserPublickey | null>( + "publicKeyByUserId", + 60 * 30, +); export type UriParseResult = | { @@ -123,17 +125,23 @@ export default class DbResolver { if (parsed.type !== "users") return null; return ( - (await userByIdCache.fetchMaybe(parsed.id, () => - Users.findOneBy({ - id: parsed.id, - }).then((x) => x ?? undefined), + (await userByIdCache.fetchMaybe( + parsed.id, + () => + Users.findOneBy({ + id: parsed.id, + }).then((x) => x ?? undefined), + true, )) ?? null ); } else { - return await uriPersonCache.fetch(parsed.uri, () => - Users.findOneBy({ - uri: parsed.uri, - }), + return await uriPersonCache.fetch( + parsed.uri, + () => + Users.findOneBy({ + uri: parsed.uri, + }), + true, ); } } @@ -156,14 +164,17 @@ export default class DbResolver { return key; }, + true, (key) => key != null, ); if (key == null) return null; return { - user: (await userByIdCache.fetch(key.userId, () => - Users.findOneByOrFail({ id: key.userId }), + user: (await userByIdCache.fetch( + key.userId, + () => Users.findOneByOrFail({ id: key.userId }), + true, )) as CacheableRemoteUser, key, }; @@ -183,6 +194,7 @@ export default class DbResolver { const key = await publicKeyByUserIdCache.fetch( user.id, () => UserPublickeys.findOneBy({ userId: user.id }), + true, (v) => v != null, ); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index f8208e6d7b..c5519ba031 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -135,14 +135,14 @@ export async function fetchPerson( ): Promise<CacheableUser | null> { if (typeof uri !== "string") throw new Error("uri is not string"); - const cached = uriPersonCache.get(uri); + const cached = await uriPersonCache.get(uri, true); if (cached) return cached; // Fetch from the database if the URI points to this server if (uri.startsWith(`${config.url}/`)) { const id = uri.split("/").pop(); const u = await Users.findOneBy({ id }); - if (u) uriPersonCache.set(uri, u); + if (u) await uriPersonCache.set(uri, u); return u; } @@ -150,7 +150,7 @@ export async function fetchPerson( const exist = await Users.findOneBy({ uri }); if (exist) { - uriPersonCache.set(uri, exist); + await uriPersonCache.set(uri, exist); return exist; } //#endregion diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts index 42274ad2a4..460a0ce84b 100644 --- a/packages/backend/src/server/api/authenticate.ts +++ b/packages/backend/src/server/api/authenticate.ts @@ -9,7 +9,7 @@ import { localUserByNativeTokenCache, } from "@/services/user-cache.js"; -const appCache = new Cache<App>(Infinity); +const appCache = new Cache<App>("app", 60 * 30); export class AuthenticationError extends Error { constructor(message: string) { @@ -49,6 +49,7 @@ export default async ( const user = await localUserByNativeTokenCache.fetch( token, () => Users.findOneBy({ token }) as Promise<ILocalUser | null>, + true, ); if (user == null) { @@ -82,11 +83,14 @@ export default async ( Users.findOneBy({ id: accessToken.userId, }) as Promise<ILocalUser>, + true, ); if (accessToken.appId) { - const app = await appCache.fetch(accessToken.appId, () => - Apps.findOneByOrFail({ id: accessToken.appId! }), + const app = await appCache.fetch( + accessToken.appId, + () => Apps.findOneByOrFail({ id: accessToken.appId! }), + true, ); return [ diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 7d40816135..4366406ec3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -6,7 +6,7 @@ import { ApiError } from "../../../error.js"; import rndstr from "rndstr"; import { publishBroadcastStream } from "@/services/stream.js"; import { db } from "@/db/postgre.js"; -import { type Size, getEmojiSize } from "@/misc/emoji-meta.js"; +import { getEmojiSize } from "@/misc/emoji-meta.js"; export const meta = { tags: ["admin"], @@ -40,12 +40,7 @@ export default define(meta, paramDef, async (ps, me) => { ? file.name.split(".")[0] : `_${rndstr("a-z0-9", 8)}_`; - let size: Size = { width: 0, height: 0 }; - try { - size = await getEmojiSize(file.url); - } catch { - /* skip if any error happens */ - } + const size = await getEmojiSize(file.url); const emoji = await Emojis.insert({ id: genId(), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 45cb9464db..c90e606335 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -6,7 +6,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js"; import { uploadFromUrl } from "@/services/drive/upload-from-url.js"; import { publishBroadcastStream } from "@/services/stream.js"; import { db } from "@/db/postgre.js"; -import { type Size, getEmojiSize } from "@/misc/emoji-meta.js"; +import { getEmojiSize } from "@/misc/emoji-meta.js"; export const meta = { tags: ["admin"], @@ -65,12 +65,7 @@ export default define(meta, paramDef, async (ps, me) => { throw new ApiError(); } - let size: Size = { width: 0, height: 0 }; - try { - size = await getEmojiSize(driveFile.url); - } catch { - /* skip if any error happens */ - } + const size = await getEmojiSize(driveFile.url); const copied = await Emojis.insert({ id: genId(), diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts index dbfb28ff6a..940ca2e135 100644 --- a/packages/backend/src/server/nodeinfo.ts +++ b/packages/backend/src/server/nodeinfo.ts @@ -100,7 +100,10 @@ const nodeinfo2 = async () => { }; }; -const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); +const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>( + "nodeinfo", + 60 * 10, +); router.get(nodeinfo2_1path, async (ctx) => { const base = await cache.fetch(null, () => nodeinfo2()); diff --git a/packages/backend/src/services/chart/charts/active-users.ts b/packages/backend/src/services/chart/charts/active-users.ts index 7a0c45cfaf..15317e68b0 100644 --- a/packages/backend/src/services/chart/charts/active-users.ts +++ b/packages/backend/src/services/chart/charts/active-users.ts @@ -25,12 +25,12 @@ export default class ActiveUsersChart extends Chart<typeof schema> { return {}; } - public async read(user: { + public read(user: { id: User["id"]; host: null; createdAt: User["createdAt"]; - }): Promise<void> { - await this.commit({ + }) { + this.commit({ read: [user.id], registeredWithinWeek: Date.now() - user.createdAt.getTime() < week ? [user.id] : [], diff --git a/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts index 50ce227eba..a8b34ea57b 100644 --- a/packages/backend/src/services/instance-actor.ts +++ b/packages/backend/src/services/instance-actor.ts @@ -6,10 +6,10 @@ import { IsNull } from "typeorm"; const ACTOR_USERNAME = "instance.actor" as const; -const cache = new Cache<ILocalUser>(Infinity); +const cache = new Cache<ILocalUser>("instanceActor", 60 * 30); export async function getInstanceActor(): Promise<ILocalUser> { - const cached = cache.get(null); + const cached = await cache.get(null, true); if (cached) return cached; const user = (await Users.findOneBy({ @@ -18,11 +18,11 @@ export async function getInstanceActor(): Promise<ILocalUser> { })) as ILocalUser | undefined; if (user) { - cache.set(null, user); + await cache.set(null, user); return user; } else { const created = (await createSystemUser(ACTOR_USERNAME)) as ILocalUser; - cache.set(null, created); + await cache.set(null, created); return created; } } diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index f00678ce22..095c75f427 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -29,17 +29,14 @@ import { Notes, Instances, UserProfiles, - Antennas, - Followings, MutedNotes, Channels, ChannelFollowings, - Blockings, NoteThreadMutings, } from "@/models/index.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import type { App } from "@/models/entities/app.js"; -import { Not, In, IsNull } from "typeorm"; +import { Not, In } from "typeorm"; import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; import { genId } from "@/misc/gen-id.js"; import { @@ -73,7 +70,7 @@ import { Mutex } from "redis-semaphore"; const mutedWordsCache = new Cache< { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] ->(1000 * 60 * 5); +>("mutedWords", 60 * 5); type NotificationType = "reply" | "renote" | "quote" | "mention"; diff --git a/packages/backend/src/services/register-or-fetch-instance-doc.ts b/packages/backend/src/services/register-or-fetch-instance-doc.ts index 4c3570e907..c0ead08190 100644 --- a/packages/backend/src/services/register-or-fetch-instance-doc.ts +++ b/packages/backend/src/services/register-or-fetch-instance-doc.ts @@ -4,30 +4,30 @@ import { genId } from "@/misc/gen-id.js"; import { toPuny } from "@/misc/convert-host.js"; import { Cache } from "@/misc/cache.js"; -const cache = new Cache<Instance>(1000 * 60 * 60); +const cache = new Cache<Instance>("registerOrFetchInstanceDoc", 60 * 60); export async function registerOrFetchInstanceDoc( host: string, ): Promise<Instance> { - host = toPuny(host); + const _host = toPuny(host); - const cached = cache.get(host); + const cached = await cache.get(_host); if (cached) return cached; - const index = await Instances.findOneBy({ host }); + const index = await Instances.findOneBy({ host: _host }); if (index == null) { const i = await Instances.insert({ id: genId(), - host, + host: _host, caughtAt: new Date(), lastCommunicatedAt: new Date(), }).then((x) => Instances.findOneByOrFail(x.identifiers[0])); - cache.set(host, i); + await cache.set(_host, i); return i; } else { - cache.set(host, index); + await cache.set(_host, index); return index; } } diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index bec4b1f86b..6f7829c218 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -15,7 +15,7 @@ import { createSystemUser } from "./create-system-user.js"; const ACTOR_USERNAME = "relay.actor" as const; -const relaysCache = new Cache<Relay[]>(1000 * 60 * 10); +const relaysCache = new Cache<Relay[]>("relay", 60 * 10); export async function getRelayActor(): Promise<ILocalUser> { const user = await Users.findOneBy({ @@ -90,7 +90,7 @@ async function updateRelaysCache() { const relays = await Relays.findBy({ status: "accepted", }); - relaysCache.set(null, relays); + await relaysCache.set(null, relays); } export async function relayRejected(id: string) { diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts index 9492448554..ed700185df 100644 --- a/packages/backend/src/services/user-cache.ts +++ b/packages/backend/src/services/user-cache.ts @@ -3,17 +3,23 @@ import type { CacheableUser, ILocalUser, } from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; import { Users } from "@/models/index.js"; import { Cache } from "@/misc/cache.js"; -import { subscriber } from "@/db/redis.js"; +import { redisClient, subscriber } from "@/db/redis.js"; -export const userByIdCache = new Cache<CacheableUser>(Infinity); +export const userByIdCache = new Cache<CacheableUser>("userById", 60 * 30); export const localUserByNativeTokenCache = new Cache<CacheableLocalUser | null>( - Infinity, + "localUserByNativeToken", + 60 * 30, +); +export const localUserByIdCache = new Cache<CacheableLocalUser>( + "localUserByIdCache", + 60 * 30, +); +export const uriPersonCache = new Cache<CacheableUser | null>( + "uriPerson", + 60 * 30, ); -export const localUserByIdCache = new Cache<CacheableLocalUser>(Infinity); -export const uriPersonCache = new Cache<CacheableUser | null>(Infinity); subscriber.on("message", async (_, data) => { const obj = JSON.parse(data); @@ -22,13 +28,12 @@ subscriber.on("message", async (_, data) => { const { type, body } = obj.message; switch (type) { case "localUserUpdated": { - userByIdCache.delete(body.id); - localUserByIdCache.delete(body.id); - localUserByNativeTokenCache.cache.forEach((v, k) => { - if (v.value?.id === body.id) { - localUserByNativeTokenCache.delete(k); - } - }); + await userByIdCache.delete(body.id); + await localUserByIdCache.delete(body.id); + const toDelete = Array.from(await localUserByNativeTokenCache.getAll()) + .filter((v) => v[1]?.id === body.id) + .map((v) => v[0]); + await localUserByNativeTokenCache.delete(...toDelete); break; } case "userChangeSuspendedState": @@ -36,15 +41,17 @@ subscriber.on("message", async (_, data) => { case "userChangeModeratorState": case "remoteUserUpdated": { const user = await Users.findOneByOrFail({ id: body.id }); - userByIdCache.set(user.id, user); - for (const [k, v] of uriPersonCache.cache.entries()) { - if (v.value?.id === user.id) { - uriPersonCache.set(k, user); + await userByIdCache.set(user.id, user); + const trans = redisClient.multi(); + for (const [k, v] of (await uriPersonCache.getAll()).entries()) { + if (v?.id === user.id) { + await uriPersonCache.set(k, user, trans); } } + await trans.exec(); if (Users.isLocalUser(user)) { - localUserByNativeTokenCache.set(user.token, user); - localUserByIdCache.set(user.id, user); + await localUserByNativeTokenCache.set(user.token, user); + await localUserByIdCache.set(user.id, user); } break; } @@ -52,8 +59,8 @@ subscriber.on("message", async (_, data) => { const user = (await Users.findOneByOrFail({ id: body.id, })) as ILocalUser; - localUserByNativeTokenCache.delete(body.oldToken); - localUserByNativeTokenCache.set(body.newToken, user); + await localUserByNativeTokenCache.delete(body.oldToken); + await localUserByNativeTokenCache.set(body.newToken, user); break; } default: diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMedia.vue similarity index 54% rename from packages/client/src/components/MkMediaImage.vue rename to packages/client/src/components/MkMedia.vue index cbd5c0515e..990ec752d8 100644 --- a/packages/client/src/components/MkMediaImage.vue +++ b/packages/client/src/components/MkMedia.vue @@ -1,34 +1,67 @@ <template> - <button v-if="hide" class="qjewsnkg" @click="hide = false"> - <ImgWithBlurhash - :hash="image.blurhash" - :title="image.comment" - :alt="image.comment" - /> - <div class="text"> - <div class="wrapper"> - <b style="display: block" - ><i class="ph-warning ph-bold ph-lg"></i> - {{ i18n.ts.sensitive }}</b - > - <span style="display: block">{{ i18n.ts.clickToShow }}</span> - </div> - </div> - </button> - <div v-else class="gqnyydlz"> - <a :href="image.url"> + <div class="media" :class="{ mini: plyrMini }"> + <button v-if="hide" class="hidden" @click="hide = false"> <ImgWithBlurhash - :hash="image.blurhash" - :src="url" - :alt="image.comment" - :type="image.type" - :cover="false" + :hash="media.blurhash" + :title="media.comment" + :alt="media.comment" /> - <div v-if="image.type === 'image/gif'" class="gif">GIF</div> - </a> + <div class="text"> + <div class="wrapper"> + <b style="display: block" + ><i class="ph-warning ph-bold ph-lg"></i> + {{ i18n.ts.sensitive }}</b + > + <span style="display: block">{{ i18n.ts.clickToShow }}</span> + </div> + </div> + </button> + <template v-else> + <a + v-if="media.type.startsWith('image')" + :href="media.url" + > + <ImgWithBlurhash + :hash="media.blurhash" + :src="url" + :alt="media.comment" + :type="media.type" + :cover="false" + /> + <div v-if="media.type === 'image/gif'" class="gif">GIF</div> + </a> + <VuePlyr + v-if="media.type.startsWith('video')" + ref="plyr" + :options="{ + controls: [ + 'play-large', + 'play', + 'progress', + 'current-time', + 'mute', + 'volume', + 'pip', + 'download', + 'fullscreen', + ], + disableContextMenu: false, + }" + > + <video + :poster="media.thumbnailUrl" + :aria-label="media.comment" + preload="none" + controls + @contextmenu.stop + > + <source :src="media.url" :type="media.type" /> + </video> + </VuePlyr> + </template> <div class="buttons"> <button - v-if="image.comment" + v-if="media.comment" v-tooltip="i18n.ts.alt" class="_button" @click.stop="captionPopup" @@ -36,9 +69,10 @@ <i class="ph-subtitles ph-bold ph-lg"></i> </button> <button + v-if="!hide" v-tooltip="i18n.ts.hide" class="_button" - @click="hide = true" + @click.stop="hide = true" > <i class="ph-eye-slash ph-bold ph-lg"></i> </button> @@ -47,7 +81,9 @@ </template> <script lang="ts" setup> -import { watch } from "vue"; +import { watch, ref, onMounted } from "vue"; +import VuePlyr from "vue-plyr"; +import "vue-plyr/dist/vue-plyr.css"; import type * as misskey from "calckey-js"; import { getStaticImageUrl } from "@/scripts/get-static-image-url"; import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue"; @@ -56,34 +92,37 @@ import { i18n } from "@/i18n"; import * as os from "@/os"; const props = defineProps<{ - image: misskey.entities.DriveFile; + media: misskey.entities.DriveFile; raw?: boolean; }>(); let hide = $ref(true); +const plyr = ref(); +const plyrMini = ref(false); + const url = props.raw || defaultStore.state.loadRawImages - ? props.image.url + ? props.media.url : defaultStore.state.disableShowingAnimatedImages - ? getStaticImageUrl(props.image.thumbnailUrl) - : props.image.thumbnailUrl; + ? getStaticImageUrl(props.media.thumbnailUrl) + : props.media.thumbnailUrl; function captionPopup() { os.alert({ type: "info", - text: props.image.comment, + text: props.media.comment, }); } // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする watch( - () => props.image, + () => props.media, () => { hide = defaultStore.state.nsfw === "force" ? true - : props.image.isSensitive && + : props.media.isSensitive && defaultStore.state.nsfw !== "ignore"; }, { @@ -91,12 +130,25 @@ watch( immediate: true, } ); + +onMounted(() => { + if (props.media.type.startsWith('video')) { + plyrMini.value = plyr.value.player.media.scrollWidth < 300; + if (plyrMini.value) { + plyr.value.player.on("play", () => { + plyr.value.player.fullscreen.enter(); + }); + } + } +}); </script> <style lang="scss" scoped> -.qjewsnkg { +.hidden { all: unset; position: relative; + width: 100%; + height: 100%; > .text { position: relative; @@ -123,7 +175,7 @@ watch( } } -.gqnyydlz { +.media { position: relative; background: var(--bg); @@ -175,5 +227,16 @@ watch( pointer-events: none; } } + &.mini { + :deep(.plyr:not(:fullscreen)) { + min-width: unset !important; + .plyr__control--overlaid, + .plyr__progress__container, + .plyr__volume, + [data-plyr="fullscreen"] { + display: none; + } + } + } } </style> diff --git a/packages/client/src/components/MkMediaList.vue b/packages/client/src/components/MkMediaList.vue index b7d98a00bb..ed3ccbeff8 100644 --- a/packages/client/src/components/MkMediaList.vue +++ b/packages/client/src/components/MkMediaList.vue @@ -17,17 +17,15 @@ previewable(media) )" > - <XVideo - v-if="media.type.startsWith('video')" + <XMedia + v-if=" + media.type.startsWith('video') || + media.type.startsWith('image') + " :key="media.id" - :video="media" - /> - <XImage - v-else-if="media.type.startsWith('image')" - :key="media.id" - class="image" + :class="{ image: media.type.startsWith('image') }" :data-id="media.id" - :image="media" + :media="media" :raw="raw" /> <XModPlayer @@ -48,8 +46,7 @@ import PhotoSwipeLightbox from "photoswipe/lightbox"; import PhotoSwipe from "photoswipe"; import "photoswipe/style.css"; import XBanner from "@/components/MkMediaBanner.vue"; -import XImage from "@/components/MkMediaImage.vue"; -import XVideo from "@/components/MkMediaVideo.vue"; +import XMedia from "@/components/MkMedia.vue"; import XModPlayer from "@/components/MkModPlayer.vue"; import * as os from "@/os"; import { diff --git a/packages/client/src/components/MkMediaVideo.vue b/packages/client/src/components/MkMediaVideo.vue deleted file mode 100644 index 53dc6a8ab8..0000000000 --- a/packages/client/src/components/MkMediaVideo.vue +++ /dev/null @@ -1,169 +0,0 @@ -<template> - <div - v-if="hide" - class="icozogqfvdetwohsdglrbswgrejoxbdj" - @click="hide = false" - > - <div> - <b - ><i class="ph-warning ph-bold ph-lg"></i> - {{ i18n.ts.sensitive }}</b - > - <span>{{ i18n.ts.clickToShow }}</span> - </div> - </div> - <div v-else class="video" :class="{ mini }"> - <VuePlyr - ref="plyr" - :options="{ - controls: [ - 'play-large', - 'play', - 'progress', - 'current-time', - 'mute', - 'volume', - 'pip', - 'download', - 'fullscreen', - ], - disableContextMenu: false, - }" - > - <video - :poster="video.thumbnailUrl" - :aria-label="video.comment" - preload="none" - controls - @contextmenu.stop - > - <source :src="video.url" :type="video.type" /> - </video> - </VuePlyr> - <div class="buttons"> - <button - v-if="video.comment" - v-tooltip="i18n.ts.alt" - class="_button" - @click.stop="captionPopup" - > - <i class="ph-subtitles ph-bold ph-lg"></i> - </button> - <button - v-tooltip="i18n.ts.hide" - class="_button" - @click="hide = true" - > - <i class="ph-eye-slash ph-bold ph-lg"></i> - </button> - </div> - </div> -</template> - -<script lang="ts" setup> -import { onMounted, ref } from "vue"; -import VuePlyr from "vue-plyr"; -import type * as misskey from "calckey-js"; -import { defaultStore } from "@/store"; -import "vue-plyr/dist/vue-plyr.css"; -import { i18n } from "@/i18n"; -import * as os from "@/os"; - -const props = defineProps<{ - video: misskey.entities.DriveFile; -}>(); - -const plyr = ref(); -const mini = ref(false); - -const hide = ref( - defaultStore.state.nsfw === "force" - ? true - : props.video.isSensitive && defaultStore.state.nsfw !== "ignore" -); - -function captionPopup() { - os.alert({ - type: "info", - text: props.video.comment, - }); -} - -onMounted(() => { - mini.value = plyr.value.player.media.scrollWidth < 300; - if (mini.value) { - plyr.value.player.on("play", () => { - plyr.value.player.fullscreen.enter(); - }); - } -}); -</script> - -<style lang="scss" scoped> -.video { - position: relative; - --plyr-color-main: var(--accent); - - > .buttons { - display: flex; - gap: 4px; - position: absolute; - border-radius: 6px; - overflow: hidden; - top: 12px; - right: 12px; - > * { - background-color: var(--accentedBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - color: var(--accent); - font-size: 0.8em; - padding: 6px 8px; - text-align: center; - } - } - - > video { - display: flex; - justify-content: center; - align-items: center; - - font-size: 3.5em; - overflow: hidden; - background-position: center; - background-size: cover; - width: 100%; - height: 100%; - } - - &.mini { - :deep(.plyr:not(:fullscreen)) { - min-width: unset !important; - .plyr__control--overlaid, - .plyr__progress__container, - .plyr__volume, - [data-plyr="fullscreen"] { - display: none; - } - } - } -} - -.icozogqfvdetwohsdglrbswgrejoxbdj { - display: flex; - justify-content: center; - align-items: center; - background: #111; - color: #fff; - - > div { - display: table-cell; - text-align: center; - font-size: 12px; - - > b { - display: block; - } - } -} -</style> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1002a6f958..560bb55a37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: '@koa/router': specifier: 9.0.1 version: 9.0.1 + '@msgpack/msgpack': + specifier: 3.0.0-beta2 + version: 3.0.0-beta2 '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 @@ -132,9 +135,6 @@ importers: argon2: specifier: ^0.30.3 version: 0.30.3 - async-mutex: - specifier: ^0.4.0 - version: 0.4.0 autobind-decorator: specifier: 2.4.0 version: 2.4.0 @@ -786,7 +786,7 @@ importers: version: 2.30.0 emojilib: specifier: github:thatonecalculator/emojilib - version: github.com/thatonecalculator/emojilib/542fcc1a25003afad78f3248ceee8ac6980ddeb8 + version: github.com/thatonecalculator/emojilib/06944984a61ee799b7083894258f5fa318d932d1 escape-regexp: specifier: 0.0.1 version: 0.0.1 @@ -2277,6 +2277,11 @@ packages: os-filter-obj: 2.0.0 dev: true + /@msgpack/msgpack@3.0.0-beta2: + resolution: {integrity: sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==} + engines: {node: '>= 14'} + dev: false + /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2: resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==} cpu: [arm64] @@ -4496,12 +4501,6 @@ packages: stream-exhaust: 1.0.2 dev: true - /async-mutex@0.4.0: - resolution: {integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==} - dependencies: - tslib: 2.6.0 - dev: false - /async-settle@1.0.0: resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==} engines: {node: '>= 0.10'} @@ -15772,8 +15771,8 @@ packages: url-polyfill: 1.1.12 dev: true - github.com/thatonecalculator/emojilib/542fcc1a25003afad78f3248ceee8ac6980ddeb8: - resolution: {tarball: https://codeload.github.com/thatonecalculator/emojilib/tar.gz/542fcc1a25003afad78f3248ceee8ac6980ddeb8} + github.com/thatonecalculator/emojilib/06944984a61ee799b7083894258f5fa318d932d1: + resolution: {tarball: https://codeload.github.com/thatonecalculator/emojilib/tar.gz/06944984a61ee799b7083894258f5fa318d932d1} name: emojilib version: 3.0.10 dev: true