Compare commits
35 Commits
13.0.0-bet
...
13.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f20d7cba74 | ||
![]() |
a3e282bc75 | ||
![]() |
49a95c34bf | ||
![]() |
ecbefce2aa | ||
![]() |
91356b1805 | ||
![]() |
2e2ed1385f | ||
![]() |
49f3090edd | ||
![]() |
4594fb11de | ||
![]() |
b93e56d2e5 | ||
![]() |
c550dafb81 | ||
![]() |
8709574f3d | ||
![]() |
1b7043fa79 | ||
![]() |
55ef2393fb | ||
![]() |
7769095efb | ||
![]() |
b8248bdd65 | ||
![]() |
6f4ad581dc | ||
![]() |
aec94920ab | ||
![]() |
155ca39063 | ||
![]() |
58bfb4dca4 | ||
![]() |
49a0b6c48b | ||
![]() |
799a653b44 | ||
![]() |
d09e1f4925 | ||
![]() |
cac784af8a | ||
![]() |
d7e0ddcbca | ||
![]() |
8c0811a442 | ||
![]() |
bab6f75260 | ||
![]() |
54e3fccd87 | ||
![]() |
6a992b6982 | ||
![]() |
ecd6fc1db8 | ||
![]() |
d99be6697e | ||
![]() |
d2d77b5dc1 | ||
![]() |
91503405b4 | ||
![]() |
c336201084 | ||
![]() |
0f3399753d | ||
![]() |
5ec89ea0c3 |
@@ -30,11 +30,12 @@ You should also include the user name that made the change.
|
||||
|
||||
#### For users
|
||||
- ノートのウォッチ機能が削除されました
|
||||
- アンケートに投票された際に通知が作成されなくなりました
|
||||
- 新たに動的なPagesを作ることはできなくなりました
|
||||
- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。
|
||||
- AiScriptが0.12.1にアップデートされました
|
||||
- AiScriptが0.12.2にアップデートされました
|
||||
- 0.12.xの変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
|
||||
- 0.12.1未満のプラグインは読み込むことはできません
|
||||
- 0.12.x未満のプラグインは読み込むことはできません
|
||||
- iOS15以下のデバイスはサポートされなくなりました
|
||||
- Firefox109以下はサポートされなくなりました
|
||||
|
||||
@@ -77,6 +78,7 @@ You should also include the user name that made the change.
|
||||
- Client: Improve RSS widget @tamaina
|
||||
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
|
||||
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
|
||||
- Client: Support remote objects in search @SoniEx2
|
||||
- Client: add user list widget @syuilo
|
||||
- Client: add heatmap of daily active users to about page @syuilo
|
||||
- Client: introduce fluent emoji @syuilo
|
||||
@@ -98,6 +100,7 @@ You should also include the user name that made the change.
|
||||
- Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
|
||||
- Client: use proxied image for instance icon @syuilo
|
||||
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
|
||||
- Client: Page編集でブロックの移動が行えない問題を修正 @syuilo
|
||||
- Client: update emoji picker immediately on all input @saschanaz
|
||||
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
|
||||
- Client: fix wrong link in tutorial @syuilo
|
||||
|
@@ -920,6 +920,10 @@ like: "Gefällt mir"
|
||||
unlike: "\"Gefällt mir\" entfernen"
|
||||
numberOfLikes: "\"Gefällt mir\"-Anzahl"
|
||||
show: "Anzeigen"
|
||||
neverShow: "Nicht wieder anzeigen"
|
||||
remindMeLater: "Vielleicht später"
|
||||
didYouLikeMisskey: "Gefällt dir Misskey?"
|
||||
pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
||||
sensitivity: "Erkennungssensitivität"
|
||||
|
@@ -920,6 +920,10 @@ like: "Like"
|
||||
unlike: "Unlike"
|
||||
numberOfLikes: "Likes"
|
||||
show: "Show"
|
||||
neverShow: "Don't show again"
|
||||
remindMeLater: "Maybe later"
|
||||
didYouLikeMisskey: "Have you taken a liking to Misskey?"
|
||||
pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||
sensitivity: "Detection sensitivity"
|
||||
|
@@ -28,7 +28,7 @@ timeline: "Timeline"
|
||||
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo."
|
||||
login: "Accedi"
|
||||
loggingIn: "Accesso in corso..."
|
||||
logout: "Esci"
|
||||
logout: "Uscita"
|
||||
signup: "Iscriviti"
|
||||
uploading: "Caricamento..."
|
||||
save: "Salva"
|
||||
|
@@ -920,6 +920,10 @@ like: "いいね!"
|
||||
unlike: "いいねを解除"
|
||||
numberOfLikes: "いいね数"
|
||||
show: "表示"
|
||||
neverShow: "今後表示しない"
|
||||
remindMeLater: "また後で"
|
||||
didYouLikeMisskey: "Misskeyを気に入っていただけましたか?"
|
||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||
@@ -1546,7 +1550,6 @@ _notification:
|
||||
youGotReply: "{name}からのリプライ"
|
||||
youGotQuote: "{name}による引用"
|
||||
youRenoted: "{name}がRenoteしました"
|
||||
youGotPoll: "{name}が投票しました"
|
||||
youGotMessagingMessageFromUser: "{name}からのチャットがあります"
|
||||
youGotMessagingMessageFromGroup: "{name}のチャットがあります"
|
||||
youWereFollowed: "フォローされました"
|
||||
@@ -1565,7 +1568,6 @@ _notification:
|
||||
renote: "Renote"
|
||||
quote: "引用"
|
||||
reaction: "リアクション"
|
||||
pollVote: "アンケートに投票された"
|
||||
pollEnded: "アンケートが終了"
|
||||
receiveFollowRequest: "フォロー申請を受け取った"
|
||||
followRequestAccepted: "フォローが受理された"
|
||||
|
@@ -920,6 +920,10 @@ like: "좋아요!"
|
||||
unlike: "좋아요 취소"
|
||||
numberOfLikes: "좋아요 수"
|
||||
show: "표시"
|
||||
neverShow: "다시 보지 않기"
|
||||
remindMeLater: "나중에 알림"
|
||||
didYouLikeMisskey: "Misskey가 마음에 드시나요?"
|
||||
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
||||
sensitivity: "탐지 민감도"
|
||||
|
@@ -913,6 +913,10 @@ tools: "Nástroje"
|
||||
cannotLoad: "Nedá sa načítať."
|
||||
like: "Páči sa mi"
|
||||
show: "Zobraziť"
|
||||
neverShow: "Nabudúce nezobrazovať"
|
||||
remindMeLater: "Pripomenúť neskôr"
|
||||
didYouLikeMisskey: "Páči sa vám Misskey?"
|
||||
pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, prispejte, aby sme ho mohli ďalej rozvíjať!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
|
||||
sensitivity: "Citlivosť detekcie"
|
||||
|
@@ -917,6 +917,8 @@ tools: "เครื่องมือ"
|
||||
cannotLoad: "ไม่สามารถโหลดได้"
|
||||
numberOfProfileView: "มุมมองโปรไฟล์"
|
||||
like: "ชื่นชอบ"
|
||||
unlike: "ไม่ชอบ"
|
||||
numberOfLikes: "จำนวนไลค์"
|
||||
show: "แสดงผล"
|
||||
_sensitiveMediaDetection:
|
||||
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
|
||||
@@ -1317,6 +1319,7 @@ _widgets:
|
||||
jobQueue: "คิวงาน"
|
||||
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
|
||||
aiscript: "AiScript คอนโซล"
|
||||
aiscriptApp: "AiScript แอพ"
|
||||
aichan: "เอไอ"
|
||||
userList: "รายชื่อผู้ใช้"
|
||||
_userList:
|
||||
@@ -1423,7 +1426,16 @@ _timelines:
|
||||
social: "โซเชี่ยล"
|
||||
global: "ทั่วโลก"
|
||||
_play:
|
||||
new: "สร้างการเล่น"
|
||||
edit: "แก้ไขเล่น"
|
||||
created: "สร้างการเล่นแล้ว"
|
||||
updated: "แก้ไขการเล่นแล้ว"
|
||||
deleted: "ลบการเล่นแล้ว"
|
||||
pageSetting: "ตั้งค่าการเล่น"
|
||||
editThisPage: "แก้ไข Play นี้"
|
||||
viewSource: "ดูต้นฉบับ"
|
||||
my: "มาย เพลย์"
|
||||
liked: "ไลค์ เพลย์"
|
||||
featured: "เป็นที่นิยม"
|
||||
title: "หัวข้อ"
|
||||
script: "สคริปต์"
|
||||
|
@@ -918,7 +918,11 @@ cannotLoad: "無法載入"
|
||||
numberOfProfileView: "個人檔案檢視次數"
|
||||
like: "讚"
|
||||
unlike: "收回讚"
|
||||
numberOfLikes: "讚數"
|
||||
show: "檢視"
|
||||
neverShow: "不再顯示"
|
||||
remindMeLater: "以後再說"
|
||||
didYouLikeMisskey: "您是否喜愛Misskey呢?"
|
||||
_sensitiveMediaDetection:
|
||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
||||
sensitivity: "檢測敏感度"
|
||||
@@ -1318,6 +1322,7 @@ _widgets:
|
||||
jobQueue: "佇列"
|
||||
serverMetric: "服務器指標 "
|
||||
aiscript: "AiScript控制台"
|
||||
aiscriptApp: "AiScript App"
|
||||
aichan: "小藍"
|
||||
userList: "使用者列表"
|
||||
_userList:
|
||||
@@ -1424,7 +1429,16 @@ _timelines:
|
||||
social: "社群"
|
||||
global: "公開"
|
||||
_play:
|
||||
new: "新增Play"
|
||||
edit: "編輯Play"
|
||||
created: "已新增Play"
|
||||
updated: "已更新Play"
|
||||
deleted: "已刪除Play"
|
||||
pageSetting: "Play設定"
|
||||
editThisPage: "編輯這個Play"
|
||||
viewSource: "檢視原始碼"
|
||||
my: "自己的Play"
|
||||
liked: "按了讚的Play"
|
||||
featured: "人氣"
|
||||
title: "標題"
|
||||
script: "腳本"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.0.0-beta.26",
|
||||
"version": "13.0.0-beta.28",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
BIN
packages/backend/assets/emoji-unknown.png
Normal file
BIN
packages/backend/assets/emoji-unknown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
@@ -92,13 +92,6 @@ export class PollService {
|
||||
choice: choice,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// Notify
|
||||
this.createNotificationService.createNotification(note.userId, 'pollVote', {
|
||||
notifierId: user.id,
|
||||
noteId: note.id,
|
||||
choice: choice,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -98,7 +98,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
}),
|
||||
reaction: notification.reaction,
|
||||
} : {}),
|
||||
...(notification.type === 'pollVote' ? {
|
||||
...(notification.type === 'pollVote' ? { // TODO: そのうち消す
|
||||
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
|
@@ -3,6 +3,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
const dictionary = {
|
||||
'safe-file': FILE_TYPE_BROWSERSAFE,
|
||||
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
|
||||
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
|
||||
};
|
||||
|
||||
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
||||
|
@@ -55,11 +55,11 @@ export class Notification {
|
||||
* 通知の種類。
|
||||
* follow - フォローされた
|
||||
* mention - 投稿で自分が言及された
|
||||
* reply - (自分または自分がWatchしている)投稿が返信された
|
||||
* renote - (自分または自分がWatchしている)投稿がRenoteされた
|
||||
* quote - (自分または自分がWatchしている)投稿が引用Renoteされた
|
||||
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
|
||||
* pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された
|
||||
* reply - 投稿に返信された
|
||||
* renote - 投稿がRenoteされた
|
||||
* quote - 投稿が引用Renoteされた
|
||||
* reaction - 投稿にリアクションされた
|
||||
* pollVote - 投稿のアンケートに投票された (廃止)
|
||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||
* receiveFollowRequest - フォローリクエストされた
|
||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||
|
@@ -79,10 +79,18 @@ export class MediaProxyServerService {
|
||||
|
||||
const { mime, ext } = await this.fileInfoService.detectType(path);
|
||||
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
|
||||
const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
|
||||
|
||||
let image: IImage;
|
||||
if ('emoji' in request.query && isConvertibleImage) {
|
||||
const data = await sharp(path, { animated: !('static' in request.query) })
|
||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||
image = {
|
||||
data: fs.readFileSync(path),
|
||||
ext,
|
||||
type: mime,
|
||||
};
|
||||
} else {
|
||||
const data = await sharp(path, { animated: !('static' in request.query) })
|
||||
.resize({
|
||||
height: 128,
|
||||
withoutEnlargement: true,
|
||||
@@ -90,11 +98,12 @@ export class MediaProxyServerService {
|
||||
.webp(webpDefault)
|
||||
.toBuffer();
|
||||
|
||||
image = {
|
||||
data,
|
||||
ext: 'webp',
|
||||
type: 'image/webp',
|
||||
};
|
||||
image = {
|
||||
data,
|
||||
ext: 'webp',
|
||||
type: 'image/webp',
|
||||
};
|
||||
}
|
||||
} else if ('static' in request.query && isConvertibleImage) {
|
||||
image = await this.imageProcessingService.convertToWebp(path, 498, 280);
|
||||
} else if ('preview' in request.query && isConvertibleImage) {
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import cluster from 'node:cluster';
|
||||
import * as fs from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Fastify from 'fastify';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { envOption } from '@/env.js';
|
||||
@@ -39,6 +38,9 @@ export class ServerService {
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private apiServerService: ApiServerService,
|
||||
private streamingApiServerService: StreamingApiServerService,
|
||||
@@ -77,6 +79,43 @@ export class ServerService {
|
||||
fastify.register(this.nodeinfoServerService.createServer);
|
||||
fastify.register(this.wellKnownServerService.createServer);
|
||||
|
||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||
const path = request.params.path;
|
||||
|
||||
if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
const name = path.split('@')[0].replace('.webp', '');
|
||||
const host = path.split('@')[1]?.replace('.webp', '');
|
||||
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
// `@.` is the spec of ReactionService.decodeReaction
|
||||
host: (host == null || host === '.') ? IsNull() : host,
|
||||
name: name,
|
||||
});
|
||||
|
||||
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
||||
|
||||
if (emoji == null) {
|
||||
return await reply.redirect('/static-assets/emoji-unknown.png');
|
||||
}
|
||||
|
||||
const url = new URL('/proxy/emoji.webp', this.config.url);
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
||||
url.searchParams.set('emoji', '1');
|
||||
if ('static' in request.query) url.searchParams.set('static', '1');
|
||||
|
||||
return await reply.redirect(
|
||||
301,
|
||||
url.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { acct: string } }>('/avatar/@:acct', async (request, reply) => {
|
||||
const { username, host } = Acct.parse(request.params.acct);
|
||||
const user = await this.usersRepository.findOne({
|
||||
|
@@ -162,13 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
// Notify
|
||||
this.createNotificationService.createNotification(note.userId, 'pollVote', {
|
||||
notifierId: me.id,
|
||||
noteId: note.id,
|
||||
choice: ps.choice,
|
||||
});
|
||||
|
||||
// リモート投票の場合リプライ送信
|
||||
if (note.userHost != null) {
|
||||
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser;
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { PathOrFileDescriptor, readFileSync } from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { createBullBoard } from '@bull-board/api';
|
||||
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
|
||||
@@ -71,9 +70,6 @@ export class ClientServerService {
|
||||
@Inject(DI.pagesRepository)
|
||||
private pagesRepository: PagesRepository,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@@ -225,49 +221,6 @@ export class ClientServerService {
|
||||
return reply.sendFile('/apple-touch-icon.png', staticAssets);
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||
const path = request.params.path;
|
||||
|
||||
if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
const name = path.split('@')[0].replace('.webp', '');
|
||||
const host = path.split('@')[1]?.replace('.webp', '');
|
||||
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
// `@.` is the spec of ReactionService.decodeReaction
|
||||
host: (host == null || host === '.') ? IsNull() : host,
|
||||
name: name,
|
||||
});
|
||||
|
||||
if (emoji == null) {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
||||
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
return await reply.redirect(301, emoji.publicUrl || emoji.originalUrl);
|
||||
|
||||
/* https://github.com/misskey-dev/misskey/pull/9431#issuecomment-1373006446
|
||||
const url = new URL('/proxy/emoji.webp', this.config.url);
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
||||
url.searchParams.set('emoji', '1');
|
||||
if ('static' in request.query) url.searchParams.set('static', '1');
|
||||
|
||||
return await reply.redirect(
|
||||
301,
|
||||
url.toString(),
|
||||
);
|
||||
*/
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => {
|
||||
const path = request.params.path;
|
||||
|
||||
@@ -362,7 +315,7 @@ export class ClientServerService {
|
||||
const name = meta.name || 'Misskey';
|
||||
let content = '';
|
||||
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
|
||||
content += `<ShortName>${name} Search</ShortName>`;
|
||||
content += `<ShortName>${name}</ShortName>`;
|
||||
content += `<Description>${name} Search</Description>`;
|
||||
content += '<InputEncoding>UTF-8</InputEncoding>';
|
||||
content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`;
|
||||
|
@@ -31,7 +31,7 @@ html
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
|
||||
link(rel='manifest' href='/manifest.json')
|
||||
link(rel='search' type='application/opensearchdescription+xml' title=((title || "Misskey") + " Search") href=`${url}/opensearch.xml`)
|
||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"@rollup/plugin-alias": "4.0.2",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.12.1",
|
||||
"@syuilo/aiscript": "0.12.2",
|
||||
"@tabler/icons": "^1.118.0",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/compiler-sfc": "3.2.45",
|
||||
|
@@ -6,12 +6,13 @@ import { del, get, set } from '@/scripts/idb-proxy';
|
||||
import { apiUrl } from '@/config';
|
||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
|
||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
type Account = misskey.entities.MeDetailed;
|
||||
|
||||
const accountData = localStorage.getItem('account');
|
||||
const accountData = miLocalStorage.getItem('account');
|
||||
|
||||
// TODO: 外部からはreadonlyに
|
||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||
@@ -21,7 +22,7 @@ export const iAmAdmin = $i != null && $i.isAdmin;
|
||||
|
||||
export async function signout() {
|
||||
waiting();
|
||||
localStorage.removeItem('account');
|
||||
miLocalStorage.removeItem('account');
|
||||
|
||||
await removeAccount($i.id);
|
||||
|
||||
@@ -119,7 +120,7 @@ export function updateAccount(accountData) {
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
localStorage.setItem('account', JSON.stringify($i));
|
||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||
}
|
||||
|
||||
export function refreshAccount() {
|
||||
@@ -130,7 +131,7 @@ export async function login(token: Account['token'], redirect?: string) {
|
||||
waiting();
|
||||
if (_DEV_) console.log('logging as token ', token);
|
||||
const me = await fetchAccount(token);
|
||||
localStorage.setItem('account', JSON.stringify(me));
|
||||
miLocalStorage.setItem('account', JSON.stringify(me));
|
||||
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
|
||||
await addAccount(me.id, token);
|
||||
|
||||
|
@@ -37,7 +37,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import { acct, userPage } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
|
@@ -28,7 +28,7 @@
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@@ -51,10 +51,10 @@
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
||||
import FormFolder from '@/components/form/folder.vue';
|
||||
|
||||
|
@@ -46,6 +46,7 @@ import { defaultStore } from '@/store';
|
||||
import { emojilist } from '@/scripts/emojilist';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
@@ -208,7 +209,7 @@ function exec() {
|
||||
}
|
||||
} else if (props.type === 'hashtag') {
|
||||
if (!props.q || props.q === '') {
|
||||
hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
|
||||
hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
|
||||
fetching.value = false;
|
||||
} else {
|
||||
const cacheKey = `autocomplete:hashtag:${props.q}`;
|
||||
|
@@ -74,7 +74,7 @@ function onMousedown(evt: Event) {
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
|
@@ -42,8 +42,8 @@
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
type Input = {
|
||||
|
109
packages/frontend/src/components/MkDonation.vue
Normal file
109
packages/frontend/src/components/MkDonation.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="_panel _shadow" :class="$style.root">
|
||||
<!-- TODO: インスタンス運営者が任意のテキストとリンクを設定できるようにする -->
|
||||
<div :class="$style.icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pig-money" width="40" height="40" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M15 11v.01"></path>
|
||||
<path d="M5.173 8.378a3 3 0 1 1 4.656 -1.377"></path>
|
||||
<path d="M16 4v3.803a6.019 6.019 0 0 1 2.658 3.197h1.341a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-1.342c-.336 .95 -.907 1.8 -1.658 2.473v2.027a1.5 1.5 0 0 1 -3 0v-.583a6.04 6.04 0 0 1 -1 .083h-4a6.04 6.04 0 0 1 -1 -.083v.583a1.5 1.5 0 0 1 -3 0v-2l.001 -.027a6 6 0 0 1 3.999 -10.473h2.5l4.5 -3h.001z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div :class="$style.main">
|
||||
<div :class="$style.title">{{ i18n.ts.didYouLikeMisskey }}</div>
|
||||
<div :class="$style.text">
|
||||
<I18n :src="i18n.ts.pleaseDonate" tag="span">
|
||||
<template #host>
|
||||
{{ $instance.name ?? host }}
|
||||
</template>
|
||||
</I18n>
|
||||
<div style="margin-top: 0.2em;">
|
||||
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate.html">{{ i18n.ts.learnMore }}</MkLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_buttons">
|
||||
<MkButton @click="close">{{ i18n.ts.remindMeLater }}</MkButton>
|
||||
<MkButton @click="neverShow">{{ i18n.ts.neverShow }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import { host } from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const zIndex = os.claimZIndex('low');
|
||||
|
||||
function close() {
|
||||
miLocalStorage.setItem('latestDonationInfoShownAt', Date.now().toString());
|
||||
emit('closed');
|
||||
}
|
||||
|
||||
function neverShow() {
|
||||
miLocalStorage.setItem('neverShowDonationInfo', 'true')
|
||||
close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: fixed;
|
||||
z-index: v-bind(zIndex);
|
||||
bottom: var(--margin);
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - (var(--margin) * 2));
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon {
|
||||
text-align: center;
|
||||
padding-top: 25px;
|
||||
width: 100px;
|
||||
color: var(--accent);
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.icon {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
.icon {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 25px 25px 25px 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0.7em 0 1em 0;
|
||||
}
|
||||
</style>
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||
<input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()">
|
||||
<div ref="emojis" class="emojis">
|
||||
<section class="result">
|
||||
@@ -94,6 +94,7 @@ const props = withDefaults(defineProps<{
|
||||
asReactionPicker?: boolean;
|
||||
maxHeight?: number;
|
||||
asDrawer?: boolean;
|
||||
asWindow?: boolean;
|
||||
}>(), {
|
||||
showPinned: true,
|
||||
});
|
||||
@@ -440,6 +441,28 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
&.asWindow {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
|
||||
> .emojis {
|
||||
::v-deep(section) {
|
||||
> .body {
|
||||
display: grid;
|
||||
grid-template-columns: var(--columns);
|
||||
font-size: 30px;
|
||||
|
||||
> .item {
|
||||
aspect-ratio: 1 / 1;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .search {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<MkWindow ref="window"
|
||||
:initial-width="null"
|
||||
:initial-height="null"
|
||||
:can-resize="false"
|
||||
:initial-width="300"
|
||||
:initial-height="290"
|
||||
:can-resize="true"
|
||||
:mini="true"
|
||||
:front="true"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
|
||||
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" as-window @chosen="chosen" :class="$style.picker"/>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
@@ -34,147 +34,8 @@ function chosen(emoji: any) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.omfetrab {
|
||||
$pad: 8px;
|
||||
--eachSize: 40px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
contain: content;
|
||||
|
||||
&.big {
|
||||
--eachSize: 44px;
|
||||
}
|
||||
|
||||
&.w1 {
|
||||
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
||||
}
|
||||
|
||||
&.w2 {
|
||||
width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
|
||||
}
|
||||
|
||||
&.w3 {
|
||||
width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
|
||||
}
|
||||
|
||||
&.h1 {
|
||||
--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
|
||||
}
|
||||
|
||||
&.h2 {
|
||||
--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
|
||||
}
|
||||
|
||||
&.h3 {
|
||||
--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
|
||||
}
|
||||
|
||||
> .search {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
font-size: 1em;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
|
||||
&:not(.filled) {
|
||||
order: 1;
|
||||
z-index: 2;
|
||||
box-shadow: 0px -1px 0 0px var(--divider);
|
||||
}
|
||||
}
|
||||
|
||||
> .emojis {
|
||||
height: var(--height);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .index {
|
||||
min-height: var(--height);
|
||||
position: relative;
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
|
||||
> .arrow {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
> header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
> div {
|
||||
padding: $pad;
|
||||
|
||||
> button {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
width: var(--eachSize);
|
||||
height: var(--eachSize);
|
||||
border-radius: 4px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: solid 2px var(--focus);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accent);
|
||||
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
|
||||
}
|
||||
|
||||
> * {
|
||||
font-size: 24px;
|
||||
height: 1.25em;
|
||||
vertical-align: -.25em;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.result {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.unicode {
|
||||
min-height: 384px;
|
||||
}
|
||||
|
||||
&.custom {
|
||||
min-height: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
<style lang="scss" module>
|
||||
.picker {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@@ -23,7 +23,7 @@
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
|
@@ -25,8 +25,9 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
const localStoragePrefix = 'ui:folder:';
|
||||
const miLocalStoragePrefix = 'ui:folder:' as const;
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -44,13 +45,13 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
bg: null,
|
||||
showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
|
||||
showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
showBody() {
|
||||
if (this.persistKey) {
|
||||
localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
|
||||
miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@@ -36,7 +36,7 @@
|
||||
import { } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import * as os from '@/os';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@@ -17,34 +17,34 @@
|
||||
<MkSpacer :margin-min="20" :margin-max="32">
|
||||
<div class="xkpnjxcv _gaps_m">
|
||||
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
||||
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
||||
<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
|
||||
</MkInput>
|
||||
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormInput>
|
||||
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
|
||||
</MkInput>
|
||||
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormTextarea>
|
||||
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
|
||||
</MkTextarea>
|
||||
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
|
||||
<span v-text="form[item].label || item"></span>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormSwitch>
|
||||
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
||||
</MkSwitch>
|
||||
<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</FormSelect>
|
||||
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
|
||||
</MkSelect>
|
||||
<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</FormRadios>
|
||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
|
||||
</MkRadios>
|
||||
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormRange>
|
||||
</MkRange>
|
||||
<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
|
||||
<span v-text="form[item].content || item"></span>
|
||||
</MkButton>
|
||||
@@ -56,25 +56,25 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormInput from './form/input.vue';
|
||||
import FormTextarea from './form/textarea.vue';
|
||||
import FormSwitch from './form/switch.vue';
|
||||
import FormSelect from './form/select.vue';
|
||||
import FormRange from './form/range.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkTextarea from './MkTextarea.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkSelect from './MkSelect.vue';
|
||||
import MkRange from './MkRange.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import FormRadios from './form/radios.vue';
|
||||
import MkRadios from './MkRadios.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModalWindow,
|
||||
FormInput,
|
||||
FormTextarea,
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormRange,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSwitch,
|
||||
MkSelect,
|
||||
MkRange,
|
||||
MkButton,
|
||||
FormRadios,
|
||||
MkRadios,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@@ -78,7 +78,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
import * as os from '@/os';
|
||||
|
@@ -31,7 +31,7 @@
|
||||
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></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">{{ item.text }}</FormSwitch>
|
||||
<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
|
||||
</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="ti-fw" :class="item.icon"></i>
|
||||
@@ -58,7 +58,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
@@ -251,17 +251,18 @@ onBeforeUnmount(() => {
|
||||
color: #fff;
|
||||
|
||||
&:before {
|
||||
background: #d42e2e;
|
||||
background: #d42e2e !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
color: var(--fgOnAccent) !important;
|
||||
opacity: 1;
|
||||
|
||||
&:before {
|
||||
background: var(--accent);
|
||||
background: var(--accent) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
|
||||
<slot :max-height="maxHeight" :type="type"></slot>
|
||||
</div>
|
||||
@@ -74,8 +74,24 @@ const type = $computed<ModalTypes>(() => {
|
||||
return props.preferType!;
|
||||
}
|
||||
});
|
||||
let transitionName = $ref(defaultStore.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : '');
|
||||
let transitionDuration = $ref(defaultStore.state.animation ? 200 : 0);
|
||||
let transitionName = $computed((() =>
|
||||
defaultStore.state.animation
|
||||
? (type === 'drawer')
|
||||
? 'modal-drawer'
|
||||
: (type === 'popup')
|
||||
? 'modal-popup'
|
||||
: 'modal'
|
||||
: ''
|
||||
));
|
||||
let transitionDuration = $computed((() =>
|
||||
transitionName === 'modal-popup'
|
||||
? 100
|
||||
: transitionName === 'modal'
|
||||
? 200
|
||||
: transitionName === 'modal-drawer'
|
||||
? 200
|
||||
: 0
|
||||
));
|
||||
|
||||
let contentClicking = false;
|
||||
|
||||
@@ -267,9 +283,8 @@ defineExpose({
|
||||
}
|
||||
|
||||
> .content {
|
||||
transform-style: preserve-3d;
|
||||
transform: perspective(50cm) translateZ(0px) translateY(0px) rotateX(0deg);
|
||||
transition: opacity 0.4s cubic-bezier(.5,-0.5,.75,1), transform 0.4s cubic-bezier(.5,-0.5,.75,1) !important;
|
||||
transform: translateY(0px);
|
||||
transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
|
||||
}
|
||||
}
|
||||
.send-enter-from, .send-leave-to {
|
||||
@@ -280,8 +295,7 @@ defineExpose({
|
||||
> .content {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform-style: preserve-3d;
|
||||
transform: perspective(50cm) translateZ(-300px) translateY(-200px) rotateX(40deg);
|
||||
transform: translateY(-300px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,12 +324,12 @@ defineExpose({
|
||||
|
||||
.modal-popup-enter-active, .modal-popup-leave-active {
|
||||
> .bg {
|
||||
transition: opacity 0.2s !important;
|
||||
transition: opacity 0.1s !important;
|
||||
}
|
||||
|
||||
> .content {
|
||||
transform-origin: var(--transformOrigin);
|
||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
|
||||
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
|
||||
}
|
||||
}
|
||||
.modal-popup-enter-from, .modal-popup-leave-to {
|
||||
|
@@ -13,7 +13,7 @@
|
||||
<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>
|
||||
<div v-if="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>
|
||||
<div v-if="isRenote" class="renote">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<MkAvatar v-once class="avatar" :user="note.user"/>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
<template #user>
|
||||
@@ -27,11 +27,16 @@
|
||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<MkVisibility :note="note"/>
|
||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
||||
<MkAvatar v-once class="avatar" :user="appearNote.user"/>
|
||||
<div class="main">
|
||||
<MkNoteHeader class="header" :note="appearNote" :mini="true"/>
|
||||
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
|
||||
@@ -44,7 +49,7 @@
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/>
|
||||
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i"/>
|
||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
@@ -75,14 +80,25 @@
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
class="button _button"
|
||||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button v-else class="button _button" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
|
||||
<i class="ti ti-plus"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||
<i class="ti ti-minus"></i>
|
||||
</button>
|
||||
<button ref="menuButton" class="button _button" @click="menu()">
|
||||
<button ref="menuButton" class="button _button" @mousedown="menu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
@@ -111,10 +127,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkRenoteButton from '@/components/MkRenoteButton.vue';
|
||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import MkVisibility from '@/components/MkVisibility.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||
@@ -128,6 +143,7 @@ import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@@ -158,7 +174,7 @@ const isRenote = (
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
|
||||
const renoteButton = shallowRef<HTMLElement>();
|
||||
const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||
@@ -175,6 +191,7 @@ const translation = ref(null);
|
||||
const translating = ref(false);
|
||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
@@ -193,6 +210,47 @@ useNoteCapture({
|
||||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
useTooltip(renoteButton, async (showing) => {
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: appearNote.id,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
const users = renotes.map(x => x.user);
|
||||
|
||||
if (users.length < 1) return;
|
||||
|
||||
os.popup(MkUsersTooltip, {
|
||||
showing,
|
||||
users,
|
||||
count: appearNote.renoteCount,
|
||||
targetElement: renoteButton.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.quote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}], renoteButton.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
}
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
os.post({
|
||||
|
@@ -25,7 +25,12 @@
|
||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<MkVisibility :note="note"/>
|
||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
@@ -38,7 +43,12 @@
|
||||
</MkA>
|
||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
||||
<div class="info">
|
||||
<MkVisibility :note="appearNote"/>
|
||||
<span v-if="appearNote.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[appearNote.visibility]">
|
||||
<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="appearNote.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
||||
@@ -85,14 +95,25 @@
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
class="button _button"
|
||||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button v-else class="button _button" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
|
||||
<i class="ti ti-plus"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||
<i class="ti ti-minus"></i>
|
||||
</button>
|
||||
<button ref="menuButton" class="button _button" @click="menu()">
|
||||
<button ref="menuButton" class="button _button" @mousedown="menu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
@@ -121,10 +142,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkRenoteButton from '@/components/MkRenoteButton.vue';
|
||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import MkVisibility from '@/components/MkVisibility.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||
import { userPage } from '@/filters/user';
|
||||
@@ -138,6 +158,7 @@ import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@@ -168,7 +189,7 @@ const isRenote = (
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
|
||||
const renoteButton = shallowRef<HTMLElement>();
|
||||
const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||
@@ -182,6 +203,7 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const conversation = ref<misskey.entities.Note[]>([]);
|
||||
const replies = ref<misskey.entities.Note[]>([]);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
@@ -198,6 +220,47 @@ useNoteCapture({
|
||||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
useTooltip(renoteButton, async (showing) => {
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: appearNote.id,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
const users = renotes.map(x => x.user);
|
||||
|
||||
if (users.length < 1) return;
|
||||
|
||||
os.popup(MkUsersTooltip, {
|
||||
showing,
|
||||
users,
|
||||
count: appearNote.renoteCount,
|
||||
targetElement: renoteButton.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.quote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}], renoteButton.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
}
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
os.post({
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<header class="kkwtjztg">
|
||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
||||
<MkA v-once v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" class="is-bot">bot</div>
|
||||
@@ -9,7 +9,12 @@
|
||||
<MkA class="created-at" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</MkA>
|
||||
<MkVisibility :note="note"/>
|
||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -17,7 +22,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkVisibility from '@/components/MkVisibility.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { notePage } from '@/filters/note';
|
||||
import { userPage } from '@/filters/user';
|
||||
|
||||
|
@@ -13,7 +13,6 @@
|
||||
<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i>
|
||||
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||
<i v-else-if="notification.type === 'pollVote'" class="ti ti-chart-arrows"></i>
|
||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||
<XReactionIcon
|
||||
@@ -51,11 +50,6 @@
|
||||
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<i class="ti ti-quote"></i>
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
@@ -239,12 +233,6 @@ useTooltip(reactionRef, (showing) => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.pollVote {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.pollEnded {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
|
@@ -35,7 +35,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import MkSwitch from './form/switch.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
|
@@ -84,6 +84,7 @@ provideMetadataReceiver((info) => {
|
||||
});
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
provide('shouldHeaderThin', true);
|
||||
provide('forceSpacerMin', true);
|
||||
|
||||
const contextmenu = $computed(() => ([{
|
||||
icon: 'ti ti-player-eject',
|
||||
@@ -136,5 +137,7 @@ defineExpose({
|
||||
.yrolvcoq {
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
|
||||
--margin: var(--marginHalf);
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||
<span class="text" :class="{ up }">+1</span>
|
||||
<span class="text" :class="{ up }">
|
||||
<XReactionIcon class="icon" :reaction="reaction"/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
reaction: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}>(), {
|
||||
@@ -20,6 +24,7 @@ const emit = defineEmits<{
|
||||
|
||||
let up = $ref(false);
|
||||
const zIndex = os.claimZIndex('veryLow');
|
||||
const angle = (90 - (Math.random() * 180)) + 'deg';
|
||||
|
||||
onMounted(() => {
|
||||
window.setTimeout(() => {
|
||||
@@ -55,10 +60,11 @@ onMounted(() => {
|
||||
font-weight: bold;
|
||||
transform: translateY(-30px);
|
||||
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
|
||||
will-change: opacity, transform;
|
||||
|
||||
&.up {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
transform: translateY(-50px) rotateZ(v-bind(angle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -49,9 +49,9 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import MkInput from './form/input.vue';
|
||||
import MkSelect from './form/select.vue';
|
||||
import MkSwitch from './form/switch.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSelect from './MkSelect.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import { formatDateTimeString } from '@/scripts/format-time-string';
|
||||
import { addTime } from '@/scripts/time';
|
||||
|
@@ -98,6 +98,7 @@ import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
@@ -156,7 +157,7 @@ let autocomplete = $ref(null);
|
||||
let draghover = $ref(false);
|
||||
let quoteId = $ref(null);
|
||||
let hasNotSpecifiedMentions = $ref(false);
|
||||
let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]'));
|
||||
let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]'));
|
||||
let imeText = $ref('');
|
||||
|
||||
const typing = throttle(3000, () => {
|
||||
@@ -543,7 +544,7 @@ function onDrop(ev): void {
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
const draftData = JSON.parse(localStorage.getItem('drafts') || '{}');
|
||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') || '{}');
|
||||
|
||||
draftData[draftKey] = {
|
||||
updatedAt: new Date(),
|
||||
@@ -558,15 +559,15 @@ function saveDraft() {
|
||||
},
|
||||
};
|
||||
|
||||
localStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
}
|
||||
|
||||
function deleteDraft() {
|
||||
const draftData = JSON.parse(localStorage.getItem('drafts') ?? '{}');
|
||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
||||
|
||||
delete draftData[draftKey];
|
||||
|
||||
localStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
}
|
||||
|
||||
async function post(ev?: MouseEvent) {
|
||||
@@ -622,8 +623,8 @@ async function post(ev?: MouseEvent) {
|
||||
emit('posted');
|
||||
if (postData.text && postData.text !== '') {
|
||||
const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
|
||||
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
|
||||
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
|
||||
const history = JSON.parse(miLocalStorage.getItem('hashtags') || '[]') as string[];
|
||||
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
|
||||
}
|
||||
posting = false;
|
||||
postAccount = null;
|
||||
@@ -698,7 +699,7 @@ onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 書きかけの投稿を復元
|
||||
if (!props.instant && !props.mention && !props.specified) {
|
||||
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
|
||||
const draft = JSON.parse(miLocalStorage.getItem('drafts') || '{}')[draftKey];
|
||||
if (draft) {
|
||||
text = draft.data.text;
|
||||
useCw = draft.data.useCw;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, h } from 'vue';
|
||||
import MkRadio from './radio.vue';
|
||||
import MkRadio from './MkRadio.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
@@ -61,7 +61,7 @@ const anime = () => {
|
||||
const rect = buttonRef.value.getBoundingClientRect();
|
||||
const x = rect.left + (buttonRef.value.offsetWidth / 2);
|
||||
const y = rect.top + (buttonRef.value.offsetHeight / 2);
|
||||
os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
|
||||
os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end');
|
||||
};
|
||||
|
||||
watch(() => props.count, (newCount, oldCount) => {
|
||||
|
@@ -16,6 +16,8 @@ defineProps<{
|
||||
padding: 16px;
|
||||
background: var(--infoWarnBg);
|
||||
color: var(--infoWarnFg);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
|
||||
> .link {
|
||||
margin-left: 4px;
|
||||
|
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="buttonRef"
|
||||
class="eddddedb _button canRenote"
|
||||
@click="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="count > 0" class="count">{{ count }}</p>
|
||||
</button>
|
||||
<button v-else class="eddddedb _button">
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XDetails from '@/components/MkUsersTooltip.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
count: number;
|
||||
}>();
|
||||
|
||||
const buttonRef = shallowRef<HTMLElement>();
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
|
||||
|
||||
useTooltip(buttonRef, async (showing) => {
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: props.note.id,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
const users = renotes.map(x => x.user);
|
||||
|
||||
if (users.length < 1) return;
|
||||
|
||||
os.popup(XDetails, {
|
||||
showing,
|
||||
users,
|
||||
count: props.count,
|
||||
targetElement: buttonRef.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
const renote = (viaKeyboard = false) => {
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: props.note.id,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.quote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: props.note,
|
||||
});
|
||||
},
|
||||
}], buttonRef.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.eddddedb {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
margin: 2px;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:not(.canRenote) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.renoted {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
> .count {
|
||||
display: inline;
|
||||
margin-left: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -30,10 +30,10 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkRadio from '@/components/form/radio.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkRadio from '@/components/MkRadio.vue';
|
||||
import * as os from '@/os';
|
||||
import * as config from '@/config';
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="vblkjoeq">
|
||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||
<div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick">
|
||||
<div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show">
|
||||
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
|
||||
<select
|
||||
ref="inputEl"
|
||||
@@ -118,7 +118,7 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const onClick = (ev: MouseEvent) => {
|
||||
function show(ev: MouseEvent) {
|
||||
focused.value = true;
|
||||
opening.value = true;
|
||||
|
||||
@@ -166,7 +166,7 @@ const onClick = (ev: MouseEvent) => {
|
||||
}).then(() => {
|
||||
focused.value = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -285,7 +285,7 @@ const onClick = (ev: MouseEvent) => {
|
||||
|
||||
<style lang="scss" module>
|
||||
.chevron {
|
||||
transition: transform 0.5s ease;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.chevronOpening {
|
@@ -53,7 +53,7 @@ import { defineAsyncComponent } from 'vue';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { apiUrl, host as configHost } from '@/config';
|
||||
import { byteify, hexify } from '@/scripts/2fa';
|
||||
|
@@ -69,8 +69,8 @@ import { } from 'vue';
|
||||
import getPasswordStrength from 'syuilo-password-strength';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInput from './form/input.vue';
|
||||
import MkSwitch from './form/switch.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkCaptcha from '@/components/MkCaptcha.vue';
|
||||
import * as config from '@/config';
|
||||
import * as os from '@/os';
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/>
|
||||
<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
@@ -73,9 +73,9 @@ const toggle = () => {
|
||||
width: 32px;
|
||||
height: 23px;
|
||||
outline: none;
|
||||
background: var(--swutchOffBg);
|
||||
background: var(--switchOffBg);
|
||||
background-clip: content-box;
|
||||
border: solid 1px var(--swutchOffBg);
|
||||
border: solid 1px var(--switchOffBg);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: inherit;
|
||||
@@ -87,7 +87,7 @@ const toggle = () => {
|
||||
left: 3px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: var(--swutchOffFg);
|
||||
background: var(--switchOffFg);
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -131,12 +131,12 @@ const toggle = () => {
|
||||
|
||||
&.checked {
|
||||
> .button {
|
||||
background-color: var(--swutchOnBg) !important;
|
||||
border-color: var(--swutchOnBg) !important;
|
||||
background-color: var(--switchOnBg) !important;
|
||||
border-color: var(--switchOnBg) !important;
|
||||
|
||||
> .knob {
|
||||
left: 12px;
|
||||
background: var(--swutchOnFg);
|
||||
background: var(--switchOnFg);
|
||||
}
|
||||
}
|
||||
}
|
@@ -36,8 +36,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { permissions as kinds } from 'misskey-js';
|
||||
import MkInput from './form/input.vue';
|
||||
import MkSwitch from './form/switch.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
|
@@ -54,7 +54,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import * as os from '@/os';
|
||||
|
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<span v-if="note.visibility !== 'public'" :class="$style.visibility" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" :class="$style.localOnly" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import XDetails from '@/components/MkUsersTooltip.vue';
|
||||
import * as os from '@/os';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
note: {
|
||||
visibility: string;
|
||||
localOnly?: boolean;
|
||||
visibleUserIds?: string[];
|
||||
},
|
||||
}>();
|
||||
|
||||
const specified = $shallowRef<HTMLElement>();
|
||||
|
||||
if (props.note.visibility === 'specified') {
|
||||
useTooltip($$(specified), async (showing) => {
|
||||
const users = await os.api('users/show', {
|
||||
userIds: props.note.visibleUserIds,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
os.popup(XDetails, {
|
||||
showing,
|
||||
users,
|
||||
count: props.note.visibleUserIds.length,
|
||||
targetElement: specified,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.visibility, .localOnly {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
@@ -45,7 +45,7 @@ export type DefaultStoredWidget = {
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, reactive, ref, computed } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { widgets as widgetDefs } from '@/widgets';
|
||||
import * as os from '@/os';
|
||||
|
@@ -61,8 +61,8 @@ function dragClear(fn) {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialWidth?: number;
|
||||
initialHeight?: number | null;
|
||||
initialWidth: number;
|
||||
initialHeight: number | null;
|
||||
canResize?: boolean;
|
||||
closeButton?: boolean;
|
||||
mini?: boolean;
|
||||
@@ -386,7 +386,7 @@ function onBrowserResize() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.initialWidth) applyTransformWidth(props.initialWidth);
|
||||
applyTransformWidth(props.initialWidth);
|
||||
if (props.initialHeight) applyTransformHeight(props.initialHeight);
|
||||
|
||||
applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2));
|
||||
|
@@ -38,13 +38,13 @@ const forceSpacerMin = inject('forceSpacerMin', false) || deviceKind === 'smartp
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (max-width: 360px) {
|
||||
@container (max-width: 450px) {
|
||||
.root {
|
||||
padding: v-bind('props.marginMin + "px"');
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 361px) {
|
||||
@container (min-width: 451px) {
|
||||
.root {
|
||||
padding: v-bind('props.marginMax + "px"');
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import MkInput from '../form/input.vue';
|
||||
import MkInput from '../MkInput.vue';
|
||||
import * as os from '@/os';
|
||||
import { Hpml } from '@/scripts/hpml/evaluator';
|
||||
import { NumberInputVarBlock } from '@/scripts/hpml/block';
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import MkTextarea from '../form/textarea.vue';
|
||||
import MkTextarea from '../MkTextarea.vue';
|
||||
import MkButton from '../MkButton.vue';
|
||||
import { apiUrl } from '@/config';
|
||||
import * as os from '@/os';
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import MkRadio from '../form/radio.vue';
|
||||
import MkRadio from '../MkRadio.vue';
|
||||
import * as os from '@/os';
|
||||
import { Hpml } from '@/scripts/hpml/evaluator';
|
||||
import { RadioButtonVarBlock } from '@/scripts/hpml/block';
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import MkSwitch from '../form/switch.vue';
|
||||
import MkSwitch from '../MkSwitch.vue';
|
||||
import * as os from '@/os';
|
||||
import { Hpml } from '@/scripts/hpml/evaluator';
|
||||
import { SwitchVarBlock } from '@/scripts/hpml/block';
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import MkInput from '../form/input.vue';
|
||||
import MkInput from '../MkInput.vue';
|
||||
import * as os from '@/os';
|
||||
import { Hpml } from '@/scripts/hpml/evaluator';
|
||||
import { TextInputVarBlock } from '@/scripts/hpml/block';
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import MkTextarea from '../form/textarea.vue';
|
||||
import MkTextarea from '../MkTextarea.vue';
|
||||
import * as os from '@/os';
|
||||
import { Hpml } from '@/scripts/hpml/evaluator';
|
||||
import { HpmlTextInput } from '@/scripts/hpml';
|
||||
|
@@ -6,7 +6,7 @@
|
||||
import { TextBlock } from '@/scripts/hpml/block';
|
||||
import { Hpml } from '@/scripts/hpml/evaluator';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import MkTextarea from '../form/textarea.vue';
|
||||
import MkTextarea from '../MkTextarea.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { miLocalStorage } from "./local-storage";
|
||||
|
||||
const address = new URL(location.href);
|
||||
const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content;
|
||||
|
||||
@@ -6,10 +8,10 @@ export const hostname = address.hostname;
|
||||
export const url = address.origin;
|
||||
export const apiUrl = url + '/api';
|
||||
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
|
||||
export const lang = localStorage.getItem('lang');
|
||||
export const lang = miLocalStorage.getItem('lang');
|
||||
export const langs = _LANGS_;
|
||||
export const locale = JSON.parse(localStorage.getItem('locale'));
|
||||
export const locale = JSON.parse(miLocalStorage.getItem('locale'));
|
||||
export const version = _VERSION_;
|
||||
export const instanceName = siteName === 'Misskey' ? host : siteName;
|
||||
export const ui = localStorage.getItem('ui');
|
||||
export const debug = localStorage.getItem('debug') === 'true';
|
||||
export const ui = miLocalStorage.getItem('ui');
|
||||
export const debug = miLocalStorage.getItem('debug') === 'true';
|
||||
|
@@ -9,9 +9,12 @@ import '@/style.scss';
|
||||
//#region account indexedDB migration
|
||||
import { set } from '@/scripts/idb-proxy';
|
||||
|
||||
if (localStorage.getItem('accounts') != null) {
|
||||
set('accounts', JSON.parse(localStorage.getItem('accounts')));
|
||||
localStorage.removeItem('accounts');
|
||||
{
|
||||
const accounts = miLocalStorage.getItem('accounts');
|
||||
if (accounts) {
|
||||
set('accounts', JSON.parse(accounts));
|
||||
miLocalStorage.removeItem('accounts');
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@@ -40,6 +43,7 @@ import { reloadChannel } from '@/scripts/unison-reload';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
||||
import { getUrlWithoutLoginId } from '@/scripts/login-id';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
|
||||
(async () => {
|
||||
console.info(`Misskey v${version}`);
|
||||
@@ -154,7 +158,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
const fetchInstanceMetaPromise = fetchInstance();
|
||||
|
||||
fetchInstanceMetaPromise.then(() => {
|
||||
localStorage.setItem('v', instance.version);
|
||||
miLocalStorage.setItem('v', instance.version);
|
||||
|
||||
// Init service worker
|
||||
initializeSw();
|
||||
@@ -223,12 +227,12 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
}
|
||||
|
||||
// クライアントが更新されたか?
|
||||
const lastVersion = localStorage.getItem('lastVersion');
|
||||
const lastVersion = miLocalStorage.getItem('lastVersion');
|
||||
if (lastVersion !== version) {
|
||||
localStorage.setItem('lastVersion', version);
|
||||
miLocalStorage.setItem('lastVersion', version);
|
||||
|
||||
// テーマリビルドするため
|
||||
localStorage.removeItem('theme');
|
||||
miLocalStorage.removeItem('theme');
|
||||
|
||||
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
|
||||
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
|
||||
@@ -244,7 +248,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
|
||||
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
|
||||
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
|
||||
}, { immediate: localStorage.theme == null });
|
||||
}, { immediate: miLocalStorage.getItem('theme') == null });
|
||||
|
||||
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
|
||||
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
|
||||
@@ -341,7 +345,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
});
|
||||
}
|
||||
|
||||
const lastUsed = localStorage.getItem('lastUsed');
|
||||
const lastUsed = miLocalStorage.getItem('lastUsed');
|
||||
if (lastUsed) {
|
||||
const lastUsedDate = parseInt(lastUsed, 10);
|
||||
// 二時間以上前なら
|
||||
@@ -351,7 +355,15 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
}));
|
||||
}
|
||||
}
|
||||
localStorage.setItem('lastUsed', Date.now().toString());
|
||||
miLocalStorage.setItem('lastUsed', Date.now().toString());
|
||||
|
||||
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3)))) {
|
||||
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
|
||||
}
|
||||
}
|
||||
|
||||
if ('Notification' in window) {
|
||||
// 許可を得ていなかったらリクエスト
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { computed, reactive } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { api } from './os';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
const instanceData = localStorage.getItem('instance');
|
||||
const instanceData = miLocalStorage.getItem('instance');
|
||||
|
||||
// TODO: instanceをリアクティブにするかは再考の余地あり
|
||||
|
||||
@@ -21,7 +22,7 @@ export async function fetchInstance() {
|
||||
instance[k] = v;
|
||||
}
|
||||
|
||||
localStorage.setItem('instance', JSON.stringify(instance));
|
||||
miLocalStorage.setItem('instance', JSON.stringify(instance));
|
||||
}
|
||||
|
||||
export const emojiCategories = computed(() => {
|
||||
|
33
packages/frontend/src/local-storage.ts
Normal file
33
packages/frontend/src/local-storage.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
type Keys =
|
||||
'v' |
|
||||
'lastVersion' |
|
||||
'instance' |
|
||||
'account' |
|
||||
'accounts' |
|
||||
'latestDonationInfoShownAt' |
|
||||
'neverShowDonationInfo' |
|
||||
'lastUsed' |
|
||||
'lang' |
|
||||
'drafts' |
|
||||
'hashtags' |
|
||||
'wallpaper' |
|
||||
'theme' |
|
||||
'colorSchema' |
|
||||
'useSystemFont' |
|
||||
'fontSize' |
|
||||
'ui' |
|
||||
'locale' |
|
||||
'theme' |
|
||||
'customCss' |
|
||||
'message_drafts' |
|
||||
'scratchpad' |
|
||||
`miux:${string}` |
|
||||
`ui:folder:${string}` |
|
||||
`themes:${string}` |
|
||||
`aiscript:${string}`;
|
||||
|
||||
export const miLocalStorage = {
|
||||
getItem: (key: Keys) => window.localStorage.getItem(key),
|
||||
setItem: (key: Keys, value: string) => window.localStorage.setItem(key, value),
|
||||
removeItem: (key: Keys) => window.localStorage.removeItem(key),
|
||||
};
|
@@ -5,6 +5,7 @@ import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { ui } from '@/config';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
|
||||
export const navbarItemDef = reactive({
|
||||
notifications: {
|
||||
@@ -110,21 +111,21 @@ export const navbarItemDef = reactive({
|
||||
text: i18n.ts.default,
|
||||
active: ui === 'default' || ui === null,
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'default');
|
||||
miLocalStorage.setItem('ui', 'default');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.deck,
|
||||
active: ui === 'deck',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'deck');
|
||||
miLocalStorage.setItem('ui', 'deck');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.classic,
|
||||
active: ui === 'classic',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'classic');
|
||||
miLocalStorage.setItem('ui', 'classic');
|
||||
unisonReload();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
@@ -26,6 +26,7 @@ import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
error?: Error;
|
||||
@@ -42,7 +43,7 @@ os.api('meta', {
|
||||
loaded = true;
|
||||
serverIsDead = false;
|
||||
meta = res;
|
||||
localStorage.setItem('v', res.version);
|
||||
miLocalStorage.setItem('v', res.version);
|
||||
}, () => {
|
||||
loaded = true;
|
||||
serverIsDead = true;
|
||||
|
@@ -32,8 +32,8 @@
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import XEmoji from './emojis.emoji.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import * as os from '@/os';
|
||||
|
@@ -47,8 +47,8 @@
|
||||
<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 MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
|
@@ -64,7 +64,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkObjectView from '@/components/MkObjectView.vue';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
|
@@ -50,8 +50,8 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import XHeader from './_header_.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
||||
import * as os from '@/os';
|
||||
|
@@ -11,12 +11,12 @@
|
||||
<MkInput v-model="ad.imageUrl">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<FormRadios v-model="ad.place">
|
||||
<MkRadios v-model="ad.place">
|
||||
<template #label>Form</template>
|
||||
<option value="square">square</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
<option value="horizontal-big">horizontal-big</option>
|
||||
</FormRadios>
|
||||
</MkRadios>
|
||||
<!--
|
||||
<div style="margin: 32px 0;">
|
||||
{{ i18n.ts.priority }}
|
||||
@@ -50,9 +50,9 @@
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@@ -30,8 +30,8 @@
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
@@ -2,50 +2,50 @@
|
||||
<div>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<FormRadios v-model="provider">
|
||||
<MkRadios v-model="provider">
|
||||
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
|
||||
<option value="hcaptcha">hCaptcha</option>
|
||||
<option value="recaptcha">reCAPTCHA</option>
|
||||
<option value="turnstile">Turnstile</option>
|
||||
</FormRadios>
|
||||
</MkRadios>
|
||||
|
||||
<template v-if="provider === 'hcaptcha'">
|
||||
<FormInput v-model="hcaptchaSiteKey">
|
||||
<MkInput v-model="hcaptchaSiteKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="hcaptchaSecretKey">
|
||||
</MkInput>
|
||||
<MkInput v-model="hcaptchaSecretKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
<template v-else-if="provider === 'recaptcha'">
|
||||
<FormInput v-model="recaptchaSiteKey">
|
||||
<MkInput v-model="recaptchaSiteKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="recaptchaSecretKey">
|
||||
</MkInput>
|
||||
<MkInput v-model="recaptchaSecretKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
<FormSlot v-if="recaptchaSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
<template v-else-if="provider === 'turnstile'">
|
||||
<FormInput v-model="turnstileSiteKey">
|
||||
<MkInput v-model="turnstileSiteKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="turnstileSecretKey">
|
||||
</MkInput>
|
||||
<MkInput v-model="turnstileSecretKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/>
|
||||
@@ -60,8 +60,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
|
@@ -4,41 +4,41 @@
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<FormSwitch v-model="enableEmail">
|
||||
<MkSwitch v-model="enableEmail">
|
||||
<template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
|
||||
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
|
||||
<template v-if="enableEmail">
|
||||
<FormInput v-model="email" type="email">
|
||||
<MkInput v-model="email" type="email">
|
||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.smtpConfig }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpHost">
|
||||
<MkInput v-model="smtpHost">
|
||||
<template #label>{{ i18n.ts.smtpHost }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPort" type="number">
|
||||
</MkInput>
|
||||
<MkInput v-model="smtpPort" type="number">
|
||||
<template #label>{{ i18n.ts.smtpPort }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpUser">
|
||||
<MkInput v-model="smtpUser">
|
||||
<template #label>{{ i18n.ts.smtpUser }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPass" type="password">
|
||||
</MkInput>
|
||||
<MkInput v-model="smtpPass" type="password">
|
||||
<template #label>{{ i18n.ts.smtpPass }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
|
||||
<FormSwitch v-model="smtpSecure">
|
||||
<MkSwitch v-model="smtpSecure">
|
||||
<template #label>{{ i18n.ts.smtpSecure }}</template>
|
||||
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</FormSection>
|
||||
</template>
|
||||
@@ -51,8 +51,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
|
@@ -32,7 +32,7 @@
|
||||
import { } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import * as os from '@/os';
|
||||
import { unique } from '@/scripts/array';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@@ -71,10 +71,10 @@
|
||||
import { computed, defineAsyncComponent, defineComponent, ref, shallowRef } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import { selectFile, selectFiles } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
|
@@ -37,8 +37,8 @@ import { computed, defineAsyncComponent } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
|
@@ -3,10 +3,10 @@
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormTextarea v-model="blockedHosts">
|
||||
<MkTextarea v-model="blockedHosts">
|
||||
<span>{{ i18n.ts.blockedInstances }}</span>
|
||||
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
|
||||
</FormTextarea>
|
||||
</MkTextarea>
|
||||
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</FormSuspense>
|
||||
@@ -18,7 +18,7 @@
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
|
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<FormSwitch v-model="enableDiscordIntegration">
|
||||
<MkSwitch v-model="enableDiscordIntegration">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
|
||||
<template v-if="enableDiscordIntegration">
|
||||
<FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
|
||||
|
||||
<FormInput v-model="discordClientId">
|
||||
<MkInput v-model="discordClientId">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Client ID</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormInput v-model="discordClientSecret">
|
||||
<MkInput v-model="discordClientSecret">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Client Secret</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
</template>
|
||||
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
|
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<FormSwitch v-model="enableGithubIntegration">
|
||||
<MkSwitch v-model="enableGithubIntegration">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
|
||||
<template v-if="enableGithubIntegration">
|
||||
<FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
|
||||
|
||||
<FormInput v-model="githubClientId">
|
||||
<MkInput v-model="githubClientId">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Client ID</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormInput v-model="githubClientSecret">
|
||||
<MkInput v-model="githubClientSecret">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Client Secret</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
</template>
|
||||
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
|
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<FormSwitch v-model="enableTwitterIntegration">
|
||||
<MkSwitch v-model="enableTwitterIntegration">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
|
||||
<template v-if="enableTwitterIntegration">
|
||||
<FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
|
||||
|
||||
<FormInput v-model="twitterConsumerKey">
|
||||
<MkInput v-model="twitterConsumerKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Consumer Key</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormInput v-model="twitterConsumerSecret">
|
||||
<MkInput v-model="twitterConsumerSecret">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Consumer Secret</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
</template>
|
||||
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
|
@@ -4,63 +4,63 @@
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<FormSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</FormSwitch>
|
||||
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
||||
|
||||
<template v-if="useObjectStorage">
|
||||
<FormInput v-model="objectStorageBaseUrl">
|
||||
<MkInput v-model="objectStorageBaseUrl">
|
||||
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormInput v-model="objectStorageBucket">
|
||||
<MkInput v-model="objectStorageBucket">
|
||||
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormInput v-model="objectStoragePrefix">
|
||||
<MkInput v-model="objectStoragePrefix">
|
||||
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormInput v-model="objectStorageEndpoint">
|
||||
<MkInput v-model="objectStorageEndpoint">
|
||||
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormInput v-model="objectStorageRegion">
|
||||
<MkInput v-model="objectStorageRegion">
|
||||
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="objectStorageAccessKey">
|
||||
<MkInput v-model="objectStorageAccessKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Access key</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
|
||||
<FormInput v-model="objectStorageSecretKey">
|
||||
<MkInput v-model="objectStorageSecretKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Secret key</template>
|
||||
</FormInput>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
|
||||
<FormSwitch v-model="objectStorageUseSSL">
|
||||
<MkSwitch v-model="objectStorageUseSSL">
|
||||
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageUseProxy">
|
||||
<MkSwitch v-model="objectStorageUseProxy">
|
||||
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageSetPublicRead">
|
||||
<MkSwitch v-model="objectStorageSetPublicRead">
|
||||
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageS3ForcePathStyle">
|
||||
<MkSwitch v-model="objectStorageS3ForcePathStyle">
|
||||
<template #label>s3ForcePathStyle</template>
|
||||
</FormSwitch>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
@@ -71,8 +71,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
|
@@ -104,6 +104,10 @@ async function renderChart() {
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: 'day',
|
||||
displayFormats: {
|
||||
day: 'M/d',
|
||||
month: 'Y/M',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
|
@@ -214,6 +214,10 @@ onMounted(async () => {
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: 'day',
|
||||
displayFormats: {
|
||||
day: 'M/d',
|
||||
month: 'Y/M',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
@@ -223,11 +227,6 @@ onMounted(async () => {
|
||||
maxRotation: 0,
|
||||
autoSkipPadding: 16,
|
||||
},
|
||||
adapters: {
|
||||
date: {
|
||||
locale: enUS,
|
||||
},
|
||||
},
|
||||
min: getDate(chartLimit).getTime(),
|
||||
},
|
||||
y: {
|
||||
|
@@ -13,7 +13,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
|
||||
let src = $ref('active-users');
|
||||
</script>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user