Compare commits

..

35 Commits

Author SHA1 Message Date
syuilo
f20d7cba74 13.0.0-beta.28 2023-01-08 11:17:02 +09:00
syuilo
a3e282bc75 New Crowdin updates (#9478)
* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Slovak)
2023-01-08 11:16:36 +09:00
syuilo
49a95c34bf Update CHANGELOG.md 2023-01-08 11:16:26 +09:00
Soni L
ecbefce2aa Support remote objects in search (#9479)
* Support remote objects in search

Closes #9428

* Use account instead of localStorage

* Use useRouter instead of mainRouter

Co-authored-by: Chaos <chaoticryptidz@owo.monster>
2023-01-08 11:15:54 +09:00
syuilo
91356b1805 tweak 2023-01-08 11:12:11 +09:00
syuilo
2e2ed1385f delete pollVote notification 2023-01-08 10:54:45 +09:00
syuilo
49f3090edd tweak note componsnt 2023-01-08 10:48:44 +09:00
syuilo
4594fb11de 🎨 2023-01-08 10:32:37 +09:00
syuilo
b93e56d2e5 🎨 2023-01-08 10:24:30 +09:00
syuilo
c550dafb81 tweak note component 2023-01-08 10:20:28 +09:00
syuilo
8709574f3d 🎨 2023-01-08 09:58:35 +09:00
syuilo
1b7043fa79 🎨 2023-01-07 19:57:48 +09:00
syuilo
55ef2393fb 13.0.0-beta.27 2023-01-07 17:44:15 +09:00
syuilo
7769095efb 🎨 2023-01-07 17:39:24 +09:00
syuilo
b8248bdd65 🎨 2023-01-07 17:37:30 +09:00
syuilo
6f4ad581dc 🎨 2023-01-07 17:27:09 +09:00
syuilo
aec94920ab 🎨 2023-01-07 17:20:50 +09:00
syuilo
155ca39063 update aiscript 2023-01-07 17:18:30 +09:00
syuilo
58bfb4dca4 refactor 2023-01-07 15:09:46 +09:00
syuilo
49a0b6c48b fix typo 2023-01-07 15:00:29 +09:00
syuilo
799a653b44 🎨 2023-01-07 15:00:00 +09:00
syuilo
d09e1f4925 refactor 2023-01-07 14:59:54 +09:00
syuilo
cac784af8a fix #9483 2023-01-07 14:44:50 +09:00
syuilo
d7e0ddcbca Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-07 14:33:36 +09:00
syuilo
8c0811a442 🎨 2023-01-07 14:33:33 +09:00
syuilo
bab6f75260 Update CHANGELOG.md 2023-01-07 14:24:33 +09:00
syuilo
54e3fccd87 enhance(server): refactor and tweak emoji proxy 2023-01-07 14:19:25 +09:00
syuilo
6a992b6982 fix chart rendering 2023-01-07 14:10:01 +09:00
syuilo
ecd6fc1db8 🎨 2023-01-07 11:49:04 +09:00
syuilo
d99be6697e enhance(client): donation dialog 2023-01-07 11:49:00 +09:00
syuilo
d2d77b5dc1 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-07 10:13:07 +09:00
syuilo
91503405b4 refactor(client): typed localStorage 2023-01-07 10:13:02 +09:00
tamaina
c336201084 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-06 14:01:02 +00:00
tamaina
0f3399753d chore: remove Search from the name of OpenSearch 2023-01-06 14:00:54 +00:00
tamaina
5ec89ea0c3 カスタム絵文字にプロキシを復活 (#9481)
* wip

* Revert "Update ClientServerService.ts"

This reverts commit 88c64ece78.

* Revert "disable custom emoji proxy temporary"

This reverts commit 495d513efd.

* ✌️
2023-01-06 22:34:50 +09:00
166 changed files with 1234 additions and 1123 deletions

View File

@@ -30,11 +30,12 @@ You should also include the user name that made the change.
#### For users #### For users
- ノートのウォッチ機能が削除されました - ノートのウォッチ機能が削除されました
- アンケートに投票された際に通知が作成されなくなりました
- 新たに動的なPagesを作ることはできなくなりました - 新たに動的なPagesを作ることはできなくなりました
- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。 - 代わりに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.xの変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
- 0.12.1未満のプラグインは読み込むことはできません - 0.12.x未満のプラグインは読み込むことはできません
- iOS15以下のデバイスはサポートされなくなりました - iOS15以下のデバイスはサポートされなくなりました
- Firefox109以下はサポートされなくなりました - Firefox109以下はサポートされなくなりました
@@ -77,6 +78,7 @@ You should also include the user name that made the change.
- Client: Improve RSS widget @tamaina - Client: Improve RSS widget @tamaina
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz - Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
- Client: OpenSearch support @SoniEx2 @chaoticryptidz - Client: OpenSearch support @SoniEx2 @chaoticryptidz
- Client: Support remote objects in search @SoniEx2
- Client: add user list widget @syuilo - Client: add user list widget @syuilo
- Client: add heatmap of daily active users to about page @syuilo - Client: add heatmap of daily active users to about page @syuilo
- Client: introduce fluent emoji @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: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
- Client: use proxied image for instance icon @syuilo - Client: use proxied image for instance icon @syuilo
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa - Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
- Client: Page編集でブロックの移動が行えない問題を修正 @syuilo
- Client: update emoji picker immediately on all input @saschanaz - Client: update emoji picker immediately on all input @saschanaz
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo - Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
- Client: fix wrong link in tutorial @syuilo - Client: fix wrong link in tutorial @syuilo

View File

@@ -920,6 +920,10 @@ like: "Gefällt mir"
unlike: "\"Gefällt mir\" entfernen" unlike: "\"Gefällt mir\" entfernen"
numberOfLikes: "\"Gefällt mir\"-Anzahl" numberOfLikes: "\"Gefällt mir\"-Anzahl"
show: "Anzeigen" 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: _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." 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" sensitivity: "Erkennungssensitivität"

View File

@@ -920,6 +920,10 @@ like: "Like"
unlike: "Unlike" unlike: "Unlike"
numberOfLikes: "Likes" numberOfLikes: "Likes"
show: "Show" 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: _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." 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" sensitivity: "Detection sensitivity"

View File

@@ -28,7 +28,7 @@ timeline: "Timeline"
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo." noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo."
login: "Accedi" login: "Accedi"
loggingIn: "Accesso in corso..." loggingIn: "Accesso in corso..."
logout: "Esci" logout: "Uscita"
signup: "Iscriviti" signup: "Iscriviti"
uploading: "Caricamento..." uploading: "Caricamento..."
save: "Salva" save: "Salva"

View File

@@ -920,6 +920,10 @@ like: "いいね!"
unlike: "いいねを解除" unlike: "いいねを解除"
numberOfLikes: "いいね数" numberOfLikes: "いいね数"
show: "表示" show: "表示"
neverShow: "今後表示しない"
remindMeLater: "また後で"
didYouLikeMisskey: "Misskeyを気に入っていただけましたか"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@@ -1546,7 +1550,6 @@ _notification:
youGotReply: "{name}からのリプライ" youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました" youRenoted: "{name}がRenoteしました"
youGotPoll: "{name}が投票しました"
youGotMessagingMessageFromUser: "{name}からのチャットがあります" youGotMessagingMessageFromUser: "{name}からのチャットがあります"
youGotMessagingMessageFromGroup: "{name}のチャットがあります" youGotMessagingMessageFromGroup: "{name}のチャットがあります"
youWereFollowed: "フォローされました" youWereFollowed: "フォローされました"
@@ -1565,7 +1568,6 @@ _notification:
renote: "Renote" renote: "Renote"
quote: "引用" quote: "引用"
reaction: "リアクション" reaction: "リアクション"
pollVote: "アンケートに投票された"
pollEnded: "アンケートが終了" pollEnded: "アンケートが終了"
receiveFollowRequest: "フォロー申請を受け取った" receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された" followRequestAccepted: "フォローが受理された"

View File

@@ -920,6 +920,10 @@ like: "좋아요!"
unlike: "좋아요 취소" unlike: "좋아요 취소"
numberOfLikes: "좋아요 수" numberOfLikes: "좋아요 수"
show: "표시" show: "표시"
neverShow: "다시 보지 않기"
remindMeLater: "나중에 알림"
didYouLikeMisskey: "Misskey가 마음에 드시나요?"
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
sensitivity: "탐지 민감도" sensitivity: "탐지 민감도"

View File

@@ -913,6 +913,10 @@ tools: "Nástroje"
cannotLoad: "Nedá sa načítať." cannotLoad: "Nedá sa načítať."
like: "Páči sa mi" like: "Páči sa mi"
show: "Zobraziť" 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: _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." 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" sensitivity: "Citlivosť detekcie"

View File

@@ -917,6 +917,8 @@ tools: "เครื่องมือ"
cannotLoad: "ไม่สามารถโหลดได้" cannotLoad: "ไม่สามารถโหลดได้"
numberOfProfileView: "มุมมองโปรไฟล์" numberOfProfileView: "มุมมองโปรไฟล์"
like: "ชื่นชอบ" like: "ชื่นชอบ"
unlike: "ไม่ชอบ"
numberOfLikes: "จำนวนไลค์"
show: "แสดงผล" show: "แสดงผล"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
@@ -1317,6 +1319,7 @@ _widgets:
jobQueue: "คิวงาน" jobQueue: "คิวงาน"
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์" serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
aiscript: "AiScript คอนโซล" aiscript: "AiScript คอนโซล"
aiscriptApp: "AiScript แอพ"
aichan: "เอไอ" aichan: "เอไอ"
userList: "รายชื่อผู้ใช้" userList: "รายชื่อผู้ใช้"
_userList: _userList:
@@ -1423,7 +1426,16 @@ _timelines:
social: "โซเชี่ยล" social: "โซเชี่ยล"
global: "ทั่วโลก" global: "ทั่วโลก"
_play: _play:
new: "สร้างการเล่น"
edit: "แก้ไขเล่น"
created: "สร้างการเล่นแล้ว"
updated: "แก้ไขการเล่นแล้ว"
deleted: "ลบการเล่นแล้ว"
pageSetting: "ตั้งค่าการเล่น"
editThisPage: "แก้ไข Play นี้"
viewSource: "ดูต้นฉบับ" viewSource: "ดูต้นฉบับ"
my: "มาย เพลย์"
liked: "ไลค์ เพลย์"
featured: "เป็นที่นิยม" featured: "เป็นที่นิยม"
title: "หัวข้อ" title: "หัวข้อ"
script: "สคริปต์" script: "สคริปต์"

View File

@@ -918,7 +918,11 @@ cannotLoad: "無法載入"
numberOfProfileView: "個人檔案檢視次數" numberOfProfileView: "個人檔案檢視次數"
like: "讚" like: "讚"
unlike: "收回讚" unlike: "收回讚"
numberOfLikes: "讚數"
show: "檢視" show: "檢視"
neverShow: "不再顯示"
remindMeLater: "以後再說"
didYouLikeMisskey: "您是否喜愛Misskey呢"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度" sensitivity: "檢測敏感度"
@@ -1318,6 +1322,7 @@ _widgets:
jobQueue: "佇列" jobQueue: "佇列"
serverMetric: "服務器指標 " serverMetric: "服務器指標 "
aiscript: "AiScript控制台" aiscript: "AiScript控制台"
aiscriptApp: "AiScript App"
aichan: "小藍" aichan: "小藍"
userList: "使用者列表" userList: "使用者列表"
_userList: _userList:
@@ -1424,7 +1429,16 @@ _timelines:
social: "社群" social: "社群"
global: "公開" global: "公開"
_play: _play:
new: "新增Play"
edit: "編輯Play"
created: "已新增Play"
updated: "已更新Play"
deleted: "已刪除Play"
pageSetting: "Play設定"
editThisPage: "編輯這個Play"
viewSource: "檢視原始碼" viewSource: "檢視原始碼"
my: "自己的Play"
liked: "按了讚的Play"
featured: "人氣" featured: "人氣"
title: "標題" title: "標題"
script: "腳本" script: "腳本"

View File

@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.0.0-beta.26", "version": "13.0.0-beta.28",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -92,13 +92,6 @@ export class PollService {
choice: choice, choice: choice,
userId: user.id, userId: user.id,
}); });
// Notify
this.createNotificationService.createNotification(note.userId, 'pollVote', {
notifierId: user.id,
noteId: note.id,
choice: choice,
});
} }
@bindThis @bindThis

View File

@@ -98,7 +98,7 @@ export class NotificationEntityService implements OnModuleInit {
}), }),
reaction: notification.reaction, reaction: notification.reaction,
} : {}), } : {}),
...(notification.type === 'pollVote' ? { ...(notification.type === 'pollVote' ? { // TODO: そのうち消す
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true, detail: true,
_hint_: options._hintForEachNotes_, _hint_: options._hintForEachNotes_,

View File

@@ -3,6 +3,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
const dictionary = { const dictionary = {
'safe-file': FILE_TYPE_BROWSERSAFE, '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-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); export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);

View File

@@ -55,11 +55,11 @@ export class Notification {
* 通知の種類。 * 通知の種類。
* follow - フォローされた * follow - フォローされた
* mention - 投稿で自分が言及された * mention - 投稿で自分が言及された
* reply - (自分または自分がWatchしている)投稿返信された * reply - 投稿返信された
* renote - (自分または自分がWatchしている)投稿がRenoteされた * renote - 投稿がRenoteされた
* quote - (自分または自分がWatchしている)投稿が引用Renoteされた * quote - 投稿が引用Renoteされた
* reaction - (自分または自分がWatchしている)投稿にリアクションされた * reaction - 投稿にリアクションされた
* pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された * pollVote - 投稿のアンケートに投票された (廃止)
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
* receiveFollowRequest - フォローリクエストされた * receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された * followRequestAccepted - 自分の送ったフォローリクエストが承認された

View File

@@ -79,9 +79,17 @@ export class MediaProxyServerService {
const { mime, ext } = await this.fileInfoService.detectType(path); const { mime, ext } = await this.fileInfoService.detectType(path);
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image'); const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
let image: IImage; let image: IImage;
if ('emoji' in request.query && isConvertibleImage) { if ('emoji' in request.query && isConvertibleImage) {
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) }) const data = await sharp(path, { animated: !('static' in request.query) })
.resize({ .resize({
height: 128, height: 128,
@@ -95,6 +103,7 @@ export class MediaProxyServerService {
ext: 'webp', ext: 'webp',
type: 'image/webp', type: 'image/webp',
}; };
}
} else if ('static' in request.query && isConvertibleImage) { } else if ('static' in request.query && isConvertibleImage) {
image = await this.imageProcessingService.convertToWebp(path, 498, 280); image = await this.imageProcessingService.convertToWebp(path, 498, 280);
} else if ('preview' in request.query && isConvertibleImage) { } else if ('preview' in request.query && isConvertibleImage) {

View File

@@ -1,12 +1,11 @@
import cluster from 'node:cluster'; import cluster from 'node:cluster';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as http from 'node:http';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import Fastify from 'fastify'; import Fastify from 'fastify';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Config } from '@/config.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 { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { envOption } from '@/env.js'; import { envOption } from '@/env.js';
@@ -39,6 +38,9 @@ export class ServerService {
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private apiServerService: ApiServerService, private apiServerService: ApiServerService,
private streamingApiServerService: StreamingApiServerService, private streamingApiServerService: StreamingApiServerService,
@@ -77,6 +79,43 @@ export class ServerService {
fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.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) => { fastify.get<{ Params: { acct: string } }>('/avatar/@:acct', async (request, reply) => {
const { username, host } = Acct.parse(request.params.acct); const { username, host } = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOne({ const user = await this.usersRepository.findOne({

View File

@@ -162,13 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userId: me.id, userId: me.id,
}); });
// Notify
this.createNotificationService.createNotification(note.userId, 'pollVote', {
notifierId: me.id,
noteId: note.id,
choice: ps.choice,
});
// リモート投票の場合リプライ送信 // リモート投票の場合リプライ送信
if (note.userHost != null) { if (note.userHost != null) {
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser; const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser;

View File

@@ -1,6 +1,5 @@
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { PathOrFileDescriptor, readFileSync } from 'node:fs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { createBullBoard } from '@bull-board/api'; import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter.js'; import { BullAdapter } from '@bull-board/api/bullAdapter.js';
@@ -71,9 +70,6 @@ export class ClientServerService {
@Inject(DI.pagesRepository) @Inject(DI.pagesRepository)
private pagesRepository: PagesRepository, private pagesRepository: PagesRepository,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
@Inject(DI.flashsRepository) @Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository, private flashsRepository: FlashsRepository,
@@ -225,49 +221,6 @@ export class ClientServerService {
return reply.sendFile('/apple-touch-icon.png', staticAssets); 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) => { fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => {
const path = request.params.path; const path = request.params.path;
@@ -362,7 +315,7 @@ export class ClientServerService {
const name = meta.name || 'Misskey'; const name = meta.name || 'Misskey';
let content = ''; let content = '';
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">'; 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 += `<Description>${name} Search</Description>`;
content += '<InputEncoding>UTF-8</InputEncoding>'; content += '<InputEncoding>UTF-8</InputEncoding>';
content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`; content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`;

View File

@@ -31,7 +31,7 @@ html
link(rel='icon' href= icon || '/favicon.ico') link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png') link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json') 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/info.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')

View File

@@ -11,7 +11,7 @@
"@rollup/plugin-alias": "4.0.2", "@rollup/plugin-alias": "4.0.2",
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2", "@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.12.1", "@syuilo/aiscript": "0.12.2",
"@tabler/icons": "^1.118.0", "@tabler/icons": "^1.118.0",
"@vitejs/plugin-vue": "4.0.0", "@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "3.2.45", "@vue/compiler-sfc": "3.2.45",

View File

@@ -6,12 +6,13 @@ import { del, get, set } from '@/scripts/idb-proxy';
import { apiUrl } from '@/config'; import { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { waiting, api, popup, popupMenu, success, alert } from '@/os';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
import { miLocalStorage } from './local-storage';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
type Account = misskey.entities.MeDetailed; type Account = misskey.entities.MeDetailed;
const accountData = localStorage.getItem('account'); const accountData = miLocalStorage.getItem('account');
// TODO: 外部からはreadonlyに // TODO: 外部からはreadonlyに
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; 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() { export async function signout() {
waiting(); waiting();
localStorage.removeItem('account'); miLocalStorage.removeItem('account');
await removeAccount($i.id); await removeAccount($i.id);
@@ -119,7 +120,7 @@ export function updateAccount(accountData) {
for (const [key, value] of Object.entries(accountData)) { for (const [key, value] of Object.entries(accountData)) {
$i[key] = value; $i[key] = value;
} }
localStorage.setItem('account', JSON.stringify($i)); miLocalStorage.setItem('account', JSON.stringify($i));
} }
export function refreshAccount() { export function refreshAccount() {
@@ -130,7 +131,7 @@ export async function login(token: Account['token'], redirect?: string) {
waiting(); waiting();
if (_DEV_) console.log('logging as token ', token); if (_DEV_) console.log('logging as token ', token);
const me = await fetchAccount(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の認証とかで使う document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
await addAccount(me.id, token); await addAccount(me.id, token);

View File

@@ -37,7 +37,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import MkButton from '@/components/MkButton.vue'; 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 MkKeyValue from '@/components/MkKeyValue.vue';
import { acct, userPage } from '@/filters/user'; import { acct, userPage } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -28,7 +28,7 @@
import { ref, shallowRef } from 'vue'; import { ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkWindow from '@/components/MkWindow.vue'; 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 MkButton from '@/components/MkButton.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';

View File

@@ -51,10 +51,10 @@
import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue'; import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/MkSelect.vue';
import { AsUiComponent } from '@/scripts/aiscript/ui'; import { AsUiComponent } from '@/scripts/aiscript/ui';
import FormFolder from '@/components/form/folder.vue'; import FormFolder from '@/components/form/folder.vue';

View File

@@ -46,6 +46,7 @@ import { defaultStore } from '@/store';
import { emojilist } from '@/scripts/emojilist'; import { emojilist } from '@/scripts/emojilist';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { miLocalStorage } from '@/local-storage';
type EmojiDef = { type EmojiDef = {
emoji: string; emoji: string;
@@ -208,7 +209,7 @@ function exec() {
} }
} else if (props.type === 'hashtag') { } else if (props.type === 'hashtag') {
if (!props.q || props.q === '') { if (!props.q || props.q === '') {
hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]'); hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
fetching.value = false; fetching.value = false;
} else { } else {
const cacheKey = `autocomplete:hashtag:${props.q}`; const cacheKey = `autocomplete:hashtag:${props.q}`;

View File

@@ -74,7 +74,7 @@ function onMousedown(evt: Event) {
} }
.fade-enter-active, .fade-leave-active { .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; transform-origin: left top;
} }

View File

@@ -42,8 +42,8 @@
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'; import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/MkSelect.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
type Input = { type Input = {

View 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>

View File

@@ -1,5 +1,5 @@
<template> <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()"> <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"> <div ref="emojis" class="emojis">
<section class="result"> <section class="result">
@@ -94,6 +94,7 @@ const props = withDefaults(defineProps<{
asReactionPicker?: boolean; asReactionPicker?: boolean;
maxHeight?: number; maxHeight?: number;
asDrawer?: boolean; asDrawer?: boolean;
asWindow?: boolean;
}>(), { }>(), {
showPinned: true, 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 { > .search {
width: 100%; width: 100%;
padding: 12px; padding: 12px;

View File

@@ -1,13 +1,13 @@
<template> <template>
<MkWindow ref="window" <MkWindow ref="window"
:initial-width="null" :initial-width="300"
:initial-height="null" :initial-height="290"
:can-resize="false" :can-resize="true"
:mini="true" :mini="true"
:front="true" :front="true"
@closed="emit('closed')" @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> </MkWindow>
</template> </template>
@@ -34,147 +34,8 @@ function chosen(emoji: any) {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.omfetrab { .picker {
$pad: 8px; height: 100%;
--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> </style>

View File

@@ -23,7 +23,7 @@
import { } from 'vue'; import { } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue'; 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 MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';

View File

@@ -25,8 +25,9 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { miLocalStorage } from '@/local-storage';
const localStoragePrefix = 'ui:folder:'; const miLocalStoragePrefix = 'ui:folder:' as const;
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -44,13 +45,13 @@ export default defineComponent({
data() { data() {
return { return {
bg: null, 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: { watch: {
showBody() { showBody() {
if (this.persistKey) { if (this.persistKey) {
localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f'); miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
} }
}, },
}, },

View File

@@ -36,7 +36,7 @@
import { } from 'vue'; import { } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.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 * as os from '@/os';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';

View File

@@ -17,34 +17,34 @@
<MkSpacer :margin-min="20" :margin-max="32"> <MkSpacer :margin-min="20" :margin-max="32">
<div class="xkpnjxcv _gaps_m"> <div class="xkpnjxcv _gaps_m">
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)"> <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 #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> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormInput> </MkInput>
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text"> <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 #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> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormInput> </MkInput>
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]"> <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 #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> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormTextarea> </MkTextarea>
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]"> <MkSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
<span v-text="form[item].label || item"></span> <span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormSwitch> </MkSwitch>
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]"> <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> <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> <option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
</FormSelect> </MkSelect>
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]"> <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> <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> <option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
</FormRadios> </MkRadios>
<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"> <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 #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> <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)"> <MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
<span v-text="form[item].content || item"></span> <span v-text="form[item].content || item"></span>
</MkButton> </MkButton>
@@ -56,25 +56,25 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import FormInput from './form/input.vue'; import MkInput from './MkInput.vue';
import FormTextarea from './form/textarea.vue'; import MkTextarea from './MkTextarea.vue';
import FormSwitch from './form/switch.vue'; import MkSwitch from './MkSwitch.vue';
import FormSelect from './form/select.vue'; import MkSelect from './MkSelect.vue';
import FormRange from './form/range.vue'; import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import FormRadios from './form/radios.vue'; import MkRadios from './MkRadios.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
MkModalWindow, MkModalWindow,
FormInput, MkInput,
FormTextarea, MkTextarea,
FormSwitch, MkSwitch,
FormSelect, MkSelect,
FormRange, MkRange,
MkButton, MkButton,
FormRadios, MkRadios,
}, },
props: { props: {

View File

@@ -78,7 +78,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { Chart } from 'chart.js'; 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 MkChart from '@/components/MkChart.vue';
import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -31,7 +31,7 @@
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> <span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
</button> </button>
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <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> </span>
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)"> <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> <i v-if="item.icon" class="ti-fw" :class="item.icon"></i>
@@ -58,7 +58,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue'; import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus'; 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 { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@@ -251,17 +251,18 @@ onBeforeUnmount(() => {
color: #fff; color: #fff;
&:before { &:before {
background: #d42e2e; background: #d42e2e !important;
} }
} }
} }
&:active,
&.active { &.active {
color: var(--fgOnAccent); color: var(--fgOnAccent) !important;
opacity: 1; opacity: 1;
&:before { &:before {
background: var(--accent); background: var(--accent) !important;
} }
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"> <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 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"> <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
<slot :max-height="maxHeight" :type="type"></slot> <slot :max-height="maxHeight" :type="type"></slot>
</div> </div>
@@ -74,8 +74,24 @@ const type = $computed<ModalTypes>(() => {
return props.preferType!; return props.preferType!;
} }
}); });
let transitionName = $ref(defaultStore.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''); let transitionName = $computed((() =>
let transitionDuration = $ref(defaultStore.state.animation ? 200 : 0); 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; let contentClicking = false;
@@ -267,9 +283,8 @@ defineExpose({
} }
> .content { > .content {
transform-style: preserve-3d; transform: translateY(0px);
transform: perspective(50cm) translateZ(0px) translateY(0px) rotateX(0deg); transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
transition: opacity 0.4s cubic-bezier(.5,-0.5,.75,1), transform 0.4s cubic-bezier(.5,-0.5,.75,1) !important;
} }
} }
.send-enter-from, .send-leave-to { .send-enter-from, .send-leave-to {
@@ -280,8 +295,7 @@ defineExpose({
> .content { > .content {
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
transform-style: preserve-3d; transform: translateY(-300px);
transform: perspective(50cm) translateZ(-300px) translateY(-200px) rotateX(40deg);
} }
} }
@@ -310,12 +324,12 @@ defineExpose({
.modal-popup-enter-active, .modal-popup-leave-active { .modal-popup-enter-active, .modal-popup-leave-active {
> .bg { > .bg {
transition: opacity 0.2s !important; transition: opacity 0.1s !important;
} }
> .content { > .content {
transform-origin: var(--transformOrigin); 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 { .modal-popup-enter-from, .modal-popup-leave-to {

View File

@@ -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._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="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>
<div v-if="isRenote" class="renote"> <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> <i class="ti ti-repeat"></i>
<I18n :src="i18n.ts.renotedBy" tag="span"> <I18n :src="i18n.ts.renotedBy" tag="span">
<template #user> <template #user>
@@ -27,11 +27,16 @@
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
</button> </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>
</div> </div>
<article class="article" @contextmenu.stop="onContextmenu"> <article class="article" @contextmenu.stop="onContextmenu">
<MkAvatar class="avatar" :user="appearNote.user"/> <MkAvatar v-once class="avatar" :user="appearNote.user"/>
<div class="main"> <div class="main">
<MkNoteHeader class="header" :note="appearNote" :mini="true"/> <MkNoteHeader class="header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> <MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
@@ -44,7 +49,7 @@
<div class="text"> <div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <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> <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> <a v-if="appearNote.renote != null" class="rp">RN:</a>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
@@ -75,14 +80,25 @@
<i class="ti ti-arrow-back-up"></i> <i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button> </button>
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> <button
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> 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> <i class="ti ti-plus"></i>
</button> </button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i> <i class="ti ti-minus"></i>
</button> </button>
<button ref="menuButton" class="button _button" @click="menu()"> <button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i> <i class="ti ti-dots"></i>
</button> </button>
</footer> </footer>
@@ -111,10 +127,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue'; import MkMediaList from '@/components/MkMediaList.vue';
import MkCwButton from '@/components/MkCwButton.vue'; import MkCwButton from '@/components/MkCwButton.vue';
import MkPoll from '@/components/MkPoll.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 MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import MkVisibility from '@/components/MkVisibility.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import { focusPrev, focusNext } from '@/scripts/focus'; import { focusPrev, focusNext } from '@/scripts/focus';
import { checkWordMute } from '@/scripts/check-word-mute'; import { checkWordMute } from '@/scripts/check-word-mute';
@@ -128,6 +143,7 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu'; import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@@ -158,7 +174,7 @@ const isRenote = (
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>(); const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
@@ -175,6 +191,7 @@ const translation = ref(null);
const translating = ref(false); const translating = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); 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 = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
@@ -193,6 +210,47 @@ useNoteCapture({
isDeletedRef: isDeleted, 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 { function reply(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
os.post({ os.post({

View File

@@ -25,7 +25,12 @@
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
</button> </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>
</div> </div>
<article class="article" @contextmenu.stop="onContextmenu"> <article class="article" @contextmenu.stop="onContextmenu">
@@ -38,7 +43,12 @@
</MkA> </MkA>
<span v-if="appearNote.user.isBot" class="is-bot">bot</span> <span v-if="appearNote.user.isBot" class="is-bot">bot</span>
<div class="info"> <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> </div>
<div class="username"><MkAcct :user="appearNote.user"/></div> <div class="username"><MkAcct :user="appearNote.user"/></div>
@@ -85,14 +95,25 @@
<i class="ti ti-arrow-back-up"></i> <i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button> </button>
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> <button
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> 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> <i class="ti ti-plus"></i>
</button> </button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i> <i class="ti ti-minus"></i>
</button> </button>
<button ref="menuButton" class="button _button" @click="menu()"> <button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i> <i class="ti ti-dots"></i>
</button> </button>
</footer> </footer>
@@ -121,10 +142,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue'; import MkMediaList from '@/components/MkMediaList.vue';
import MkCwButton from '@/components/MkCwButton.vue'; import MkCwButton from '@/components/MkCwButton.vue';
import MkPoll from '@/components/MkPoll.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 MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import MkVisibility from '@/components/MkVisibility.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import { checkWordMute } from '@/scripts/check-word-mute'; import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
@@ -138,6 +158,7 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu'; import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@@ -168,7 +189,7 @@ const isRenote = (
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>(); const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); 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 showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const conversation = ref<misskey.entities.Note[]>([]); const conversation = ref<misskey.entities.Note[]>([]);
const replies = ref<misskey.entities.Note[]>([]); const replies = ref<misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
@@ -198,6 +220,47 @@ useNoteCapture({
isDeletedRef: isDeleted, 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 { function reply(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
os.post({ os.post({

View File

@@ -1,6 +1,6 @@
<template> <template>
<header class="kkwtjztg"> <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"/> <MkUserName :user="note.user"/>
</MkA> </MkA>
<div v-if="note.user.isBot" class="is-bot">bot</div> <div v-if="note.user.isBot" class="is-bot">bot</div>
@@ -9,7 +9,12 @@
<MkA class="created-at" :to="notePage(note)"> <MkA class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
</MkA> </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> </div>
</header> </header>
</template> </template>
@@ -17,7 +22,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkVisibility from '@/components/MkVisibility.vue'; import { i18n } from '@/i18n';
import { notePage } from '@/filters/note'; import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';

View File

@@ -13,7 +13,6 @@
<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i> <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 === 'mention'" class="ti ti-at"></i>
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></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> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon <XReactionIcon
@@ -51,11 +50,6 @@
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <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"/> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA> </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)"> <MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i> <i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
@@ -239,12 +233,6 @@ useTooltip(reactionRef, (showing) => {
pointer-events: none; pointer-events: none;
} }
&.pollVote {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
&.pollEnded { &.pollEnded {
padding: 3px; padding: 3px;
background: #88a6b7; background: #88a6b7;

View File

@@ -35,7 +35,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import { notificationTypes } from 'misskey-js'; import { notificationTypes } from 'misskey-js';
import MkSwitch from './form/switch.vue'; import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue'; import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';

View File

@@ -84,6 +84,7 @@ provideMetadataReceiver((info) => {
}); });
provide('shouldOmitHeaderTitle', true); provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true); provide('shouldHeaderThin', true);
provide('forceSpacerMin', true);
const contextmenu = $computed(() => ([{ const contextmenu = $computed(() => ([{
icon: 'ti ti-player-eject', icon: 'ti ti-player-eject',
@@ -136,5 +137,7 @@ defineExpose({
.yrolvcoq { .yrolvcoq {
min-height: 100%; min-height: 100%;
background: var(--bg); background: var(--bg);
--margin: var(--marginHalf);
} }
</style> </style>

View File

@@ -1,14 +1,18 @@
<template> <template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> <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> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import XReactionIcon from '@/components/MkReactionIcon.vue';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
reaction: string;
x: number; x: number;
y: number; y: number;
}>(), { }>(), {
@@ -20,6 +24,7 @@ const emit = defineEmits<{
let up = $ref(false); let up = $ref(false);
const zIndex = os.claimZIndex('veryLow'); const zIndex = os.claimZIndex('veryLow');
const angle = (90 - (Math.random() * 180)) + 'deg';
onMounted(() => { onMounted(() => {
window.setTimeout(() => { window.setTimeout(() => {
@@ -55,10 +60,11 @@ onMounted(() => {
font-weight: bold; font-weight: bold;
transform: translateY(-30px); transform: translateY(-30px);
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5); transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
will-change: opacity, transform;
&.up { &.up {
opacity: 0; opacity: 0;
transform: translateY(-50px); transform: translateY(-50px) rotateZ(v-bind(angle));
} }
} }
} }

View File

@@ -49,9 +49,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import MkInput from './form/input.vue'; import MkInput from './MkInput.vue';
import MkSelect from './form/select.vue'; import MkSelect from './MkSelect.vue';
import MkSwitch from './form/switch.vue'; import MkSwitch from './MkSwitch.vue';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import { formatDateTimeString } from '@/scripts/format-time-string'; import { formatDateTimeString } from '@/scripts/format-time-string';
import { addTime } from '@/scripts/time'; import { addTime } from '@/scripts/time';

View File

@@ -98,6 +98,7 @@ import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'
import { uploadFile } from '@/scripts/upload'; import { uploadFile } from '@/scripts/upload';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage';
const modal = inject('modal'); const modal = inject('modal');
@@ -156,7 +157,7 @@ let autocomplete = $ref(null);
let draghover = $ref(false); let draghover = $ref(false);
let quoteId = $ref(null); let quoteId = $ref(null);
let hasNotSpecifiedMentions = $ref(false); let hasNotSpecifiedMentions = $ref(false);
let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]')); let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]'));
let imeText = $ref(''); let imeText = $ref('');
const typing = throttle(3000, () => { const typing = throttle(3000, () => {
@@ -543,7 +544,7 @@ function onDrop(ev): void {
} }
function saveDraft() { function saveDraft() {
const draftData = JSON.parse(localStorage.getItem('drafts') || '{}'); const draftData = JSON.parse(miLocalStorage.getItem('drafts') || '{}');
draftData[draftKey] = { draftData[draftKey] = {
updatedAt: new Date(), updatedAt: new Date(),
@@ -558,15 +559,15 @@ function saveDraft() {
}, },
}; };
localStorage.setItem('drafts', JSON.stringify(draftData)); miLocalStorage.setItem('drafts', JSON.stringify(draftData));
} }
function deleteDraft() { function deleteDraft() {
const draftData = JSON.parse(localStorage.getItem('drafts') ?? '{}'); const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
delete draftData[draftKey]; delete draftData[draftKey];
localStorage.setItem('drafts', JSON.stringify(draftData)); miLocalStorage.setItem('drafts', JSON.stringify(draftData));
} }
async function post(ev?: MouseEvent) { async function post(ev?: MouseEvent) {
@@ -622,8 +623,8 @@ async function post(ev?: MouseEvent) {
emit('posted'); emit('posted');
if (postData.text && postData.text !== '') { if (postData.text && postData.text !== '') {
const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); 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[]; const history = JSON.parse(miLocalStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
} }
posting = false; posting = false;
postAccount = null; postAccount = null;
@@ -698,7 +699,7 @@ onMounted(() => {
nextTick(() => { nextTick(() => {
// 書きかけの投稿を復元 // 書きかけの投稿を復元
if (!props.instant && !props.mention && !props.specified) { 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) { if (draft) {
text = draft.data.text; text = draft.data.text;
useCw = draft.data.useCw; useCw = draft.data.useCw;

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, h } from 'vue'; import { defineComponent, h } from 'vue';
import MkRadio from './radio.vue'; import MkRadio from './MkRadio.vue';
export default defineComponent({ export default defineComponent({
components: { components: {

View File

@@ -61,7 +61,7 @@ const anime = () => {
const rect = buttonRef.value.getBoundingClientRect(); const rect = buttonRef.value.getBoundingClientRect();
const x = rect.left + (buttonRef.value.offsetWidth / 2); const x = rect.left + (buttonRef.value.offsetWidth / 2);
const y = rect.top + (buttonRef.value.offsetHeight / 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) => { watch(() => props.count, (newCount, oldCount) => {

View File

@@ -16,6 +16,8 @@ defineProps<{
padding: 16px; padding: 16px;
background: var(--infoWarnBg); background: var(--infoWarnBg);
color: var(--infoWarnFg); color: var(--infoWarnFg);
border-radius: var(--radius);
overflow: clip;
> .link { > .link {
margin-left: 4px; margin-left: 4px;

View File

@@ -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>

View File

@@ -30,10 +30,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkRadio from '@/components/form/radio.vue'; import MkRadio from '@/components/MkRadio.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as config from '@/config'; import * as config from '@/config';

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="vblkjoeq"> <div class="vblkjoeq">
<div class="label" @click="focus"><slot name="label"></slot></div> <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> <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
<select <select
ref="inputEl" ref="inputEl"
@@ -118,7 +118,7 @@ onMounted(() => {
}); });
}); });
const onClick = (ev: MouseEvent) => { function show(ev: MouseEvent) {
focused.value = true; focused.value = true;
opening.value = true; opening.value = true;
@@ -166,7 +166,7 @@ const onClick = (ev: MouseEvent) => {
}).then(() => { }).then(() => {
focused.value = false; focused.value = false;
}); });
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -285,7 +285,7 @@ const onClick = (ev: MouseEvent) => {
<style lang="scss" module> <style lang="scss" module>
.chevron { .chevron {
transition: transform 0.5s ease; transition: transform 0.1s ease-out;
} }
.chevronOpening { .chevronOpening {

View File

@@ -53,7 +53,7 @@ import { defineAsyncComponent } from 'vue';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import { showSuspendedDialog } from '../scripts/show-suspended-dialog'; import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import MkButton from '@/components/MkButton.vue'; 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 MkInfo from '@/components/MkInfo.vue';
import { apiUrl, host as configHost } from '@/config'; import { apiUrl, host as configHost } from '@/config';
import { byteify, hexify } from '@/scripts/2fa'; import { byteify, hexify } from '@/scripts/2fa';

View File

@@ -69,8 +69,8 @@ import { } from 'vue';
import getPasswordStrength from 'syuilo-password-strength'; import getPasswordStrength from 'syuilo-password-strength';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import MkInput from './form/input.vue'; import MkInput from './MkInput.vue';
import MkSwitch from './form/switch.vue'; import MkSwitch from './MkSwitch.vue';
import MkCaptcha from '@/components/MkCaptcha.vue'; import MkCaptcha from '@/components/MkCaptcha.vue';
import * as config from '@/config'; import * as config from '@/config';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -4,7 +4,7 @@
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <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> <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> <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> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div> </div>
<details v-if="note.files.length > 0"> <details v-if="note.files.length > 0">

View File

@@ -73,9 +73,9 @@ const toggle = () => {
width: 32px; width: 32px;
height: 23px; height: 23px;
outline: none; outline: none;
background: var(--swutchOffBg); background: var(--switchOffBg);
background-clip: content-box; background-clip: content-box;
border: solid 1px var(--swutchOffBg); border: solid 1px var(--switchOffBg);
border-radius: 999px; border-radius: 999px;
cursor: pointer; cursor: pointer;
transition: inherit; transition: inherit;
@@ -87,7 +87,7 @@ const toggle = () => {
left: 3px; left: 3px;
width: 15px; width: 15px;
height: 15px; height: 15px;
background: var(--swutchOffFg); background: var(--switchOffFg);
border-radius: 999px; border-radius: 999px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@@ -131,12 +131,12 @@ const toggle = () => {
&.checked { &.checked {
> .button { > .button {
background-color: var(--swutchOnBg) !important; background-color: var(--switchOnBg) !important;
border-color: var(--swutchOnBg) !important; border-color: var(--switchOnBg) !important;
> .knob { > .knob {
left: 12px; left: 12px;
background: var(--swutchOnFg); background: var(--switchOnFg);
} }
} }
} }

View File

@@ -36,8 +36,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import { permissions as kinds } from 'misskey-js'; import { permissions as kinds } from 'misskey-js';
import MkInput from './form/input.vue'; import MkInput from './MkInput.vue';
import MkSwitch from './form/switch.vue'; import MkSwitch from './MkSwitch.vue';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import MkInfo from './MkInfo.vue'; import MkInfo from './MkInfo.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';

View File

@@ -54,7 +54,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted } from 'vue'; import { nextTick, onMounted } from 'vue';
import * as misskey from 'misskey-js'; 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 FormSplit from '@/components/form/split.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -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>

View File

@@ -45,7 +45,7 @@ export type DefaultStoredWidget = {
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, reactive, ref, computed } from 'vue'; import { defineAsyncComponent, reactive, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid'; 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 MkButton from '@/components/MkButton.vue';
import { widgets as widgetDefs } from '@/widgets'; import { widgets as widgetDefs } from '@/widgets';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -61,8 +61,8 @@ function dragClear(fn) {
} }
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
initialWidth?: number; initialWidth: number;
initialHeight?: number | null; initialHeight: number | null;
canResize?: boolean; canResize?: boolean;
closeButton?: boolean; closeButton?: boolean;
mini?: boolean; mini?: boolean;
@@ -386,7 +386,7 @@ function onBrowserResize() {
} }
onMounted(() => { onMounted(() => {
if (props.initialWidth) applyTransformWidth(props.initialWidth); applyTransformWidth(props.initialWidth);
if (props.initialHeight) applyTransformHeight(props.initialHeight); if (props.initialHeight) applyTransformHeight(props.initialHeight);
applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2)); applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2));

View File

@@ -38,13 +38,13 @@ const forceSpacerMin = inject('forceSpacerMin', false) || deviceKind === 'smartp
container-type: inline-size; container-type: inline-size;
} }
@container (max-width: 360px) { @container (max-width: 450px) {
.root { .root {
padding: v-bind('props.marginMin + "px"'); padding: v-bind('props.marginMin + "px"');
} }
} }
@container (min-width: 361px) { @container (min-width: 451px) {
.root { .root {
padding: v-bind('props.marginMax + "px"'); padding: v-bind('props.marginMax + "px"');
} }

View File

@@ -8,7 +8,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../form/input.vue'; import MkInput from '../MkInput.vue';
import * as os from '@/os'; import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator'; import { Hpml } from '@/scripts/hpml/evaluator';
import { NumberInputVarBlock } from '@/scripts/hpml/block'; import { NumberInputVarBlock } from '@/scripts/hpml/block';

View File

@@ -10,7 +10,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import MkTextarea from '../form/textarea.vue'; import MkTextarea from '../MkTextarea.vue';
import MkButton from '../MkButton.vue'; import MkButton from '../MkButton.vue';
import { apiUrl } from '@/config'; import { apiUrl } from '@/config';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -7,7 +7,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkRadio from '../form/radio.vue'; import MkRadio from '../MkRadio.vue';
import * as os from '@/os'; import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator'; import { Hpml } from '@/scripts/hpml/evaluator';
import { RadioButtonVarBlock } from '@/scripts/hpml/block'; import { RadioButtonVarBlock } from '@/scripts/hpml/block';

View File

@@ -6,7 +6,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkSwitch from '../form/switch.vue'; import MkSwitch from '../MkSwitch.vue';
import * as os from '@/os'; import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator'; import { Hpml } from '@/scripts/hpml/evaluator';
import { SwitchVarBlock } from '@/scripts/hpml/block'; import { SwitchVarBlock } from '@/scripts/hpml/block';

View File

@@ -8,7 +8,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../form/input.vue'; import MkInput from '../MkInput.vue';
import * as os from '@/os'; import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator'; import { Hpml } from '@/scripts/hpml/evaluator';
import { TextInputVarBlock } from '@/scripts/hpml/block'; import { TextInputVarBlock } from '@/scripts/hpml/block';

View File

@@ -8,7 +8,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from 'vue'; import { computed, defineComponent, PropType } from 'vue';
import MkTextarea from '../form/textarea.vue'; import MkTextarea from '../MkTextarea.vue';
import * as os from '@/os'; import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator'; import { Hpml } from '@/scripts/hpml/evaluator';
import { HpmlTextInput } from '@/scripts/hpml'; import { HpmlTextInput } from '@/scripts/hpml';

View File

@@ -6,7 +6,7 @@
import { TextBlock } from '@/scripts/hpml/block'; import { TextBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator'; import { Hpml } from '@/scripts/hpml/evaluator';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import MkTextarea from '../form/textarea.vue'; import MkTextarea from '../MkTextarea.vue';
export default defineComponent({ export default defineComponent({
components: { components: {

View File

@@ -1,3 +1,5 @@
import { miLocalStorage } from "./local-storage";
const address = new URL(location.href); const address = new URL(location.href);
const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content; 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 url = address.origin;
export const apiUrl = url + '/api'; export const apiUrl = url + '/api';
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; 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 langs = _LANGS_;
export const locale = JSON.parse(localStorage.getItem('locale')); export const locale = JSON.parse(miLocalStorage.getItem('locale'));
export const version = _VERSION_; export const version = _VERSION_;
export const instanceName = siteName === 'Misskey' ? host : siteName; export const instanceName = siteName === 'Misskey' ? host : siteName;
export const ui = localStorage.getItem('ui'); export const ui = miLocalStorage.getItem('ui');
export const debug = localStorage.getItem('debug') === 'true'; export const debug = miLocalStorage.getItem('debug') === 'true';

View File

@@ -9,9 +9,12 @@ import '@/style.scss';
//#region account indexedDB migration //#region account indexedDB migration
import { set } from '@/scripts/idb-proxy'; import { set } from '@/scripts/idb-proxy';
if (localStorage.getItem('accounts') != null) { {
set('accounts', JSON.parse(localStorage.getItem('accounts'))); const accounts = miLocalStorage.getItem('accounts');
localStorage.removeItem('accounts'); if (accounts) {
set('accounts', JSON.parse(accounts));
miLocalStorage.removeItem('accounts');
}
} }
//#endregion //#endregion
@@ -40,6 +43,7 @@ import { reloadChannel } from '@/scripts/unison-reload';
import { reactionPicker } from '@/scripts/reaction-picker'; import { reactionPicker } from '@/scripts/reaction-picker';
import { getUrlWithoutLoginId } from '@/scripts/login-id'; import { getUrlWithoutLoginId } from '@/scripts/login-id';
import { getAccountFromId } from '@/scripts/get-account-from-id'; import { getAccountFromId } from '@/scripts/get-account-from-id';
import { miLocalStorage } from './local-storage';
(async () => { (async () => {
console.info(`Misskey v${version}`); console.info(`Misskey v${version}`);
@@ -154,7 +158,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
const fetchInstanceMetaPromise = fetchInstance(); const fetchInstanceMetaPromise = fetchInstance();
fetchInstanceMetaPromise.then(() => { fetchInstanceMetaPromise.then(() => {
localStorage.setItem('v', instance.version); miLocalStorage.setItem('v', instance.version);
// Init service worker // Init service worker
initializeSw(); 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) { if (lastVersion !== version) {
localStorage.setItem('lastVersion', version); miLocalStorage.setItem('lastVersion', version);
// テーマリビルドするため // テーマリビルドするため
localStorage.removeItem('theme'); miLocalStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) { if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
@@ -244,7 +248,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため) // NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
watch(defaultStore.reactiveState.darkMode, (darkMode) => { watch(defaultStore.reactiveState.darkMode, (darkMode) => {
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
}, { immediate: localStorage.theme == null }); }, { immediate: miLocalStorage.getItem('theme') == null });
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); 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) { if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10); 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) { if ('Notification' in window) {
// 許可を得ていなかったらリクエスト // 許可を得ていなかったらリクエスト

View File

@@ -1,10 +1,11 @@
import { computed, reactive } from 'vue'; import { computed, reactive } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { api } from './os'; import { api } from './os';
import { miLocalStorage } from './local-storage';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
const instanceData = localStorage.getItem('instance'); const instanceData = miLocalStorage.getItem('instance');
// TODO: instanceをリアクティブにするかは再考の余地あり // TODO: instanceをリアクティブにするかは再考の余地あり
@@ -21,7 +22,7 @@ export async function fetchInstance() {
instance[k] = v; instance[k] = v;
} }
localStorage.setItem('instance', JSON.stringify(instance)); miLocalStorage.setItem('instance', JSON.stringify(instance));
} }
export const emojiCategories = computed(() => { export const emojiCategories = computed(() => {

View 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),
};

View File

@@ -5,6 +5,7 @@ import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { ui } from '@/config'; import { ui } from '@/config';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import { miLocalStorage } from './local-storage';
export const navbarItemDef = reactive({ export const navbarItemDef = reactive({
notifications: { notifications: {
@@ -110,21 +111,21 @@ export const navbarItemDef = reactive({
text: i18n.ts.default, text: i18n.ts.default,
active: ui === 'default' || ui === null, active: ui === 'default' || ui === null,
action: () => { action: () => {
localStorage.setItem('ui', 'default'); miLocalStorage.setItem('ui', 'default');
unisonReload(); unisonReload();
}, },
}, { }, {
text: i18n.ts.deck, text: i18n.ts.deck,
active: ui === 'deck', active: ui === 'deck',
action: () => { action: () => {
localStorage.setItem('ui', 'deck'); miLocalStorage.setItem('ui', 'deck');
unisonReload(); unisonReload();
}, },
}, { }, {
text: i18n.ts.classic, text: i18n.ts.classic,
active: ui === 'classic', active: ui === 'classic',
action: () => { action: () => {
localStorage.setItem('ui', 'classic'); miLocalStorage.setItem('ui', 'classic');
unisonReload(); unisonReload();
}, },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);

View File

@@ -26,6 +26,7 @@ import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload'; import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { miLocalStorage } from '@/local-storage';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
error?: Error; error?: Error;
@@ -42,7 +43,7 @@ os.api('meta', {
loaded = true; loaded = true;
serverIsDead = false; serverIsDead = false;
meta = res; meta = res;
localStorage.setItem('v', res.version); miLocalStorage.setItem('v', res.version);
}, () => { }, () => {
loaded = true; loaded = true;
serverIsDead = true; serverIsDead = true;

View File

@@ -32,8 +32,8 @@
import { defineComponent, computed } from 'vue'; import { defineComponent, computed } from 'vue';
import XEmoji from './emojis.emoji.vue'; import XEmoji from './emojis.emoji.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkTab from '@/components/MkTab.vue'; import MkTab from '@/components/MkTab.vue';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -47,8 +47,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';

View File

@@ -64,7 +64,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import MkButton from '@/components/MkButton.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 MkObjectView from '@/components/MkObjectView.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';

View File

@@ -50,8 +50,8 @@
import { computed } from 'vue'; import { computed } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import XAbuseReport from '@/components/MkAbuseReport.vue'; import XAbuseReport from '@/components/MkAbuseReport.vue';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -11,12 +11,12 @@
<MkInput v-model="ad.imageUrl"> <MkInput v-model="ad.imageUrl">
<template #label>{{ i18n.ts.imageUrl }}</template> <template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput> </MkInput>
<FormRadios v-model="ad.place"> <MkRadios v-model="ad.place">
<template #label>Form</template> <template #label>Form</template>
<option value="square">square</option> <option value="square">square</option>
<option value="horizontal">horizontal</option> <option value="horizontal">horizontal</option>
<option value="horizontal-big">horizontal-big</option> <option value="horizontal-big">horizontal-big</option>
</FormRadios> </MkRadios>
<!-- <!--
<div style="margin: 32px 0;"> <div style="margin: 32px 0;">
{{ i18n.ts.priority }} {{ i18n.ts.priority }}
@@ -50,9 +50,9 @@
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import FormRadios from '@/components/form/radios.vue'; import MkRadios from '@/components/MkRadios.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';

View File

@@ -30,8 +30,8 @@
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';

View File

@@ -2,50 +2,50 @@
<div> <div>
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_gaps_m"> <div class="_gaps_m">
<FormRadios v-model="provider"> <MkRadios v-model="provider">
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option> <option value="hcaptcha">hCaptcha</option>
<option value="recaptcha">reCAPTCHA</option> <option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option> <option value="turnstile">Turnstile</option>
</FormRadios> </MkRadios>
<template v-if="provider === 'hcaptcha'"> <template v-if="provider === 'hcaptcha'">
<FormInput v-model="hcaptchaSiteKey"> <MkInput v-model="hcaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</FormInput> </MkInput>
<FormInput v-model="hcaptchaSecretKey"> <MkInput v-model="hcaptchaSecretKey">
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</FormInput> </MkInput>
<FormSlot> <FormSlot>
<template #label>{{ i18n.ts.preview }}</template> <template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
</FormSlot> </FormSlot>
</template> </template>
<template v-else-if="provider === 'recaptcha'"> <template v-else-if="provider === 'recaptcha'">
<FormInput v-model="recaptchaSiteKey"> <MkInput v-model="recaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template> <template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</FormInput> </MkInput>
<FormInput v-model="recaptchaSecretKey"> <MkInput v-model="recaptchaSecretKey">
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template> <template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</FormInput> </MkInput>
<FormSlot v-if="recaptchaSiteKey"> <FormSlot v-if="recaptchaSiteKey">
<template #label>{{ i18n.ts.preview }}</template> <template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/> <MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
</FormSlot> </FormSlot>
</template> </template>
<template v-else-if="provider === 'turnstile'"> <template v-else-if="provider === 'turnstile'">
<FormInput v-model="turnstileSiteKey"> <MkInput v-model="turnstileSiteKey">
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template> <template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</FormInput> </MkInput>
<FormInput v-model="turnstileSecretKey"> <MkInput v-model="turnstileSecretKey">
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template> <template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</FormInput> </MkInput>
<FormSlot> <FormSlot>
<template #label>{{ i18n.ts.preview }}</template> <template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/> <MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/>
@@ -60,8 +60,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import FormRadios from '@/components/form/radios.vue'; import MkRadios from '@/components/MkRadios.vue';
import FormInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormSlot from '@/components/form/slot.vue'; import FormSlot from '@/components/form/slot.vue';

View File

@@ -4,41 +4,41 @@
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_gaps_m"> <div class="_gaps_m">
<FormSwitch v-model="enableEmail"> <MkSwitch v-model="enableEmail">
<template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template> <template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template> <template #caption>{{ i18n.ts.emailConfigInfo }}</template>
</FormSwitch> </MkSwitch>
<template v-if="enableEmail"> <template v-if="enableEmail">
<FormInput v-model="email" type="email"> <MkInput v-model="email" type="email">
<template #label>{{ i18n.ts.emailAddress }}</template> <template #label>{{ i18n.ts.emailAddress }}</template>
</FormInput> </MkInput>
<FormSection> <FormSection>
<template #label>{{ i18n.ts.smtpConfig }}</template> <template #label>{{ i18n.ts.smtpConfig }}</template>
<div class="_gaps_m"> <div class="_gaps_m">
<FormSplit :min-width="280"> <FormSplit :min-width="280">
<FormInput v-model="smtpHost"> <MkInput v-model="smtpHost">
<template #label>{{ i18n.ts.smtpHost }}</template> <template #label>{{ i18n.ts.smtpHost }}</template>
</FormInput> </MkInput>
<FormInput v-model="smtpPort" type="number"> <MkInput v-model="smtpPort" type="number">
<template #label>{{ i18n.ts.smtpPort }}</template> <template #label>{{ i18n.ts.smtpPort }}</template>
</FormInput> </MkInput>
</FormSplit> </FormSplit>
<FormSplit :min-width="280"> <FormSplit :min-width="280">
<FormInput v-model="smtpUser"> <MkInput v-model="smtpUser">
<template #label>{{ i18n.ts.smtpUser }}</template> <template #label>{{ i18n.ts.smtpUser }}</template>
</FormInput> </MkInput>
<FormInput v-model="smtpPass" type="password"> <MkInput v-model="smtpPass" type="password">
<template #label>{{ i18n.ts.smtpPass }}</template> <template #label>{{ i18n.ts.smtpPass }}</template>
</FormInput> </MkInput>
</FormSplit> </FormSplit>
<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo> <FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<FormSwitch v-model="smtpSecure"> <MkSwitch v-model="smtpSecure">
<template #label>{{ i18n.ts.smtpSecure }}</template> <template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template> <template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
</FormSwitch> </MkSwitch>
</div> </div>
</FormSection> </FormSection>
</template> </template>
@@ -51,8 +51,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import FormInfo from '@/components/MkInfo.vue'; import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';

View File

@@ -32,7 +32,7 @@
import { } from 'vue'; import { } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.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 * as os from '@/os';
import { unique } from '@/scripts/array'; import { unique } from '@/scripts/array';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';

View File

@@ -71,10 +71,10 @@
import { computed, defineAsyncComponent, defineComponent, ref, shallowRef } from 'vue'; import { computed, defineAsyncComponent, defineComponent, ref, shallowRef } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.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 MkPagination from '@/components/MkPagination.vue';
import MkTab from '@/components/MkTab.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 FormSplit from '@/components/form/split.vue';
import { selectFile, selectFiles } from '@/scripts/select-file'; import { selectFile, selectFiles } from '@/scripts/select-file';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -37,8 +37,8 @@ import { computed, defineAsyncComponent } from 'vue';
import * as Acct from 'misskey-js/built/acct'; import * as Acct from 'misskey-js/built/acct';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as os from '@/os'; import * as os from '@/os';

View File

@@ -3,10 +3,10 @@
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormTextarea v-model="blockedHosts"> <MkTextarea v-model="blockedHosts">
<span>{{ i18n.ts.blockedInstances }}</span> <span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</FormTextarea> </MkTextarea>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</FormSuspense> </FormSuspense>
@@ -18,7 +18,7 @@
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.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 FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os'; import * as os from '@/os';
import { fetchInstance } from '@/instance'; import { fetchInstance } from '@/instance';

View File

@@ -1,22 +1,22 @@
<template> <template>
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_gaps_m"> <div class="_gaps_m">
<FormSwitch v-model="enableDiscordIntegration"> <MkSwitch v-model="enableDiscordIntegration">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</FormSwitch> </MkSwitch>
<template v-if="enableDiscordIntegration"> <template v-if="enableDiscordIntegration">
<FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo> <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 #prefix><i class="ti ti-key"></i></template>
<template #label>Client ID</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 #prefix><i class="ti ti-key"></i></template>
<template #label>Client Secret</template> <template #label>Client Secret</template>
</FormInput> </MkInput>
</template> </template>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
@@ -26,8 +26,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue'; import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';

View File

@@ -1,22 +1,22 @@
<template> <template>
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_gaps_m"> <div class="_gaps_m">
<FormSwitch v-model="enableGithubIntegration"> <MkSwitch v-model="enableGithubIntegration">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</FormSwitch> </MkSwitch>
<template v-if="enableGithubIntegration"> <template v-if="enableGithubIntegration">
<FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo> <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 #prefix><i class="ti ti-key"></i></template>
<template #label>Client ID</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 #prefix><i class="ti ti-key"></i></template>
<template #label>Client Secret</template> <template #label>Client Secret</template>
</FormInput> </MkInput>
</template> </template>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
@@ -26,8 +26,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue'; import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';

View File

@@ -1,22 +1,22 @@
<template> <template>
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_gaps_m"> <div class="_gaps_m">
<FormSwitch v-model="enableTwitterIntegration"> <MkSwitch v-model="enableTwitterIntegration">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</FormSwitch> </MkSwitch>
<template v-if="enableTwitterIntegration"> <template v-if="enableTwitterIntegration">
<FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo> <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 #prefix><i class="ti ti-key"></i></template>
<template #label>Consumer Key</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 #prefix><i class="ti ti-key"></i></template>
<template #label>Consumer Secret</template> <template #label>Consumer Secret</template>
</FormInput> </MkInput>
</template> </template>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
@@ -26,8 +26,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import FormSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue'; import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';

View File

@@ -4,63 +4,63 @@
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init"> <FormSuspense :p="init">
<div class="_gaps_m"> <div class="_gaps_m">
<FormSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</FormSwitch> <MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
<template v-if="useObjectStorage"> <template v-if="useObjectStorage">
<FormInput v-model="objectStorageBaseUrl"> <MkInput v-model="objectStorageBaseUrl">
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template> <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template> <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</FormInput> </MkInput>
<FormInput v-model="objectStorageBucket"> <MkInput v-model="objectStorageBucket">
<template #label>{{ i18n.ts.objectStorageBucket }}</template> <template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template> <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</FormInput> </MkInput>
<FormInput v-model="objectStoragePrefix"> <MkInput v-model="objectStoragePrefix">
<template #label>{{ i18n.ts.objectStoragePrefix }}</template> <template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template> <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</FormInput> </MkInput>
<FormInput v-model="objectStorageEndpoint"> <MkInput v-model="objectStorageEndpoint">
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template> <template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template> <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</FormInput> </MkInput>
<FormInput v-model="objectStorageRegion"> <MkInput v-model="objectStorageRegion">
<template #label>{{ i18n.ts.objectStorageRegion }}</template> <template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template> <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</FormInput> </MkInput>
<FormSplit :min-width="280"> <FormSplit :min-width="280">
<FormInput v-model="objectStorageAccessKey"> <MkInput v-model="objectStorageAccessKey">
<template #prefix><i class="ti ti-key"></i></template> <template #prefix><i class="ti ti-key"></i></template>
<template #label>Access key</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 #prefix><i class="ti ti-key"></i></template>
<template #label>Secret key</template> <template #label>Secret key</template>
</FormInput> </MkInput>
</FormSplit> </FormSplit>
<FormSwitch v-model="objectStorageUseSSL"> <MkSwitch v-model="objectStorageUseSSL">
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template> <template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template> <template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</FormSwitch> </MkSwitch>
<FormSwitch v-model="objectStorageUseProxy"> <MkSwitch v-model="objectStorageUseProxy">
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template> <template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template> <template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</FormSwitch> </MkSwitch>
<FormSwitch v-model="objectStorageSetPublicRead"> <MkSwitch v-model="objectStorageSetPublicRead">
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template> <template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</FormSwitch> </MkSwitch>
<FormSwitch v-model="objectStorageS3ForcePathStyle"> <MkSwitch v-model="objectStorageS3ForcePathStyle">
<template #label>s3ForcePathStyle</template> <template #label>s3ForcePathStyle</template>
</FormSwitch> </MkSwitch>
</template> </template>
</div> </div>
</FormSuspense> </FormSuspense>
@@ -71,8 +71,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormInput from '@/components/form/input.vue'; import MkInput from '@/components/MkInput.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';

View File

@@ -104,6 +104,10 @@ async function renderChart() {
time: { time: {
stepSize: 1, stepSize: 1,
unit: 'day', unit: 'day',
displayFormats: {
day: 'M/d',
month: 'Y/M',
},
}, },
grid: { grid: {
display: false, display: false,

View File

@@ -214,6 +214,10 @@ onMounted(async () => {
time: { time: {
stepSize: 1, stepSize: 1,
unit: 'day', unit: 'day',
displayFormats: {
day: 'M/d',
month: 'Y/M',
},
}, },
grid: { grid: {
display: false, display: false,
@@ -223,11 +227,6 @@ onMounted(async () => {
maxRotation: 0, maxRotation: 0,
autoSkipPadding: 16, autoSkipPadding: 16,
}, },
adapters: {
date: {
locale: enUS,
},
},
min: getDate(chartLimit).getTime(), min: getDate(chartLimit).getTime(),
}, },
y: { y: {

View File

@@ -13,7 +13,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import MkHeatmap from '@/components/MkHeatmap.vue'; import MkHeatmap from '@/components/MkHeatmap.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/MkSelect.vue';
let src = $ref('active-users'); let src = $ref('active-users');
</script> </script>

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