Compare commits

..

108 Commits

Author SHA1 Message Date
syuilo
b796aacf7f Merge branch 'develop' 2022-07-16 23:53:24 +09:00
syuilo
a974ab00d7 12.116.0 2022-07-16 23:53:15 +09:00
syuilo
4352331b70 New Crowdin updates (#9007)
* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Chinese Traditional)
2022-07-16 23:53:01 +09:00
syuilo
e4453e9ca8 Update 1651224615271-foreign-key.js
#8852
2022-07-16 23:51:32 +09:00
syuilo
3811b90150 12.116.0-beta.1 2022-07-16 23:11:08 +09:00
syuilo
003f592ef6 feat(client): registry editor 2022-07-16 23:11:05 +09:00
syuilo
a3f3ef4226 chore(client): tweak style 2022-07-16 22:16:01 +09:00
syuilo
fbb0cc686e chore(client): tweak style 2022-07-16 20:53:53 +09:00
syuilo
c1c8c9c37c fix(server): fix bind of method
Fix #9006
2022-07-16 19:03:41 +09:00
syuilo
ff24811676 Merge branch 'develop' 2022-07-16 18:21:44 +09:00
syuilo
ab3bc4a982 12.115.0 2022-07-16 18:21:29 +09:00
syuilo
b6e4ec7056 New Crowdin updates (#9004)
* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

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

* New translations ja-JP.yml (Chinese Simplified)
2022-07-16 18:19:38 +09:00
Kainoa Kanter
5619a3390d chore: improve ad style (#8995)
* Improve ad style

* 🎨

* `ad` -> `info`
2022-07-16 17:50:12 +09:00
syuilo
e9fd064624 chore(client): tweak user-info routing 2022-07-16 16:52:12 +09:00
syuilo
274bfc965f 12.115.0-beta.1 2022-07-16 15:56:27 +09:00
syuilo
672ceb8687 chore(client): tweak style 2022-07-16 15:19:52 +09:00
syuilo
a3a9b7fbd3 chore(client): tweak deck ui 2022-07-16 15:19:44 +09:00
syuilo
57e533a5ef chore(client): tweak ui 2022-07-16 13:49:23 +09:00
syuilo
4f9b03a997 refactor(client): use setup syntax 2022-07-16 13:14:16 +09:00
syuilo
01d07edfe3 chore(client): tweak style 2022-07-16 13:13:58 +09:00
syuilo
4c8a1867f0 Merge branch 'develop' 2022-07-15 22:45:13 +09:00
syuilo
7d63118941 12.114.0 2022-07-15 22:44:53 +09:00
syuilo
aa5eab746a New Crowdin updates (#9001)
* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Kabyle)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)
2022-07-15 22:43:42 +09:00
syuilo
3d0870f414 12.114.0-beta.8 2022-07-15 22:10:02 +09:00
syuilo
92e8a5dbd6 chore(client): tweak ui 2022-07-15 22:09:05 +09:00
syuilo
113df68843 chore(client): tweak style 2022-07-15 19:31:04 +09:00
syuilo
92792719bd chore(client): tweak style 2022-07-15 19:15:23 +09:00
syuilo
47186c0fff 12.114.0-beta.7 2022-07-15 17:14:41 +09:00
syuilo
eb73a8137d Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2022-07-15 17:14:29 +09:00
Johann150
7cb5b5c8c2 refactor: signup component as composition api (#8957) 2022-07-15 17:14:05 +09:00
syuilo
6f45208ab6 enhance(client): RSSティッカーで表示順序をシャッフルできるように 2022-07-15 17:12:08 +09:00
syuilo
165c4b2c00 chore(client): tweak ui 2022-07-15 17:01:13 +09:00
syuilo
a4b5a0072d move rollup to devDeps 2022-07-14 23:54:52 +09:00
syuilo
ddc899938a chore(client): improve usability 2022-07-14 23:50:07 +09:00
tamaina
77c2a7cd71 refactor(client): remove useCssModule (#8999)
* refactor(client): remove useCssModule()

* use MkStickyContainer

* Revert "use MkStickyContainer"

This reverts commit 639746786b.
2022-07-14 23:32:00 +09:00
tamaina
de24131993 use MkStickyContainer (#9000) 2022-07-14 23:31:01 +09:00
syuilo
7b7fe019c0 Update CHANGELOG.md 2022-07-14 23:24:10 +09:00
syuilo
c30ffec1af 12.114.0-beta.6 2022-07-14 22:26:02 +09:00
syuilo
44f560b453 tweak boot.js 2022-07-14 22:25:57 +09:00
syuilo
5fa8c62305 12.114.0-beta.5 2022-07-14 22:15:08 +09:00
syuilo
17500fc9c9 Revert "fix(client): revert es2017"
This reverts commit 0ddabdbf68.
2022-07-14 22:14:42 +09:00
syuilo
e3bad795e0 Revert "debug"
This reverts commit fa5140310f.
2022-07-14 22:13:04 +09:00
syuilo
1ba559a98b Revert "debug"
This reverts commit 800bbc4328.
2022-07-14 22:13:00 +09:00
syuilo
3053767c71 12.114.0-beta.4 2022-07-14 22:05:03 +09:00
syuilo
f2e91f4d62 tweak boot.js 2022-07-14 22:04:16 +09:00
syuilo
10f4815d34 tweak boot.js 2022-07-14 21:52:58 +09:00
syuilo
d43eb123b1 12.114.0-beta.3 2022-07-14 21:40:55 +09:00
syuilo
800bbc4328 debug 2022-07-14 21:40:30 +09:00
syuilo
7a43cac6b3 12.114.0-beta.2 2022-07-14 21:32:33 +09:00
syuilo
fa5140310f debug 2022-07-14 21:32:21 +09:00
syuilo
1dec3461cd 12.114.0-beta.1 2022-07-14 21:06:38 +09:00
syuilo
0ddabdbf68 fix(client): revert es2017 2022-07-14 21:06:07 +09:00
syuilo
d78faf1134 chore: use tab 2022-07-14 21:02:45 +09:00
syuilo
cb00786f1e improve error handling of client boot 2022-07-14 20:41:17 +09:00
syuilo
61e26696aa log error while client boot 2022-07-14 20:29:45 +09:00
syuilo
f3b0c6f1e7 chore(client): tweak ui 2022-07-14 17:52:59 +09:00
syuilo
242538ddce refactor(client): rename menu(sidebar) -> navbar 2022-07-14 17:42:12 +09:00
syuilo
64be9baed0 chore(client): tweak style 2022-07-14 17:22:06 +09:00
syuilo
bce48dfee9 Merge branch 'develop' 2022-07-13 21:59:47 +09:00
syuilo
e365139961 12.113.0 2022-07-13 21:59:39 +09:00
syuilo
719c438b41 New Crowdin updates (#8959)
* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

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

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Catalan)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Russian)

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

* New translations ja-JP.yml (Chinese Traditional)
2022-07-13 21:59:08 +09:00
syuilo
5948a9da0f 12.113.0-beta.3 2022-07-13 21:46:39 +09:00
syuilo
db21be3282 chore(client): trust ios 2022-07-13 21:41:06 +09:00
syuilo
096b306bc5 lit fixes 2022-07-13 21:39:16 +09:00
syuilo
e30a03b5c4 update deps 2022-07-13 21:37:48 +09:00
syuilo
21d22200a3 update vite 2022-07-13 21:29:38 +09:00
Andreas Nedbal
53227d76d6 fix(client): hide bot protection warning with disabled registrations (#8794)
* fix(client): hide bot protection warning with disabled registrations

* Apply review suggestion from @Johann150

Co-authored-by: Johann150 <johann@qwertqwefsday.eu>

Co-authored-by: Johann150 <johann@qwertqwefsday.eu>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2022-07-13 21:23:20 +09:00
syuilo
9cc08aebbe chore(dev): add note 2022-07-13 21:19:27 +09:00
Johann150
ee9bb8286e fix: add es2017 build target (#8931)
* remove top level awaits

* add es2017 target

* refactor: use setup and ref sugar
2022-07-13 21:17:19 +09:00
tamaina
ae92378689 fix(sw, notification): Don't issue an event if there is no affect (#8979)
* test

* ]v]
2022-07-13 21:07:49 +09:00
Johann150
714c80bf3f enhance: read theme color nodeinfo (#8977)
* provide theme color in nodeinfo metadata

* read theme color from nodeinfo

Prefer to read the theme color from the nodeinfo since it is more
performant than performing selector search on a DOM.
2022-07-13 21:06:24 +09:00
nvisser
947f079735 Make tensorflow an optinal dependency (#8986) 2022-07-13 21:00:59 +09:00
syuilo
c771135fd8 chore(client): fix routing 2022-07-13 18:28:04 +09:00
syuilo
ad970dffda fix(client): fix url encoded string handling of nirax
Fix #8878
2022-07-13 18:11:54 +09:00
syuilo
dd4f7be3da 🎨 2022-07-13 18:09:47 +09:00
syuilo
0bcfa2d04f chore(client): tweak explore page 2022-07-13 18:09:41 +09:00
syuilo
170b1b89ba chore(client): fix type def 2022-07-13 16:33:52 +09:00
syuilo
b08ead1dce chore(client): fix mention style 2022-07-13 16:33:39 +09:00
syuilo
05a342009f 🎨 2022-07-13 16:33:18 +09:00
syuilo
d7247e2db2 chore(client): tweak ui 2022-07-12 22:42:50 +09:00
syuilo
4550a4459b feat: support <plain> syntax for mfm 2022-07-12 12:03:38 +09:00
MeiMei
660781afd9 Fix crash at startup if TensorFlow is not supported (#8984)
* Lazy loading tensorflow

* CHANGELOG

* CHANGELOG

* Check CPU flags

* .
2022-07-12 10:38:57 +09:00
syuilo
1557d0afb8 12.113.0-beta.2 2022-07-11 23:36:55 +09:00
syuilo
d8264b11e2 enhance(client): improve widgets component 2022-07-11 23:36:39 +09:00
syuilo
a5648fb07f re: update mfm-js 0.23.0-canary.1 2022-07-11 23:13:23 +09:00
syuilo
43316ec355 12.113.0-beta.1 2022-07-10 21:11:03 +09:00
syuilo
18206e3cf6 update mfm-js 0.23.0-canary.1 2022-07-10 21:09:37 +09:00
yzhe819
9107de63b4 chore: fix lint errors (#8981) 2022-07-10 19:47:29 +09:00
syuilo
bbe4824955 enhance(client): update themes 2022-07-10 15:36:02 +09:00
syuilo
e7f4ec72b4 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2022-07-10 15:35:45 +09:00
syuilo
5cdb9fb748 chore(client): tweak style 2022-07-10 15:35:43 +09:00
tamaina
83ebe79a3f enhance(sw): If receiving a push notification issued more than a day, ignore it. (#8980)
* enhance(sw): ignore old push notification

* ✌️

* 半日

* !==

* 1日
2022-07-10 15:15:21 +09:00
syuilo
7f808eaf42 fix(client): fix style of mention 2022-07-10 13:16:11 +09:00
syuilo
fbd6b90bf8 chore(dev): remove duplicated lint rule 2022-07-10 11:14:41 +09:00
dogcraft
d92200a6d6 fix: QueryFailedError when logging user's IPs (#8973)
* fix QueryFailedError when logging user's IPs

* use `orIgnore` to fix
2022-07-10 11:02:46 +09:00
syuilo
c20311b8a7 Merge branch 'develop' 2022-07-09 18:32:55 +09:00
syuilo
111d4d0149 12.112.3 2022-07-09 18:32:45 +09:00
syuilo
49012f8352 update summaly 2022-07-09 18:23:21 +09:00
MeiMei
53e54c22fa Fix Attempts to update all notifications (#8974)
* Fix massive update notification parameters

* CHANGELOG

* CHANGELOG
2022-07-09 18:18:39 +09:00
syuilo
75d516011b enhance: make active email validation configurable 2022-07-09 15:05:55 +09:00
syuilo
522ddba3d7 enhance(server): tweak identicon generation 2022-07-09 13:22:35 +09:00
syuilo
fb14ac50b8 Merge branch 'develop' 2022-07-08 17:34:53 +09:00
syuilo
4ac75243e5 12.112.2 2022-07-08 17:34:44 +09:00
MeiMei
417e75b470 DockerでAlpineではなくDebianを使用するように (#8966)
* Debian

* CHANGELOG

* Update CHANGELOG
2022-07-08 17:17:21 +09:00
syuilo
481783606f Update CHANGELOG.md 2022-07-08 11:27:49 +09:00
syuilo
3b71f985a5 Update CHANGELOG.md 2022-07-08 11:24:12 +09:00
syuilo
9a58b4160b Update CHANGELOG.md 2022-07-08 11:21:14 +09:00
syuilo
207d7e0478 fix test 2022-07-08 11:21:11 +09:00
153 changed files with 3142 additions and 2052 deletions

View File

@@ -9,6 +9,53 @@
You should also include the user name that made the change.
-->
## 12.116.0 (2022/07/16)
### Improvements
- Client: registry editor @syuilo
- Client: UIのブラッシュアップ @syuilo
### Bugfixes
- Error During Migration Run to 12.111.x
- Server: TypeError: Cannot convert undefined or null to object @syuilo
## 12.115.0 (2022/07/16)
### Improvements
- Client: Deckのプロファイル切り替えを簡単に @syuilo
- Client: UIのブラッシュアップ @syuilo
## 12.114.0 (2022/07/15)
### Improvements
- RSSティッカーで表示順序をシャッフルできるように @syuilo
### Bugfixes
- クライアントが起動しなくなることがある問題を修正 @syuilo
## 12.113.0 (2022/07/13)
### Improvements
- Support <plain> syntax for MFM
### Bugfixes
- Server: Fix crash at startup if TensorFlow is not supported @mei23
- Client: URLエンコードされたルーティングを修正
## 12.112.3 (2022/07/09)
### Improvements
- Make active email validation configurable
### Bugfixes
- Server: Fix Attempts to update all notifications @mei23
## 12.112.2 (2022/07/08)
### Bugfixes
- Fix Docker doesn't work @mei23
Still not working on arm64 environment. (See 12.112.0)
## 12.112.1 (2022/07/07)
same as 12.112.0
@@ -21,6 +68,10 @@ same as 12.112.0
- ハイライトがみつけるに統合されました
- カスタム絵文字ページはインスタンス情報ページに統合されました
- 連合ページはインスタンス情報ページに統合されました
- メンション一覧ページは通知一覧ページに統合されました
- ダイレクト投稿一覧ページは通知一覧ページに統合されました
- メニューからアンテナタイムラインを表示する方法は廃止され、タイムライン上部のアイコンからアクセスするようになりました
- メニューからリストタイムラインを表示する方法は廃止され、タイムライン上部のアイコンからアクセスするようになりました
### Improvements
- Server: Allow GET method for some endpoints @syuilo
@@ -28,6 +79,7 @@ same as 12.112.0
- Server: Add rate limit to i/notifications @tamaina
- Client: Improve control panel @syuilo
- Client: Show warning in control panel when there is an unresolved abuse report @syuilo
- Client: Statusbars @syuilo
- Client: Add instance-cloud widget @syuilo
- Client: Add rss-ticker widget @syuilo
- Client: Removing entries from a clip @futchitwo

View File

@@ -1,28 +1,24 @@
FROM node:18.0.0-alpine3.15 AS base
FROM node:16.15.1-bullseye AS builder
ARG NODE_ENV=production
WORKDIR /misskey
ENV BUILD_DEPS autoconf automake file g++ gcc libc-dev libtool make nasm pkgconfig python3 zlib-dev git
FROM base AS builder
COPY . ./
RUN apk add --no-cache $BUILD_DEPS && \
git submodule update --init && \
yarn install && \
yarn build && \
rm -rf .git
RUN apt-get update
RUN apt-get install -y build-essential
RUN git submodule update --init
RUN yarn install
RUN yarn build
RUN rm -rf .git
FROM base AS runner
FROM node:16.15.1-bullseye-slim AS runner
RUN apk add --no-cache \
ffmpeg \
tini
WORKDIR /misskey
ENTRYPOINT ["/sbin/tini", "--"]
RUN apt-get update
RUN apt-get install -y ffmpeg tini
COPY --from=builder /misskey/node_modules ./node_modules
COPY --from=builder /misskey/built ./built
@@ -32,5 +28,5 @@ COPY --from=builder /misskey/packages/client/node_modules ./packages/client/node
COPY . ./
ENV NODE_ENV=production
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["npm", "run", "migrateandstart"]

View File

@@ -804,6 +804,11 @@ oneDay: "يوم"
oneWeek: "أسبوع"
failedToFetchAccountInformation: "تعذر جلب معلومات الحساب"
file: "الملفات"
reverse: "اقلب"
colored: "ملوّن"
label: "التسمية"
localOnly: "المحلي فقط"
account: "الحسابات"
_emailUnavailable:
used: "هذا البريد الإلكتروني مستخدم"
format: "صيغة البريد الإلكتروني غير صالحة"

View File

@@ -844,6 +844,11 @@ reflectMayTakeTime: "এটির কাজ দেখা যেতে কিছ
failedToFetchAccountInformation: "অ্যাকাউন্টের তথ্য উদ্ধার করা যায়নি"
rateLimitExceeded: "রেট লিমিট ছাড়িয়ে গেছে "
file: "ফাইলগুলি"
reverse: "উল্টান"
colored: "রঙ্গিন"
label: "লেবেল"
localOnly: "শুধুমাত্র লোকাল"
account: "অ্যাকাউন্টগুলি"
_emailUnavailable:
used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে"
format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি"

View File

@@ -111,6 +111,14 @@ reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, pr
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
attachCancel: "Eliminar el fitxer adjunt"
markAsSensitive: "Marcar com a NSFW"
unmarkAsSensitive: "Deixar de marcar com a sensible"
enterFileName: "Defineix nom del fitxer"
mute: "Silencia"
unmute: "Deixa de silenciar"
block: "Bloqueja"
unblock: "Desbloqueja"
suspend: "Suspèn"
unsuspend: "Deixa de suspendre"
instances: "Instàncies"
remove: "Eliminar"
nsfw: "NSFW"
@@ -148,6 +156,8 @@ _profile:
username: "Nom d'usuari"
_exportOrImport:
followingList: "Seguint"
muteList: "Silencia"
blockingList: "Bloqueja"
userLists: "Llistes"
_pages:
script:

View File

@@ -869,6 +869,35 @@ logoutConfirm: "Wirklich abmelden?"
lastActiveDate: "Zuletzt verwendet am"
statusbar: "Statusleiste"
pleaseSelect: "Wähle eine Option"
reverse: "Umkehren"
colored: "Farbig"
refreshInterval: "Aktualisierungsrate"
label: "Beschriftung"
type: "Art"
speed: "Geschwindigkeit"
slow: "Langsam"
fast: "Schnell"
sensitiveMediaDetection: "Erkennung von NSFW-Medien"
localOnly: "Nur Lokal"
remoteOnly: "Nur für fremde Instanzen"
failedToUpload: "Hochladen fehlgeschlagen"
cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise NSFW festgestellt wurden."
cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist."
beta: "Beta"
enableAutoSensitive: "NSFW-Automarkierung"
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch NSFW-Markierungen für Medien, die NSFW-Anteile beinhalten. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft."
navbar: "Navigationsleiste"
shuffle: "Mischen"
account: "Benutzerkonto"
_sensitiveMediaDetection:
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
sensitivity: "Erkennungssensitivität"
sensitivityDescription: "Durch das Senken der Sensitivität kann die Anzahl an Fehlerkennungen (sog. false positives) reduziert werden. Durch ein Erhöhen dieser kann die Anzahl an verpassten Erkennungen (sog. false negatives) reduziert werden."
setSensitiveFlagAutomatically: "Als NSFW markieren"
setSensitiveFlagAutomaticallyDescription: "Die Resultate der internen Erkennung werden beibehalten, auch wenn diese Option deaktiviert ist."
analyzeVideos: "Videoanalyse aktivieren"
analyzeVideosDescription: "Analysiert zusätzlich zu Bildern auch Videos. Die Last des Servers wird hierdurch etwas erhöht."
_emailUnavailable:
used: "Diese Email-Adresse wird bereits verwendet"
format: "Das Format dieser Email-Adresse ist ungültig"
@@ -1674,6 +1703,8 @@ _deck:
stackLeft: "Auf linke Spalte stapeln"
popRight: "Nach rechts vom Stapel nehmen"
profile: "Profil"
newProfile: "Neues Profil"
deleteProfile: "Profil löschen"
introduction: "Erstelle eine auf dich zugeschneiderte Benutzeroberfläche durch das Aneinanderreihen von Spalten!"
introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen."
widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu."

View File

@@ -869,6 +869,35 @@ logoutConfirm: "Really log out?"
lastActiveDate: "Last used at"
statusbar: "Status bar"
pleaseSelect: "Select an option"
reverse: "Reverse"
colored: "Colored"
refreshInterval: "Update interval "
label: "Label"
type: "Type"
speed: "Speed"
slow: "Slow"
fast: "Fast"
sensitiveMediaDetection: "Detection of NSFW media"
localOnly: "Local only"
remoteOnly: "Remote only"
failedToUpload: "Upload failed"
cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially NSFW."
cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity."
beta: "Beta"
enableAutoSensitive: "Automatic NSFW-Marking"
enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated."
navbar: "Navigation bar"
shuffle: "Shuffle"
account: "Account"
_sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
sensitivity: "Detection sensitivity"
sensitivityDescription: "Reducing the sensitivity will lead to fewer misdetections (false positives) whereas increasing it will lead to fewer missed detections (false negatives)."
setSensitiveFlagAutomatically: "Mark as NSFW"
setSensitiveFlagAutomaticallyDescription: "The results of the internal detection will be retained even if this option is turned off."
analyzeVideos: "Enable analysis of videos"
analyzeVideosDescription: "Analyzes videos in addition to images. This will slightly increase the load on the server."
_emailUnavailable:
used: "This email address is already being used"
format: "The format of this email address is invalid"
@@ -1674,6 +1703,8 @@ _deck:
stackLeft: "Stack with the left column"
popRight: "Pop column to the right"
profile: "Profile"
newProfile: "New profile"
deleteProfile: "Delete profile"
introduction: "Create the perfect interface for you by arranging columns freely!"
introduction2: "Click on the + on the right of the screen to add new colums whenever you want."
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."

View File

@@ -861,6 +861,11 @@ document: "Documento"
numberOfPageCache: "Cantidad de páginas cacheadas"
numberOfPageCacheDescription: "Al aumentar el número mejora la conveniencia pero tambien puede aumentar la carga y la memoria a usarse"
logoutConfirm: "¿Cerrar sesión?"
reverse: "Echar de un capirotazo"
colored: "Color"
label: "Etiqueta"
localOnly: "Solo local"
account: "Cuentas"
_emailUnavailable:
used: "Ya fue usado"
format: "Formato no válido."

View File

@@ -839,6 +839,11 @@ rateLimitExceeded: "Limite de taux dépassée"
cropImage: "Recadrer l'image"
cropImageAsk: "Voulez-vous recadrer cette image ?"
file: "Fichiers"
reverse: "Inverser"
colored: "Coloré"
label: "Étiquette"
localOnly: "Local seulement"
account: "Comptes"
_emailUnavailable:
used: "Non disponible"
format: "Le format de cette adresse de courriel est invalide"

View File

@@ -848,6 +848,11 @@ rateLimitExceeded: "Batas sudah terlampaui"
cropImage: "potong gambar"
cropImageAsk: "Ingin memotong gambar?"
file: "Berkas"
reverse: "Balik"
colored: "Diwarnai"
label: "Label"
localOnly: "Hanya lokal"
account: "Akun"
_emailUnavailable:
used: "Alamat surel ini telah digunakan"
format: "Format tidak valid."

View File

@@ -810,6 +810,11 @@ oneHour: "1 ora"
oneDay: "1 giorno"
oneWeek: "1 settimana"
file: "Allegati"
reverse: "Inverti"
colored: "Colorato"
label: "Etichetta"
localOnly: "Soltanto locale"
account: "Account"
_emailUnavailable:
used: "Email già in uso"
format: "Formato email non valido"

View File

@@ -886,6 +886,10 @@ cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためア
beta: "ベータ"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、インスタンスによっては自動で設定されることがあります。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
navbar: "ナビゲーションバー"
shuffle: "シャッフル"
account: "アカウント"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@@ -1758,6 +1762,8 @@ _deck:
stackLeft: "左に重ねる"
popRight: "右に出す"
profile: "プロファイル"
newProfile: "新規プロファイル"
deleteProfile: "プロファイルを削除"
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"

View File

@@ -658,6 +658,7 @@ hide: "隠す"
searchByGoogle: "探す"
indefinitely: "無期限"
file: "ファイル"
colored: "色付き"
_ad:
back: "戻る"
_gallery:

View File

@@ -57,6 +57,7 @@ selectAccount: "Fren amiḍan"
accounts: "Imiḍan"
searchByGoogle: "Nadi"
file: "Ifuyla"
account: "Imiḍan"
_email:
_follow:
title: "Yeṭṭafaṛ-ik·em-id"

View File

@@ -203,6 +203,7 @@ done: "완료"
processing: "처리중"
preview: "미리보기"
default: "기본값"
defaultValueIs: "기본값: {value}"
noCustomEmojis: "이모지가 없습니다"
noJobs: "작업이 없습니다"
federating: "연합 중"
@@ -381,6 +382,7 @@ administrator: "관리자"
token: "토큰"
twoStepAuthentication: "2단계 인증"
moderator: "모더레이터"
moderation: "모더레이션"
nUsersMentioned: "{n}명이 언급함"
securityKey: "보안 키"
securityKeyName: "키 이름"
@@ -854,9 +856,45 @@ noEmailServerWarning: "메일 서버가 설정되어 있지 않습니다."
thereIsUnresolvedAbuseReportWarning: "해결되지 않은 신고가 있습니다."
recommended: "추천"
check: "체크"
driveCapOverrideLabel: "이 유저의 드라이브 용량을 변경"
driveCapOverrideCaption: "0 이하를 지정하면 해제됩니다."
requireAdminForView: "열람하려면 관리자 계정으로 로그인해야 합니다."
isSystemAccount: "시스템에 의해 자동으로 생성되어 관리되는 계정입니다."
typeToConfirm: "계속하시려면 {x} 을 입력하세요"
deleteAccount: "계정 삭제"
document: "문서"
numberOfPageCache: "페이지 캐시 수"
numberOfPageCacheDescription: "숫자가 클 수록 편리성이 높아지지만, 시스템 자원과 메모리를 더 많이 사용합니다."
logoutConfirm: "로그아웃 하시겠습니까?"
lastActiveDate: "마지막 이용"
pleaseSelect: "선택해 주세요"
reverse: "플립"
colored: "색 입히기"
refreshInterval: "업데이트 주기"
label: "라벨"
type: "종류"
speed: "속도"
slow: "느리게"
fast: "빠르게"
sensitiveMediaDetection: "민감한 미디어 탐지"
localOnly: "로컬에만"
remoteOnly: "리모트만"
failedToUpload: "업로드 실패"
cannotUploadBecauseInappropriate: "이 파일은 부적절한 내용을 포함한다고 판단되어 업로드할 수 없습니다."
cannotUploadBecauseNoFreeSpace: "드라이브 용량이 부족하여 업로드할 수 없습니다."
beta: "베타"
enableAutoSensitive: "자동 NSFW 탐지"
enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 인스턴스 정책에 따라 자동으로 설정될 수 있습니다."
activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다."
account: "계정"
_sensitiveMediaDetection:
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
sensitivity: "탐지 민감도"
sensitivityDescription: "민감도가 낮을수록 안전한 미디어가 잘못 탐지될 확률이 줄어들며, 높을수록 민감한 미디어가 탐지되지 않을 확률이 줄어듭니다."
setSensitiveFlagAutomatically: "자동으로 NSFW로 설정하기"
setSensitiveFlagAutomaticallyDescription: "이 설정을 해제해도 탐지 결과는 유지됩니다."
analyzeVideos: "동영상도 같이 확인하기"
analyzeVideosDescription: "사진 뿐만 아니라 동영상의 NSFW 여부도 탐지합니다. 서버의 부하를 약간 증가시킵니다."
_emailUnavailable:
used: "이 메일 주소는 사용중입니다"
format: "형식이 올바르지 않습니다"
@@ -1215,6 +1253,7 @@ _widgets:
photos: "사진"
digitalClock: "디지털 시계"
federation: "연합"
instanceCloud: "인스턴스 구름"
postForm: "글 입력란"
slideshow: "슬라이드 쇼"
button: "버튼"
@@ -1660,6 +1699,9 @@ _deck:
stackLeft: "왼쪽에 쌓기"
popRight: "오른쪽으로 빼기"
profile: "프로파일"
introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!"
introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다."
widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요"
_columns:
main: "메인"
widgets: "위젯"

View File

@@ -761,6 +761,10 @@ hide: "Ukryj"
searchByGoogle: "Szukaj"
indefinitely: "Nigdy"
file: "Pliki"
reverse: "Odwróć"
colored: "Kolorowe"
label: "Etykieta"
account: "Konta"
_ffVisibility:
public: "Publikuj"
_ad:

View File

@@ -203,6 +203,7 @@ done: "Готово"
processing: "Обработка"
preview: "Предпросмотр"
default: "По умолчанию"
defaultValueIs: "По умолчанию: {value}"
noCustomEmojis: "Собственные эмодзи отсутствуют"
noJobs: "Нет заданий"
federating: "Федерируется"
@@ -834,6 +835,20 @@ instanceDefaultLightTheme: "Светлая тема по умолчанию"
instanceDefaultDarkTheme: "Темная тема по умолчанию"
indefinitely: "вечно"
file: "Файлы"
recommended: "Рекомендуем"
check: "Проверить"
driveCapOverrideLabel: "Изменение лимита дискового пространства для этого пользователя"
reverse: "Переворот"
colored: "Выделена цветом"
label: "Метка"
localOnly: "Локально"
beta: "Бета"
enableAutoSensitive: "Автоматическое определение NSFW"
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта."
account: "Учётные записи"
_sensitiveMediaDetection:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
setSensitiveFlagAutomatically: "Установить флаг NSFW"
_emailUnavailable:
used: "Уже используется"
format: "Неверный формат"

View File

@@ -865,6 +865,34 @@ numberOfPageCacheDescription: "Zvýši rýchlosť ale tiež nároky na pamäť."
logoutConfirm: "Naozaj sa chcete odhlásiť?"
statusbar: "Stavový riadok"
pleaseSelect: "Prosím vyberte"
reverse: "Preklopiť"
colored: "Farebné"
refreshInterval: "Interval obnovenia"
label: "Popisok"
type: "Typ"
speed: "Rýchlosť"
slow: "Pomaly"
fast: "Rýchlo"
sensitiveMediaDetection: "Detekcia citlivých médií."
localOnly: "Iba lokálne"
remoteOnly: "Len vzdialené"
failedToUpload: "Nahrávanie zlyhalo"
cannotUploadBecauseInappropriate: "Nemožno nahrať, pretože pravdepodobne obsahuje nevhodný obsah."
cannotUploadBecauseNoFreeSpace: "Nemožno nahrať kvôli nedostatku voľného úložiska."
beta: "Beta"
enableAutoSensitive: "Automatická detekcia NSFW"
enableAutoSensitiveDescription: "Ak je zapnuté, príznak NSFW sa na médiách automaticky nastaví pomocou strojového učenia. Aj keď je táto funkcia vypnutá, v niektorých prípadoch sa môže nastaviť automaticky."
activeEmailValidationDescription: "Dôkladnejšie overí e-mailovú adresu používateľa tým, že zistí, či ide o vyradenú e-mailovú adresu a či sa s ňou dá skutočne komunikovať. Ak nie je začiarknuté, e-mailová adresa sa kontroluje len ako text."
navbar: "Navigačný panel"
account: "Účty"
_sensitiveMediaDetection:
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
sensitivity: "Citlivosť detekcie"
sensitivityDescription: "Nižšia citlivosť znižuje počet falošne pozitívnych výsledkov (false positives). Vyššia citlivosť znižuje počet falošne negatívnych výsledkov (false negatives)."
setSensitiveFlagAutomatically: "Nastaviť príznak NSFW"
setSensitiveFlagAutomaticallyDescription: "Aj keď je toto nastavenie vypnuté, výsledok rozhodnutia je interne uložený."
analyzeVideos: "Zapnúť analýzu videa"
analyzeVideosDescription: "Okrem obrázkov zapne detekciu aj pre videá. Zaťaženie servera sa mierne zvýši."
_emailUnavailable:
used: "Táto emailová adresa sa už používa"
format: "Formát emailovej adresy je nesprávny"
@@ -1670,6 +1698,8 @@ _deck:
stackLeft: "Priložiť do ľavého stĺpca"
popRight: "Vybrať napravo"
profile: "Profil"
newProfile: "Nový profil"
deleteProfile: "Odstrániť profil"
introduction: "Kombinujte stĺpce a vytvorte si svoje vlastné rozhranie!"
introduction2: "Stlačením tlačidla + v pravej časti obrazovky môžete kedykoľvek pridať stĺpce."
widgetsIntroduction: "V ponuke stĺpca vyberte možnosť \"Upraviť widget\" a pridajte widget"

View File

@@ -795,8 +795,48 @@ whatIsNew: "แสดงการเปลี่ยนแปลง"
translate: "แปลภาษา"
translatedFrom: "แปลมาจาก {x}"
accountDeletionInProgress: "กำลังดำเนินการลบบัญชีอยู่"
usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง"
aiChanMode: "โหมด Ai "
keepCw: "เก็บคำเตือนเนื้อหา"
pubSub: "บัญชีผับ/ย่อย"
lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด"
resolved: "คลี่คลายแล้ว"
unresolved: "รอการเฉลย"
breakFollow: "ลบผู้ติดตาม"
itsOn: "เปิดใช้งาน"
itsOff: "ปิดใช้งาน"
emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร"
unread: "ไม่ได้อ่าน"
filter: "กรอง"
controlPanel: "แผงควบคุม"
manageAccounts: "จัดการบัญชี"
makeReactionsPublic: "ตั้งค่าประวัติปฏิกิริยาต่อสาธารณะ"
makeReactionsPublicDescription: "การทำเช่นนี้จะทำให้รายการปฏิกิริยาที่ผ่านมาของคุณจะปรากฏต่อสาธารณะนะ"
classic: "คลาสสิค"
muteThread: "ปิดเสียงเธรด"
unmuteThread: "เปิดเสียงเธรด"
ffVisibility: "การมองเห็นผู้ติดตาม/ผู้ติดตาม"
ffVisibilityDescription: "ช่วยให้คุณสามารถกำหนดค่าได้ว่าใครสามารถดูได้ว่าคุณติดตามใครและใครติดตามคุณบ้าง"
continueThread: "ดูความต่อเนื่องเธรด"
deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?"
incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?"
hide: "ซ่อน"
leaveGroup: "ออกจากกลุ่ม"
leaveGroupConfirm: "คุณแน่ใจหรอว่าต้องการออกจาก \"{name}\""
useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ"
welcomeBackWithName: "ยินดีต้อนรับการกลับมานะค่ะ, {name}"
clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ"
overridedDeviceKind: "ประเภทอุปกรณ์"
smartphone: "สมาร์ทโฟน"
tablet: "แท็บเล็ต"
auto: "อัตโนมัติ"
themeColor: "อินสแตนซ์ Ticker Color"
size: "ขนาด"
numberOfColumn: "จำนวนคอลัมน์"
searchByGoogle: "ค้นหา"
file: "ไฟล์"
account: "บัญชีผู้ใช้"
_ffVisibility:
public: "เผยแพร่"
_ad:

View File

@@ -738,6 +738,10 @@ hide: "Сховати"
searchByGoogle: "Пошук"
indefinitely: "Ніколи"
file: "Файли"
reverse: "Перевернути"
colored: "Кольоровий"
label: "Назва"
localOnly: "Локально"
_ffVisibility:
public: "Опублікувати"
_ad:

View File

@@ -869,6 +869,35 @@ logoutConfirm: "Bạn có chắc muốn đăng xuất?"
lastActiveDate: "Lần cuối vào"
statusbar: "Thanh trạng thái"
pleaseSelect: "Chọn một lựa chọn"
reverse: "Lật"
colored: "Với màu"
refreshInterval: "Cập nhật nội bộ"
label: "Nhãn"
type: "Loại"
speed: "Tốc độ"
slow: "Chậm"
fast: "Nhanh"
sensitiveMediaDetection: "Tự động phát hiện NSFW"
localOnly: "Chỉ trên máy chủ"
remoteOnly: "Chỉ máy chủ từ xa"
failedToUpload: "Tải lên thất bại"
cannotUploadBecauseInappropriate: "Không thể tải lên tập tin này vì các phần của tập tin đã được phát hiện có khả năng là NSFW."
cannotUploadBecauseNoFreeSpace: "Tải lên không thành công do thiếu dung lượng Drive."
beta: "Beta"
enableAutoSensitive: "Tự động đánh dấu NSFW"
enableAutoSensitiveDescription: "Cho phép tự động phát hiện và đánh dấu media NSFW thông qua học máy, nếu có thể. Ngay cả khi tùy chọn này bị tắt, nó vẫn có thể được bật trên toàn máy chủ."
activeEmailValidationDescription: "Cho phép xác minh địa chỉ email chặt chẽ hơn, bao gồm việc kiểm tra các địa chỉ dùng một lần và xem nó có thực sự được giao tiếp hay không. Khi bỏ chọn, chỉ định dạng của email được xác minh."
navbar: "Thanh điều hướng"
shuffle: "Xáo trộn"
account: "Tài khoản của bạn"
_sensitiveMediaDetection:
description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ."
sensitivity: "Phát hiện nhạy cảm"
sensitivityDescription: "Giảm độ nhạy sẽ dẫn đến ít phát hiện sai hơn (dương tính giả), tăng nó sẽ dẫn đến ít phát hiện sai hơn (âm tính giả)."
setSensitiveFlagAutomatically: "Đánh dấu là NSFW"
setSensitiveFlagAutomaticallyDescription: "Kết quả của phát hiện nội bộ sẽ được giữ lại ngay cả khi tùy chọn này bị tắt."
analyzeVideos: "Bật chuẩn đoán video"
analyzeVideosDescription: "Phân tích video bên cạnh hình ảnh. Điều này sẽ làm tăng một chút áp lực trên máy chủ."
_emailUnavailable:
used: "Địa chỉ email đã được sử dụng"
format: "Địa chỉ email không hợp lệ"
@@ -1674,6 +1703,8 @@ _deck:
stackLeft: "Xếp chồng với cột bên trái"
popRight: "Xếp chồng với cột bên trái"
profile: "Hồ sơ"
newProfile: "Hồ sơ mới"
deleteProfile: "Xóa hồ sơ"
introduction: "Kết hợp các cột để tạo giao diện của riêng bạn!"
introduction2: "Bạn có thể thêm cột bất kỳ lúc nào bằng cách nhấn + ở bên phải màn hình."
widgetsIntroduction: "Chọn \"Sửa widget\" trong menu cột và thêm một widget."

View File

@@ -203,6 +203,7 @@ done: "完成"
processing: "正在处理"
preview: "预览"
default: "默认"
defaultValueIs: "默认值: {value}"
noCustomEmojis: "没有自定义表情符号"
noJobs: "没有任务"
federating: "联合中"
@@ -336,7 +337,7 @@ pinnedUsers: "置顶用户"
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
pinnedPages: "固定页面"
pinnedPagesDescription: "输入您要固定到实例首页的页面路径,以换行符分隔。"
pinnedClipId: "置顶的签ID"
pinnedClipId: "置顶的便签ID"
pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha"
@@ -381,6 +382,7 @@ administrator: "管理员"
token: "Token (令牌)"
twoStepAuthentication: "两步验证"
moderator: "监察员"
moderation: "管理"
nUsersMentioned: "{n} 被提到"
securityKey: "安全密钥"
securityKeyName: "密钥名称"
@@ -639,10 +641,12 @@ random: "随机"
system: "系统"
switchUi: "切换界面"
desktop: "桌面"
clip: "签"
clip: "便签"
createNew: "新建"
optional: "可选"
createNewClip: "新建签"
createNewClip: "新建便签"
unclip: "移除便签"
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签\"{name}\"里。您想要将本帖从该便签中移除吗?"
public: "公开"
i18nInfo: "Misskey已经被志愿者们翻译成了各种语言。如果你也有兴趣可以通过{link}帮助翻译。"
manageAccessTokens: "管理 Access Tokens"
@@ -676,7 +680,7 @@ pageLikesCount: "页面点赞次数"
pageLikedCount: "页面被点赞次数"
contact: "联系人"
useSystemFont: "使用系统默认字体"
clips: "签"
clips: "便签"
experimentalFeatures: "实验性功能"
developer: "开发者"
makeExplorable: "使账号可见。"
@@ -851,7 +855,45 @@ noEmailServerWarning: "电子邮件服务器未设置。"
thereIsUnresolvedAbuseReportWarning: "有未解决的报告"
recommended: "推荐"
check: "检查"
driveCapOverrideCaption: "设定为 0 以下则会解除此限制。"
requireAdminForView: "需要使用管理员账户登录才能查看。"
isSystemAccount: "该账号由系统自动创建和管理。"
typeToConfirm: "输入 {x} 以确认操作。"
deleteAccount: "删除账户"
document: "文档"
numberOfPageCache: "缓存页数"
numberOfPageCacheDescription: "设置较高的值会更方便用户,但服务器负载和内存使用量会增加。"
logoutConfirm: "是否确认登出?"
lastActiveDate: "最后活跃时间"
statusbar: "状态栏"
pleaseSelect: "请选择"
reverse: "翻转"
colored: "彩色"
refreshInterval: "刷新间隔"
label: "标签"
type: "类型"
speed: "速度"
slow: "慢"
fast: "快"
sensitiveMediaDetection: "检测到敏感媒体"
localOnly: "仅限本地"
remoteOnly: "仅远程"
failedToUpload: "上传失败"
cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法上传。"
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
beta: "测试"
enableAutoSensitive: "自动 NSFW 识别"
navbar: "导航栏"
shuffle: "随机"
account: "账户"
_sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
sensitivity: "检测敏感度"
sensitivityDescription: "敏感度较低,则误检(假阳性)会减少;敏感度较高,则漏检(假阴性)会减少。"
setSensitiveFlagAutomatically: "自动设置 NSFW 标签"
setSensitiveFlagAutomaticallyDescription: "即使关闭此配置,识别结果也会在内部保存。"
analyzeVideos: "启用对视频的检测"
analyzeVideosDescription: "除了静止图像之外,还对视频进行分析。服务器负载会略微增加。"
_emailUnavailable:
used: "已经被使用过"
format: "无效的格式"
@@ -1210,6 +1252,7 @@ _widgets:
photos: "照片"
digitalClock: "数字时钟"
federation: "联邦宇宙"
instanceCloud: "实例云"
postForm: "投稿窗口"
slideshow: "幻灯片展示"
button: "按钮"

View File

@@ -1,7 +1,7 @@
---
_lang_: "繁體中文"
headlineMisskey: "貼文連繫網路"
introMisskey: "歡迎! Misskey是一個開放原始碼且去中心化的社群網路。\n透過「貼文」分享周邊新鮮事並告訴其他人您的想法📡\n透過「情感」功能,對大家的貼文表達情感!👍\n一起來探索這個新的世界吧🚀"
introMisskey: "歡迎! Misskey是一個開放原始碼且去中心化的社群網路。\n透過「貼文」分享周邊新鮮事並告訴其他人您的想法📡\n透過「反應」功能,對大家的貼文表達情感!👍\n一起來探索這個新的世界吧🚀"
monthAndDay: "{month}月 {day}日"
search: "搜尋"
notifications: "通知"
@@ -727,7 +727,7 @@ receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知"
emailNotification: "郵件通知"
publish: "發佈"
inChannelSearch: "頻道内搜尋"
useReactionPickerForContextMenu: "點擊右鍵開啟應工具欄"
useReactionPickerForContextMenu: "點擊右鍵開啟應工具欄"
typingUsers: "{users}輸入中..."
jumpToSpecifiedDate: "跳轉到特定日期"
showingPastTimeline: "顯示過往的時間線"
@@ -810,8 +810,8 @@ unread: "未讀"
filter: "篩選"
controlPanel: "控制台"
manageAccounts: "管理帳戶"
makeReactionsPublic: "將應設為公開"
makeReactionsPublicDescription: "將您做過的應設為公開可見。"
makeReactionsPublic: "將應設為公開"
makeReactionsPublicDescription: "將您做過的應設為公開可見。"
classic: "經典"
muteThread: "將貼文串設為靜音"
unmuteThread: "將貼文串的靜音解除"
@@ -869,6 +869,34 @@ logoutConfirm: "確定要登出嗎?"
lastActiveDate: "上次使用日期及時間"
statusbar: "狀態列"
pleaseSelect: "請選擇"
reverse: "翻轉"
colored: "彩色"
refreshInterval: "更新間隔"
label: "標籤"
type: "類型"
speed: "速度"
slow: "慢"
fast: "快"
sensitiveMediaDetection: "敏感性媒體的檢測"
localOnly: "僅限本地"
remoteOnly: "僅限遠端"
failedToUpload: "上傳失敗"
cannotUploadBecauseInappropriate: "由於判定可能包含不適當的內容,因此無法上船。"
cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此無法上船>"
beta: "Beta"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "如果可用,請利用機器學習在媒體上自動設置 NSFW 旗標。 即使關閉此功能,依實例而定也可能會自動設置。"
activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,判斷它是否為免洗地址,或者它是否可以通信。 若關閉,則只會檢查字元是否正確。"
navbar: "導覽列"
account: "帳戶"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度"
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
setSensitiveFlagAutomatically: "設定 NSFW 旗標"
setSensitiveFlagAutomaticallyDescription: "即使將此設定關閉,判定結果也會保留在內部。"
analyzeVideos: "啟用影片分析"
analyzeVideosDescription: "除了靜止影像以外,也分析影片。伺服器的負荷會稍微增加。"
_emailUnavailable:
used: "已經在使用中"
format: "格式無效"
@@ -1674,6 +1702,8 @@ _deck:
stackLeft: "向左折疊"
popRight: "向右彈出"
profile: "個人檔案"
newProfile: "新建個人檔案"
deleteProfile: "刪除個人檔案"
introduction: "組合欄位來製作屬於自己的介面吧!"
introduction2: "您可以隨時透過按畫面右方的 + 來添加欄位。"
widgetsIntroduction: "請從欄位的選單中,選擇「編輯小工具」來添加小工具"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "12.112.1",
"version": "12.116.0",
"codename": "indigo",
"repository": {
"type": "git",
@@ -41,7 +41,7 @@
"devDependencies": {
"@types/gulp": "4.0.9",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/parser": "5.30.0",
"@typescript-eslint/parser": "5.30.6",
"cross-env": "7.0.3",
"cypress": "10.3.0",
"start-server-and-test": "1.14.0",

View File

@@ -28,7 +28,7 @@ export class foreignKeyReports1651224615271 {
queryRunner.query(`CREATE INDEX "IDX_315c779174fe8247ab324f036e" ON "drive_file" ("isLink")`),
queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId")`),
queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`),
//queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`),
queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`).then(() => {
queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);

View File

@@ -0,0 +1,11 @@
export class activeEmailValidation1657346559800 {
name = 'activeEmailValidation1657346559800'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableActiveEmailValidation" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableActiveEmailValidation"`);
}
}

View File

@@ -13,6 +13,9 @@
"chokidar": "^3.3.1",
"lodash": "^4.17.21"
},
"optionalDependencies": {
"@tensorflow/tfjs-node": "3.18.0"
},
"dependencies": {
"@bull-board/koa": "4.0.0",
"@discordapp/twemoji": "14.0.2",
@@ -23,7 +26,6 @@
"@peertube/http-signature": "1.6.0",
"@sinonjs/fake-timers": "9.1.2",
"@syuilo/aiscript": "0.11.1",
"@tensorflow/tfjs-node": "3.18.0",
"abort-controller": "3.0.0",
"ajv": "8.11.0",
"archiver": "5.3.1",
@@ -37,7 +39,7 @@
"cbor": "8.1.0",
"chalk": "5.0.1",
"chalk-template": "0.4.0",
"chokidar": "3.3.1",
"chokidar": "3.5.3",
"cli-highlight": "2.1.11",
"color-convert": "2.0.1",
"content-disposition": "0.5.4",
@@ -67,15 +69,15 @@
"koa-send": "5.0.1",
"koa-slow": "2.1.0",
"koa-views": "7.0.2",
"mfm-js": "0.22.1",
"mfm-js": "0.23.0-canary.1",
"mime-types": "2.1.35",
"misskey-js": "0.0.14",
"mocha": "10.0.0",
"ms": "3.0.0-canary.1",
"multer": "1.4.4",
"nested-property": "4.0.0",
"node-fetch": "3.2.6",
"nodemailer": "6.7.6",
"node-fetch": "3.2.8",
"nodemailer": "6.7.7",
"nsfwjs": "2.4.1",
"os-utils": "0.0.14",
"parse5": "7.0.0",
@@ -104,13 +106,13 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"style-loader": "3.3.1",
"summaly": "2.6.0",
"summaly": "2.7.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.11.22",
"systeminformation": "5.12.0",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
"ts-loader": "9.3.1",
"ts-node": "10.8.1",
"ts-node": "10.8.2",
"tsc-alias": "1.6.11",
"tsconfig-paths": "4.0.0",
"twemoji-parser": "14.0.0",
@@ -135,7 +137,7 @@
"@types/jsdom": "16.2.14",
"@types/jsonld": "1.5.6",
"@types/jsrsasign": "10.5.1",
"@types/koa": "2.13.4",
"@types/koa": "2.13.5",
"@types/koa-bodyparser": "4.3.7",
"@types/koa-cors": "0.0.2",
"@types/koa-favicon": "2.0.21",
@@ -147,7 +149,7 @@
"@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11",
"@types/mocha": "9.1.1",
"@types/node": "18.0.0",
"@types/node": "18.0.3",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1",
@@ -169,10 +171,10 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.30.0",
"@typescript-eslint/parser": "5.30.0",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
"cross-env": "7.0.3",
"eslint": "8.18.0",
"eslint": "8.19.0",
"eslint-plugin-import": "2.26.0",
"execa": "6.1.0",
"typescript": "4.7.4"

View File

@@ -145,6 +145,12 @@ export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMenti
a.textContent = node.props.content;
return a;
},
plain(node) {
const el = doc.createElement('span');
appendChildren(node.children, el);
return el;
},
};
appendChildren(nodes, doc.body);

View File

@@ -7,28 +7,31 @@ import { WriteStream } from 'node:fs';
import * as p from 'pureimage';
import gen from 'random-seed';
const size = 256; // px
const size = 128; // px
const n = 5; // resolution
const margin = (size / n);
const margin = (size / 4);
const colors = [
'#e57373',
'#F06292',
'#BA68C8',
'#9575CD',
'#7986CB',
'#64B5F6',
'#4FC3F7',
'#4DD0E1',
'#4DB6AC',
'#81C784',
'#8BC34A',
'#AFB42B',
'#F57F17',
'#FF5722',
'#795548',
'#455A64',
['#FF512F', '#DD2476'],
['#FF61D2', '#FE9090'],
['#72FFB6', '#10D164'],
['#FD8451', '#FFBD6F'],
['#305170', '#6DFC6B'],
['#00C0FF', '#4218B8'],
['#009245', '#FCEE21'],
['#0100EC', '#FB36F4'],
['#FDABDD', '#374A5A'],
['#38A2D7', '#561139'],
['#121C84', '#8278DA'],
['#5761B2', '#1FC5A8'],
['#FFDB01', '#0E197D'],
['#FF3E9D', '#0E1F40'],
['#766eff', '#00d4ff'],
['#9bff6e', '#00d4ff'],
['#ff6e94', '#00d4ff'],
['#ffa96e', '#00d4ff'],
['#ffa96e', '#ff009d'],
['#ffdd6e', '#ff009d'],
];
const bg = '#e9e9e9';
const actualSize = size - (margin * 2);
const cellSize = actualSize / n;
@@ -42,11 +45,17 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
const canvas = p.make(size, size, undefined);
const ctx = canvas.getContext('2d');
const bgColors = colors[rand(colors.length)];
const bg = ctx.createLinearGradient(0, 0, size, size);
bg.addColorStop(0, bgColors[0]);
bg.addColorStop(1, bgColors[1]);
ctx.fillStyle = bg;
ctx.beginPath();
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = colors[rand(colors.length)];
ctx.fillStyle = '#ffffff';
// side bitmap (filled by false)
const side: boolean[][] = new Array(sideN);

View File

@@ -454,4 +454,9 @@ export class Meta {
default: false,
})
public enableIpLogging: boolean;
@Column('boolean', {
default: true,
})
public enableActiveEmailValidation: boolean;
}

View File

@@ -8,7 +8,7 @@ import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns';
import { Users, Emojis } from '@/models/index.js';
import { } from '@/queue/types.js';
import { } from '@/queue/types.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { downloadUrl } from '@/misc/download-url.js';
import config from '@/config/index.js';

View File

@@ -1,6 +1,6 @@
import config from '@/config/index.js';
import Resolver from '../resolver.js';
import { IObject, IQuestion, isQuestion } from '../type.js';
import { IObject, IQuestion, isQuestion } from '../type.js';
import { apLogger } from '../logger.js';
import { Notes, Polls } from '@/models/index.js';
import { IPoll } from '@/models/entities/poll.js';

View File

@@ -8,7 +8,7 @@ import renderEmoji from './emoji.js';
export const renderLike = async (noteReaction: NoteReaction, note: Note) => {
const reaction = noteReaction.reaction;
const object = {
const object = {
type: 'Like',
id: `${config.url}/likes/${noteReaction.id}`,
actor: `${config.url}/users/${noteReaction.userId}`,

View File

@@ -68,11 +68,11 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
}
try {
UserIps.insert({
UserIps.createQueryBuilder().insert().values({
createdAt: new Date(),
userId: user.id,
ip: ip,
});
}).orIgnore(true).execute();
} catch {
}
}

View File

@@ -12,13 +12,15 @@ export async function readNotification(
if (notificationIds.length === 0) return;
// Update documents
await Notifications.update({
const result = await Notifications.update({
id: In(notificationIds),
isRead: false,
}, {
isRead: true,
});
if (result.affected === 0) return;
if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId);
else return postReadNotifications(userId, notificationIds);
}
@@ -27,7 +29,7 @@ export async function readNotificationByQuery(
userId: User['id'],
query: Record<string, any>
) {
const notificationIds = await Notifications.find({
const notificationIds = await Notifications.findBy({
...query,
notifieeId: userId,
isRead: false,

View File

@@ -324,6 +324,10 @@ export const meta = {
type: 'boolean',
optional: true, nullable: false,
},
enableActiveEmailValidation: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
} as const;
@@ -421,5 +425,6 @@ export default define(meta, paramDef, async (ps, me) => {
deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation,
};
});

View File

@@ -101,6 +101,7 @@ export const paramDef = {
objectStorageSetPublicRead: { type: 'boolean' },
objectStorageS3ForcePathStyle: { type: 'boolean' },
enableIpLogging: { type: 'boolean' },
enableActiveEmailValidation: { type: 'boolean' },
},
required: [],
} as const;
@@ -421,6 +422,10 @@ export default define(meta, paramDef, async (ps, me) => {
set.enableIpLogging = ps.enableIpLogging;
}
if (ps.enableActiveEmailValidation !== undefined) {
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
}
await db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(Meta, {
order: {

View File

@@ -214,7 +214,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
if (newName != null) {
const tokens = mfm.parsePlain(newName);
const tokens = mfm.parseSimple(newName);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
}

View File

@@ -16,6 +16,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.onNote.bind(this);
this.emitTypers = this.emitTypers.bind(this);
}
public async init(params: any) {

View File

@@ -78,6 +78,7 @@ const nodeinfo2 = async () => {
enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,
themeColor: meta.themeColor || '#86b300',
},
};
};

View File

@@ -14,9 +14,11 @@
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
(async () => {
window.onerror = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED', e);
};
window.onunhandledrejection = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
};
@@ -47,18 +49,30 @@
localStorage.setItem('localeVersion', v);
} else {
await checkUpdate();
renderError('LOCALE_FETCH_FAILED');
renderError('LOCALE_FETCH');
return;
}
}
//#endregion
//#region Script
import(`/assets/${CLIENT_ENTRY}`)
.catch(async e => {
await checkUpdate();
renderError('APP_FETCH_FAILED', e);
})
function importAppScript() {
import(`/assets/${CLIENT_ENTRY}`)
.catch(async e => {
await checkUpdate();
console.error(e);
renderError('APP_IMPORT', e);
});
}
// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
if (document.readyState !== 'loading') {
importAppScript();
} else {
window.addEventListener('DOMContentLoaded', () => {
importAppScript();
});
}
//#endregion
//#region Theme
@@ -112,35 +126,35 @@
let errorsElement = document.getElementById('errors');
if (!errorsElement) {
document.documentElement.innerHTML = `
document.body.innerHTML = `
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 9v2m0 4v.01"></path>
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 9v2m0 4v.01"></path>
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
</svg>
<h1>An error has occurred!</h1>
<button class="button-big" onclick="location.reload(true);">
<span class="button-label-big">Refresh</span>
</button>
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
<a href="/flush">
<button class="button-small">
<span class="button-label-small">Clear preferences and cache</span>
</button>
</a>
<a href="/flush">
<button class="button-small">
<span class="button-label-small">Clear preferences and cache</span>
</button>
</a>
<br>
<a href="/cli">
<button class="button-small">
<span class="button-label-small">Start the simple client</span>
</button>
</a>
<a href="/cli">
<button class="button-small">
<span class="button-label-small">Start the simple client</span>
</button>
</a>
<br>
<a href="/bios">
<button class="button-small">
<span class="button-label-small">Start the repair tool</span>
</button>
</a>
<a href="/bios">
<button class="button-small">
<span class="button-label-small">Start the repair tool</span>
</button>
</a>
<br>
<div id="errors"></div>
`;
@@ -269,17 +283,22 @@
// eslint-disable-next-line no-inner-declarations
async function checkUpdate() {
// TODO: サーバーが落ちている場合などのエラーハンドリング
const res = await fetch('/api/meta', {
method: 'POST',
cache: 'no-cache'
});
try {
const res = await fetch('/api/meta', {
method: 'POST',
cache: 'no-cache'
});
const meta = await res.json();
const meta = await res.json();
if (meta.version != v) {
localStorage.setItem('v', meta.version);
refresh();
if (meta.version != v) {
localStorage.setItem('v', meta.version);
refresh();
}
} catch (e) {
console.error(e);
renderError('UPDATE_CHECK', e);
throw e;
}
}

View File

@@ -2,19 +2,34 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as nsfw from 'nsfwjs';
import * as tf from '@tensorflow/tfjs-node';
import si from 'systeminformation';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const REQUIRED_CPU_FLAGS = ['avx2', 'fma'];
let isSupportedCpu: undefined | boolean = undefined;
let model: nsfw.NSFWJS;
export async function detectSensitive(path: string): Promise<nsfw.predictionType[] | null> {
try {
if (isSupportedCpu === undefined) {
const cpuFlags = await getCpuFlags();
isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required));
}
if (!isSupportedCpu) {
console.error('This CPU cannot use TensorFlow.');
return null;
}
const tf = await import('@tensorflow/tfjs-node');
if (model == null) model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
const buffer = await fs.promises.readFile(path);
const image = await tf.node.decodeImage(buffer, 3) as tf.Tensor3D;
const image = await tf.node.decodeImage(buffer, 3) as any;
try {
const predictions = await model.classify(image);
return predictions;
@@ -26,3 +41,8 @@ export async function detectSensitive(path: string): Promise<nsfw.predictionType
return null;
}
}
async function getCpuFlags(): Promise<string[]> {
const str = await si.cpuFlags();
return str.split(/\s+/);
}

View File

@@ -34,7 +34,7 @@ export async function fetchInstanceMetadata(instance: Instance, force = false):
const [favicon, icon, themeColor, name, description] = await Promise.all([
fetchFaviconUrl(instance, dom).catch(() => null),
fetchIconUrl(instance, dom, manifest).catch(() => null),
getThemeColor(dom, manifest).catch(() => null),
getThemeColor(info, dom, manifest).catch(() => null),
getSiteName(info, dom, manifest).catch(() => null),
getDescription(info, dom, manifest).catch(() => null),
]);
@@ -208,8 +208,8 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul
return null;
}
async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color;
async function getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
const themeColor = info?.metadata?.themeColor || doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color;
if (themeColor) {
const color = new tinycolor(themeColor);

View File

@@ -64,6 +64,7 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(u
type,
body: type === 'notification' ? truncateNotification(body as Packed<'Notification'>) : body,
userId,
dateTime: (new Date()).getTime(),
}), {
proxy: config.proxy,
}).catch((err: any) => {

View File

@@ -1,34 +1,37 @@
import { validate as validateEmail } from 'deep-email-validator';
import { UserProfiles } from '@/models/index.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
export async function validateEmailForAccount(emailAddress: string): Promise<{
available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
}> {
const meta = await fetchMeta();
const exist = await UserProfiles.countBy({
emailVerified: true,
email: emailAddress,
});
const validated = await validateEmail({
const validated = meta.enableActiveEmailValidation ? await validateEmail({
email: emailAddress,
validateRegex: true,
validateMx: true,
validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
validateDisposable: true, // 捨てアドかどうかチェック
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
});
}) : { valid: true };
const available = exist === 0 && validated.valid;
return {
available,
reason: available ? null :
exist !== 0 ? 'used' :
validated.reason === 'regex' ? 'format' :
validated.reason === 'disposable' ? 'disposable' :
validated.reason === 'mx' ? 'mx' :
validated.reason === 'smtp' ? 'smtp' :
null,
exist !== 0 ? 'used' :
validated.reason === 'regex' ? 'format' :
validated.reason === 'disposable' ? 'disposable' :
validated.reason === 'mx' ? 'mx' :
validated.reason === 'smtp' ? 'smtp' :
null,
};
}

View File

@@ -14,6 +14,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 0,
md5: 'd41d8cd98f00b204e9800998ecf8427e',
@@ -33,6 +34,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 25360,
md5: '091b3f259662aa31e2ffef4519951168',
@@ -52,6 +54,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 1868,
md5: '08189c607bea3b952704676bb3c979e0',
@@ -71,6 +74,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 2248,
md5: '32c47a11555675d9267aee1a86571e7e',
@@ -90,6 +94,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 3772,
md5: 'f73535c3e1e27508885b69b10cf6e991',
@@ -109,6 +114,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 505,
md5: 'b6f52b4b021e7b92cdd04509c7267965',
@@ -129,6 +135,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 544,
md5: '4b7a346cde9ccbeb267e812567e33397',
@@ -148,6 +155,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 75933,
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
@@ -167,6 +175,7 @@ describe('Get file info', () => {
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 12624,
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',

View File

@@ -303,12 +303,7 @@
pluralize "^8.0.0"
yaml-ast-parser "0.0.43"
"@sindresorhus/is@^3.0.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-3.1.2.tgz#548650de521b344e3781fbdb0ece4aa6f729afb8"
integrity sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==
"@sindresorhus/is@^4.6.0":
"@sindresorhus/is@^4.0.0", "@sindresorhus/is@^4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==
@@ -735,10 +730,10 @@
"@types/koa-compose" "*"
"@types/node" "*"
"@types/koa@2.13.4":
version "2.13.4"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==
"@types/koa@2.13.5":
version "2.13.5"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.5.tgz#64b3ca4d54e08c0062e89ec666c9f45443b21a61"
integrity sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==
dependencies:
"@types/accepts" "*"
"@types/content-disposition" "*"
@@ -805,10 +800,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
"@types/node@18.0.0":
version "18.0.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a"
integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==
"@types/node@18.0.3":
version "18.0.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199"
integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==
"@types/node@^14.11.8":
version "14.17.9"
@@ -999,14 +994,14 @@
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.0.tgz#524a11e15c09701733033c96943ecf33f55d9ca1"
integrity sha512-lvhRJ2pGe2V9MEU46ELTdiHgiAFZPKtLhiU5wlnaYpMc2+c1R8fh8i80ZAa665drvjHKUJyRRGg3gEm1If54ow==
"@typescript-eslint/eslint-plugin@5.30.6":
version "5.30.6"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz#9c6017b6c1d04894141b4a87816388967f64c359"
integrity sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg==
dependencies:
"@typescript-eslint/scope-manager" "5.30.0"
"@typescript-eslint/type-utils" "5.30.0"
"@typescript-eslint/utils" "5.30.0"
"@typescript-eslint/scope-manager" "5.30.6"
"@typescript-eslint/type-utils" "5.30.6"
"@typescript-eslint/utils" "5.30.6"
debug "^4.3.4"
functional-red-black-tree "^1.0.1"
ignore "^5.2.0"
@@ -1014,69 +1009,69 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/parser@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.30.0.tgz#a2184fb5f8ef2bf1db0ae61a43907e2e32aa1b8f"
integrity sha512-2oYYUws5o2liX6SrFQ5RB88+PuRymaM2EU02/9Ppoyu70vllPnHVO7ioxDdq/ypXHA277R04SVjxvwI8HmZpzA==
"@typescript-eslint/parser@5.30.6":
version "5.30.6"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.30.6.tgz#add440db038fa9d777e4ebdaf66da9e7fb7abe92"
integrity sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==
dependencies:
"@typescript-eslint/scope-manager" "5.30.0"
"@typescript-eslint/types" "5.30.0"
"@typescript-eslint/typescript-estree" "5.30.0"
"@typescript-eslint/scope-manager" "5.30.6"
"@typescript-eslint/types" "5.30.6"
"@typescript-eslint/typescript-estree" "5.30.6"
debug "^4.3.4"
"@typescript-eslint/scope-manager@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.30.0.tgz#bf585ee801ab4ad84db2f840174e171a6bb002c7"
integrity sha512-3TZxvlQcK5fhTBw5solQucWSJvonXf5yua5nx8OqK94hxdrT7/6W3/CS42MLd/f1BmlmmbGEgQcTHHCktUX5bQ==
"@typescript-eslint/scope-manager@5.30.6":
version "5.30.6"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz#ce1b49ff5ce47f55518d63dbe8fc9181ddbd1a33"
integrity sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==
dependencies:
"@typescript-eslint/types" "5.30.0"
"@typescript-eslint/visitor-keys" "5.30.0"
"@typescript-eslint/types" "5.30.6"
"@typescript-eslint/visitor-keys" "5.30.6"
"@typescript-eslint/type-utils@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.30.0.tgz#98f3af926a5099153f092d4dad87148df21fbaae"
integrity sha512-GF8JZbZqSS+azehzlv/lmQQ3EU3VfWYzCczdZjJRxSEeXDQkqFhCBgFhallLDbPwQOEQ4MHpiPfkjKk7zlmeNg==
"@typescript-eslint/type-utils@5.30.6":
version "5.30.6"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.30.6.tgz#a64aa9acbe609ab77f09f53434a6af2b9685f3af"
integrity sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==
dependencies:
"@typescript-eslint/utils" "5.30.0"
"@typescript-eslint/utils" "5.30.6"
debug "^4.3.4"
tsutils "^3.21.0"
"@typescript-eslint/types@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.30.0.tgz#db7d81d585a3da3801432a9c1d2fafbff125e110"
integrity sha512-vfqcBrsRNWw/LBXyncMF/KrUTYYzzygCSsVqlZ1qGu1QtGs6vMkt3US0VNSQ05grXi5Yadp3qv5XZdYLjpp8ag==
"@typescript-eslint/types@5.30.6":
version "5.30.6"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.30.6.tgz#86369d0a7af8c67024115ac1da3e8fb2d38907e1"
integrity sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==
"@typescript-eslint/typescript-estree@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.0.tgz#4565ee8a6d2ac368996e20b2344ea0eab1a8f0bb"
integrity sha512-hDEawogreZB4n1zoqcrrtg/wPyyiCxmhPLpZ6kmWfKF5M5G0clRLaEexpuWr31fZ42F96SlD/5xCt1bT5Qm4Nw==
"@typescript-eslint/typescript-estree@5.30.6":
version "5.30.6"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz#a84a0d6a486f9b54042da1de3d671a2c9f14484e"
integrity sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==
dependencies:
"@typescript-eslint/types" "5.30.0"
"@typescript-eslint/visitor-keys" "5.30.0"
"@typescript-eslint/types" "5.30.6"
"@typescript-eslint/visitor-keys" "5.30.6"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.30.0.tgz#1dac771fead5eab40d31860716de219356f5f754"
integrity sha512-0bIgOgZflLKIcZsWvfklsaQTM3ZUbmtH0rJ1hKyV3raoUYyeZwcjQ8ZUJTzS7KnhNcsVT1Rxs7zeeMHEhGlltw==
"@typescript-eslint/utils@5.30.6":
version "5.30.6"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.30.6.tgz#1de2da14f678e7d187daa6f2e4cdb558ed0609dc"
integrity sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==
dependencies:
"@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "5.30.0"
"@typescript-eslint/types" "5.30.0"
"@typescript-eslint/typescript-estree" "5.30.0"
"@typescript-eslint/scope-manager" "5.30.6"
"@typescript-eslint/types" "5.30.6"
"@typescript-eslint/typescript-estree" "5.30.6"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/visitor-keys@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.0.tgz#07721d23daca2ec4c2da7f1e660d41cd78bacac3"
integrity sha512-6WcIeRk2DQ3pHKxU1Ni0qMXJkjO/zLjBymlYBy/53qxe7yjEFSvzKLDToJjURUhSl2Fzhkl4SMXQoETauF74cw==
"@typescript-eslint/visitor-keys@5.30.6":
version "5.30.6"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz#94dd10bb481c8083378d24de1742a14b38a2678c"
integrity sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==
dependencies:
"@typescript-eslint/types" "5.30.0"
"@typescript-eslint/types" "5.30.6"
eslint-visitor-keys "^3.3.0"
"@ungap/promise-all-settled@1.1.2":
@@ -1283,6 +1278,14 @@ anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
app-root-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad"
@@ -1736,19 +1739,6 @@ cacheable-lookup@^5.0.3:
resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3"
integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==
cacheable-request@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58"
integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==
dependencies:
clone-response "^1.0.2"
get-stream "^5.1.0"
http-cache-semantics "^4.0.0"
keyv "^4.0.0"
lowercase-keys "^2.0.0"
normalize-url "^4.1.0"
responselike "^2.0.0"
cacheable-request@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27"
@@ -1896,7 +1886,7 @@ cheerio@0.22.0:
lodash.reject "^4.4.0"
lodash.some "^4.4.0"
chokidar@3.3.1, chokidar@3.5.3, chokidar@^3.3.1, chokidar@^3.5.3:
chokidar@3.5.3, chokidar@^3.3.1, chokidar@^3.5.3:
version "3.3.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
@@ -2900,10 +2890,10 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.18.0.tgz#78d565d16c993d0b73968c523c0446b13da784fd"
integrity sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==
eslint@8.19.0:
version "8.19.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.19.0.tgz#7342a3cbc4fbc5c106a1eefe0fd0b50b6b1a7d28"
integrity sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==
dependencies:
"@eslint/eslintrc" "^1.3.0"
"@humanwhocodes/config-array" "^0.9.2"
@@ -3299,6 +3289,11 @@ fsevents@~2.1.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
fstream@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
@@ -3435,7 +3430,7 @@ github-from-package@0.0.0:
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0:
glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -3520,19 +3515,19 @@ google-protobuf@^3.9.2:
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.20.1.tgz#1b255c2b59bcda7c399df46c65206aa3c7a0ce8b"
integrity sha512-XMf1+O32FjYIV3CYu6Tuh5PNbfNEU5Xu22X+Xkdb/DUexFlCzhvv7d5Iirm4AOwn8lv4al1YvIhzGrg2j9Zfzw==
got@11.5.1:
version "11.5.1"
resolved "https://registry.yarnpkg.com/got/-/got-11.5.1.tgz#bf098a270fe80b3fb88ffd5a043a59ebb0a391db"
integrity sha512-reQEZcEBMTGnujmQ+Wm97mJs/OK6INtO6HmLI+xt3+9CvnRwWjXutUvb2mqr+Ao4Lu05Rx6+udx9sOQAmExMxA==
got@11.8.5:
version "11.8.5"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046"
integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==
dependencies:
"@sindresorhus/is" "^3.0.0"
"@sindresorhus/is" "^4.0.0"
"@szmarczak/http-timer" "^4.0.5"
"@types/cacheable-request" "^6.0.1"
"@types/responselike" "^1.0.0"
cacheable-lookup "^5.0.3"
cacheable-request "^7.0.1"
cacheable-request "^7.0.2"
decompress-response "^6.0.0"
http2-wrapper "^1.0.0-beta.5.0"
http2-wrapper "^1.0.0-beta.5.2"
lowercase-keys "^2.0.0"
p-cancelable "^2.0.0"
responselike "^2.0.0"
@@ -3734,7 +3729,7 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
http2-wrapper@^1.0.0-beta.5.0:
http2-wrapper@^1.0.0-beta.5.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==
@@ -4874,12 +4869,12 @@ methods@^1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mfm-js@0.22.1:
version "0.22.1"
resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.22.1.tgz#ad5f0b95cc903ca5a5e414e2edf64ac4648dc8c2"
integrity sha512-UV5zvDKlWPpBFeABhyCzuOTJ3RwrNrmVpJ+zz/dFX6D/ntEywljgxkfsLamcy0ZSwUAr0O+WQxGHvAwyxUgsAQ==
mfm-js@0.23.0-canary.1:
version "0.23.0-canary.1"
resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.23.0-canary.1.tgz#1b7b7635f18bed9776054406b72e6bd613c8d0eb"
integrity sha512-SkMrW1rQAv+mFGtLKN9DSv6vxSREDu8ChmkBl1Ch5sQfp47BhuvcRg5EKGmt3WAQRAOylXYMg8wVYR7C5BHKeA==
dependencies:
twemoji-parser "14.0.x"
twemoji-parser "14.0.0"
micromatch@^4.0.0, micromatch@^4.0.2:
version "4.0.2"
@@ -5301,10 +5296,10 @@ node-fetch@*:
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-fetch@3.2.6, node-fetch@^3.2.2:
version "3.2.6"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.6.tgz#6d4627181697a9d9674aae0d61548e0d629b31b9"
integrity sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw==
node-fetch@3.2.8:
version "3.2.8"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.8.tgz#0d9516dcf43a758d78d6dbe538adf0b1f6a4944e"
integrity sha512-KtpD1YhGszhntMpBDyp5lyagk8KIMopC1LEb7cQUAh7zcosaX5uK8HnbNb2i3NTQK3sIawCItS0uFC3QzcLHdg==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
@@ -5317,6 +5312,15 @@ node-fetch@^2.6.1, node-fetch@~2.6.1:
dependencies:
whatwg-url "^5.0.0"
node-fetch@^3.2.2:
version "3.2.6"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.6.tgz#6d4627181697a9d9674aae0d61548e0d629b31b9"
integrity sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-gyp-build@^4.2.3:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
@@ -5343,10 +5347,10 @@ node-gyp@^9.0.0:
tar "^6.1.2"
which "^2.0.2"
nodemailer@6.7.6:
version "6.7.6"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.6.tgz#d3de8f644eaa0dad784d1be1375c596de492f3fc"
integrity sha512-/6KF/umU7r7X21Y648/yiRLrgkfz0dmpyuo4BfgYWIpnT/jCbkPTvegMfxCsDAu+O810p2L1BGXieMTPp3nJVA==
nodemailer@6.7.7:
version "6.7.7"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.7.tgz#e522fbd7507b81c51446d3f79c4603bf00083ddd"
integrity sha512-pOLC/s+2I1EXuSqO5Wa34i3kXZG3gugDssH+ZNCevHad65tc8vQlCQpOLaUjopvkRQKm2Cki2aME7fEOPRy3bA==
nofilter@^2.0.3:
version "2.0.3"
@@ -5380,11 +5384,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-url@^4.1.0:
version "4.5.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
normalize-url@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
@@ -6286,6 +6285,13 @@ readdirp@~3.3.0:
dependencies:
picomatch "^2.0.7"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
reconnecting-websocket@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
@@ -6974,15 +6980,15 @@ style-loader@3.3.1:
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
summaly@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.6.0.tgz#aaac80eb8ae88b130318f44d9b98da9c2ccb328c"
integrity sha512-wIv6fL3aeFfXcQoZISzeUfNUgD3u8Hwx8Rg0awZliQhans62w23K3nDezwfvmYAQCgXs6e0EF7jtGmJv/qeVTA==
summaly@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.7.0.tgz#ccccec0477938edea13cb34412a33e705398c0c4"
integrity sha512-pEz9LL8Gp0oPIQfn6TrnBCcv/HkFE14hxhH3W6LPGdopXlPXjRcMlDMJaO+VupUNMOGaMjCsjq7+0rWnu8sp7w==
dependencies:
cheerio "0.22.0"
debug "4.3.3"
escape-regexp "0.0.1"
got "11.5.1"
got "11.8.5"
html-entities "2.3.2"
iconv-lite "0.6.3"
jschardet "3.0.0"
@@ -7029,10 +7035,10 @@ syslog-pro@1.0.0:
dependencies:
moment "^2.22.2"
systeminformation@5.11.22:
version "5.11.22"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.22.tgz#52eb78fd6bb48eef372f502b494ff59aacf82c02"
integrity sha512-sBZJ/WBCf2vDLeMZaEyVuo+aXylOSmNHHB2cX0jHULFxSBLXHX+QUHYrCvmz+YiflKY3bsahVWX7vwuz1p1QZA==
systeminformation@5.12.0:
version "5.12.0"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.12.0.tgz#1ff8d0b0fef9c2a6f70e1b1f2e681b8cd2d4b78e"
integrity sha512-2fa/2cnWxiC/g8v3XX4aEC2CQWl/WJ+JrfLq9f76lxYqkW4DWiU3vJhrEgAzBT07ta73c5wHd0xgMeMo0vBiMQ==
tapable@^2.2.0:
version "2.2.0"
@@ -7226,10 +7232,10 @@ ts-loader@9.3.1:
micromatch "^4.0.0"
semver "^7.3.4"
ts-node@10.8.1:
version "10.8.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.1.tgz#ea2bd3459011b52699d7e88daa55a45a1af4f066"
integrity sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==
ts-node@10.8.2:
version "10.8.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.2.tgz#3185b75228cef116bf82ffe8762594f54b2a23f2"
integrity sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
@@ -7310,7 +7316,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
twemoji-parser@14.0.0, twemoji-parser@14.0.x:
twemoji-parser@14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-14.0.0.tgz#13dabcb6d3a261d9efbf58a1666b182033bf2b62"
integrity sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==

View File

@@ -15,7 +15,7 @@
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"@syuilo/aiscript": "0.11.1",
"@vitejs/plugin-vue": "3.0.0-beta.1",
"@vitejs/plugin-vue": "3.0.0",
"@vue/compiler-sfc": "3.2.37",
"abort-controller": "3.0.0",
"autobind-decorator": "2.4.0",
@@ -40,7 +40,7 @@
"json5": "2.2.1",
"katex": "0.15.6",
"matter-js": "0.18.0",
"mfm-js": "0.22.1",
"mfm-js": "0.23.0-canary.1",
"misskey-js": "0.0.14",
"mocha": "10.0.0",
"ms": "2.1.3",
@@ -56,7 +56,6 @@
"random-seed": "0.3.0",
"reflect-metadata": "0.1.13",
"rndstr": "1.0.0",
"rollup": "2.75.7",
"s-age": "1.1.2",
"sass": "1.53.0",
"seedrandom": "3.0.5",
@@ -74,7 +73,7 @@
"uuid": "8.3.2",
"v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2",
"vite": "3.0.0-beta.7",
"vite": "3.0.0",
"vue": "3.2.37",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "4.0.1",
@@ -100,13 +99,14 @@
"@types/uuid": "8.3.4",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.30.0",
"@typescript-eslint/parser": "5.30.0",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
"rollup": "2.76.0",
"cross-env": "7.0.3",
"cypress": "10.3.0",
"eslint": "8.18.0",
"eslint": "8.19.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-vue": "9.1.1",
"eslint-plugin-vue": "9.2.0",
"start-server-and-test": "1.14.0"
}
}

View File

@@ -316,7 +316,7 @@ const render = () => {
plugins: [{
id: 'vLine',
beforeDraw(chart, args, options) {
if (chart.tooltip._active && chart.tooltip._active.length) {
if (chart.tooltip?._active?.length) {
const activePoint = chart.tooltip._active[0];
const ctx = chart.ctx;
const x = activePoint.element.x;

View File

@@ -59,7 +59,7 @@ const isThumbnailAvailable = computed(() => {
display: flex;
background: var(--panel);
border-radius: 8px;
overflow: hidden; overflow: clip;
overflow: clip;
> .icon-sub {
position: absolute;

View File

@@ -13,7 +13,7 @@
</template>
<script lang="ts" setup>
import { } from 'vue';
import { } from 'vue';
import * as Misskey from 'misskey-js';
import XDrive from './drive.vue';
import XWindow from '@/components/ui/window.vue';

View File

@@ -26,7 +26,8 @@
</div>
<button class="menu _button" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
</nav>
<div ref="main" class="main"
<div
ref="main" class="main"
:class="{ uploading: uploadings.length > 0, fetching }"
@dragover.prevent.stop="onDragover"
@dragenter="onDragenter"
@@ -142,7 +143,7 @@ const isDragSource = ref(false);
const fetching = ref(true);
const ilFilesObserver = new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
);
watch(folder, () => emit('cd', folder.value));
@@ -232,7 +233,7 @@ function onDrop(ev: DragEvent): any {
removeFile(file.id);
os.api('drive/files/update', {
fileId: file.id,
folderId: folder.value ? folder.value.id : null
folderId: folder.value ? folder.value.id : null,
});
}
//#endregion
@@ -248,7 +249,7 @@ function onDrop(ev: DragEvent): any {
removeFolder(droppedFolder.id);
os.api('drive/folders/update', {
folderId: droppedFolder.id,
parentId: folder.value ? folder.value.id : null
parentId: folder.value ? folder.value.id : null,
}).then(() => {
// noop
}).catch(err => {
@@ -256,13 +257,13 @@ function onDrop(ev: DragEvent): any {
case 'detected-circular-definition':
os.alert({
title: i18n.ts.unableToProcess,
text: i18n.ts.circularReferenceFolder
text: i18n.ts.circularReferenceFolder,
});
break;
default:
os.alert({
type: 'error',
text: i18n.ts.somethingHappened
text: i18n.ts.somethingHappened,
});
}
});
@@ -278,17 +279,17 @@ function urlUpload() {
os.inputText({
title: i18n.ts.uploadFromUrl,
type: 'url',
placeholder: i18n.ts.uploadFromUrlDescription
placeholder: i18n.ts.uploadFromUrlDescription,
}).then(({ canceled, result: url }) => {
if (canceled || !url) return;
os.api('drive/files/upload-from-url', {
url: url,
folderId: folder.value ? folder.value.id : undefined
folderId: folder.value ? folder.value.id : undefined,
});
os.alert({
title: i18n.ts.uploadFromUrlRequested,
text: i18n.ts.uploadFromUrlMayTakeTime
text: i18n.ts.uploadFromUrlMayTakeTime,
});
});
}
@@ -296,12 +297,12 @@ function urlUpload() {
function createFolder() {
os.inputText({
title: i18n.ts.createFolder,
placeholder: i18n.ts.folderName
placeholder: i18n.ts.folderName,
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/folders/create', {
name: name,
parentId: folder.value ? folder.value.id : undefined
parentId: folder.value ? folder.value.id : undefined,
}).then(createdFolder => {
addFolder(createdFolder, true);
});
@@ -312,12 +313,12 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
os.inputText({
title: i18n.ts.renameFolder,
placeholder: i18n.ts.inputNewFolderName,
default: folderToRename.name
default: folderToRename.name,
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/folders/update', {
folderId: folderToRename.id,
name: name
name: name,
}).then(updatedFolder => {
// FIXME: 画面を更新するために自分自身に移動
move(updatedFolder);
@@ -327,7 +328,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
os.api('drive/folders/delete', {
folderId: folderToDelete.id
folderId: folderToDelete.id,
}).then(() => {
// 削除時に親フォルダに移動
move(folderToDelete.parentId);
@@ -337,15 +338,15 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
os.alert({
type: 'error',
title: i18n.ts.unableToDelete,
text: i18n.ts.hasChildFilesOrFolders
text: i18n.ts.hasChildFilesOrFolders,
});
break;
default:
os.alert({
type: 'error',
text: i18n.ts.unableToDelete
text: i18n.ts.unableToDelete,
});
}
}
});
}
@@ -411,7 +412,7 @@ function move(target?: Misskey.entities.DriveFolder) {
fetching.value = true;
os.api('drive/folders/show', {
folderId: target
folderId: target,
}).then(folderToMove => {
folder.value = folderToMove;
hierarchyFolders.value = [];
@@ -510,7 +511,7 @@ async function fetch() {
const foldersPromise = os.api('drive/folders', {
folderId: folder.value ? folder.value.id : null,
limit: foldersMax + 1
limit: foldersMax + 1,
}).then(fetchedFolders => {
if (fetchedFolders.length === foldersMax + 1) {
moreFolders.value = true;
@@ -522,7 +523,7 @@ async function fetch() {
const filesPromise = os.api('drive/files', {
folderId: folder.value ? folder.value.id : null,
type: props.type,
limit: filesMax + 1
limit: filesMax + 1,
}).then(fetchedFiles => {
if (fetchedFiles.length === filesMax + 1) {
moreFiles.value = true;
@@ -549,7 +550,7 @@ function fetchMoreFiles() {
folderId: folder.value ? folder.value.id : null,
type: props.type,
untilId: files.value[files.value.length - 1].id,
limit: max + 1
limit: max + 1,
}).then(files => {
if (files.length === max + 1) {
moreFiles.value = true;
@@ -569,30 +570,30 @@ function getMenu() {
ref: keepOriginal,
}, null, {
text: i18n.ts.addFile,
type: 'label'
type: 'label',
}, {
text: i18n.ts.upload,
icon: 'fas fa-upload',
action: () => { selectLocalFile(); }
action: () => { selectLocalFile(); },
}, {
text: i18n.ts.fromUrl,
icon: 'fas fa-link',
action: () => { urlUpload(); }
action: () => { urlUpload(); },
}, null, {
text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label'
type: 'label',
}, folder.value ? {
text: i18n.ts.renameFolder,
icon: 'fas fa-i-cursor',
action: () => { renameFolder(folder.value); }
action: () => { renameFolder(folder.value); },
} : undefined, folder.value ? {
text: i18n.ts.deleteFolder,
icon: 'fas fa-trash-alt',
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
} : undefined, {
text: i18n.ts.createFolder,
icon: 'fas fa-folder-plus',
action: () => { createFolder(); }
action: () => { createFolder(); },
}];
}
@@ -662,14 +663,14 @@ onBeforeUnmount(() => {
> .path {
display: inline-block;
vertical-align: bottom;
line-height: 50px;
line-height: 42px;
white-space: nowrap;
> * {
display: inline-block;
margin: 0;
padding: 0 8px;
line-height: 50px;
line-height: 42px;
cursor: pointer;
* {

View File

@@ -41,7 +41,7 @@ import { useInterval } from '@/scripts/use-interval';
const props = defineProps<{
modelValue: string | number;
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time';
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search';
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@@ -77,9 +77,9 @@ const inputEl = ref<HTMLElement>();
const prefixEl = ref<HTMLElement>();
const suffixEl = ref<HTMLElement>();
const height =
props.small ? 38 :
props.large ? 42 :
40;
props.small ? 36 :
props.large ? 40 :
38;
const focus = () => inputEl.value.focus();
const onInput = (ev: KeyboardEvent) => {

View File

@@ -189,7 +189,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
height: 3px;
background: rgba(0, 0, 0, 0.1);
border-radius: 999px;
overflow: hidden; overflow: clip;
overflow: clip;
> .highlight {
position: absolute;

View File

@@ -63,9 +63,9 @@ const prefixEl = ref(null);
const suffixEl = ref(null);
const container = ref(null);
const height =
props.small ? 38 :
props.large ? 42 :
40;
props.small ? 36 :
props.large ? 40 :
38;
const focus = () => inputEl.value.focus();
const onInput = (ev) => {

View File

@@ -3,7 +3,7 @@
<div v-if="!showMenu" class="main" :class="ad.place">
<a :href="ad.url" target="_blank">
<img :src="ad.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle info-circle"></span></button>
</a>
</div>
<div v-else class="menu">
@@ -135,13 +135,19 @@ export default defineComponent({
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
> .menu {
position: absolute;
top: 0;
right: 0;
background: var(--panel);
top: 1px;
right: 1px;
> .info-circle {
border: 3px solid var(--panel);
border-radius: 50%;
background: var(--panel);
}
}
}

View File

@@ -1,68 +1,41 @@
char2filePath<template>
<template>
<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/>
<img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt" decoding="async"/>
<span v-else-if="char && useOsNativeEmojis">{{ char }}</span>
<span v-else>{{ emoji }}</span>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch } from 'vue';
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { CustomEmoji } from 'misskey-js/built/entities';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { char2filePath } from '@/scripts/twemoji-base';
import { defaultStore } from '@/store';
import { instance } from '@/instance';
export default defineComponent({
props: {
emoji: {
type: String,
required: true
},
normal: {
type: Boolean,
required: false,
default: false
},
noStyle: {
type: Boolean,
required: false,
default: false
},
customEmojis: {
required: false
},
isReaction: {
type: Boolean,
default: false
},
},
const props = defineProps<{
emoji: string;
normal?: boolean;
noStyle?: boolean;
customEmojis?: CustomEmoji[];
isReaction?: boolean;
}>();
setup(props) {
const isCustom = computed(() => props.emoji.startsWith(':'));
const char = computed(() => isCustom.value ? null : props.emoji);
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction);
const ce = computed(() => props.customEmojis || instance.emojis || []);
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
const url = computed(() => {
if (char.value) {
return char2filePath(char.value);
} else {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.value.url)
: customEmoji.value.url;
}
});
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
return {
url,
char,
alt,
customEmoji,
useOsNativeEmojis,
};
},
const isCustom = computed(() => props.emoji.startsWith(':'));
const char = computed(() => isCustom.value ? null : props.emoji);
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction);
const ce = computed(() => props.customEmojis ?? instance.emojis ?? []);
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
const url = computed(() => {
if (char.value) {
return char2filePath(char.value);
} else {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.value.url)
: customEmoji.value.url;
}
});
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
</script>
<style lang="scss" scoped>

View File

@@ -16,9 +16,7 @@
</template>
<script lang="ts" setup>
import { useCssModule } from 'vue';
useCssModule();
import { } from 'vue';
const props = withDefaults(defineProps<{
inline?: boolean;

View File

@@ -18,7 +18,7 @@
</div>
</div>
<div v-if="!narrow || hideTitle" class="tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button>
@@ -27,7 +27,7 @@
</template>
<div class="buttons right">
<template v-for="action in actions">
<button v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
<button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</div>
</div>

View File

@@ -1,5 +1,6 @@
<template>
<component :is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
<component
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
@contextmenu.stop="() => {}"
>
<template v-if="!self">
@@ -23,14 +24,7 @@ import { toUnicode as decodePunycode } from 'punycode/';
import { url as local } from '@/config';
import * as os from '@/os';
import { useTooltip } from '@/scripts/use-tooltip';
function safeURIDecode(str: string) {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
import { safeURIDecode } from '@/scripts/safe-uri-decode';
export default defineComponent({
props: {
@@ -42,7 +36,7 @@ export default defineComponent({
type: String,
required: false,
default: null,
}
},
},
setup(props) {
const self = props.url.startsWith(local);

View File

@@ -36,7 +36,7 @@
<script lang="ts" setup>
import { } from 'vue';
import MkModal from '@/components/ui/modal.vue';
import { menuDef } from '@/menu';
import { navbarItemDef } from '@/navbar';
import { instanceName } from '@/config';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -62,7 +62,7 @@ const modal = $ref<InstanceType<typeof MkModal>>();
const menu = defaultStore.state.menu;
const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
type: def.to ? 'link' : 'button',
text: i18n.ts[def.title],
icon: def.icon,

View File

@@ -76,7 +76,7 @@ export default {
<style lang="scss" module>
.wrap {
overflow: hidden; overflow: clip;
overflow: clip;
animation-play-state: running;
&:hover {

View File

@@ -1,22 +1,22 @@
<template>
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { isMe }]" :to="url" :style="{ background: bgCss }">
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }">
<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
<span class="main">
<span class="username">@{{ username }}</span>
<span v-if="(host != localHost) || $store.state.showFullAcct" :class="$style.mainHost">@{{ toUnicode(host) }}</span>
<span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span>
</span>
</MkA>
<a v-else :class="$style.root" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }">
<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }">
<span class="main">
<span class="username">@{{ username }}</span>
<span :class="$style.mainHost">@{{ toUnicode(host) }}</span>
<span class="host">@{{ toUnicode(host) }}</span>
</span>
</a>
</template>
<script lang="ts" setup>
import { toUnicode } from 'punycode';
import { useCssModule } from 'vue';
import { } from 'vue';
import tinycolor from 'tinycolor2';
import { host as localHost } from '@/config';
import { $i } from '@/account';
@@ -37,12 +37,10 @@ const isMe = $i && (
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
useCssModule();
</script>
<style lang="scss" module>
.root {
<style lang="scss" scoped>
.akbvjaqn {
display: inline-block;
padding: 4px 8px 4px 4px;
border-radius: 999px;
@@ -51,18 +49,18 @@ useCssModule();
&.isMe {
color: var(--mentionMe);
}
}
.icon {
width: 1.5em;
height: 1.5em;
object-fit: cover;
margin: 0 0.2em 0 0;
vertical-align: bottom;
border-radius: 100%;
}
> .icon {
width: 1.5em;
height: 1.5em;
object-fit: cover;
margin: 0 0.2em 0 0;
vertical-align: bottom;
border-radius: 100%;
}
.mainHost {
opacity: 0.5;
> .main > .host {
opacity: 0.5;
}
}
</style>

View File

@@ -47,7 +47,7 @@ export default defineComponent({
render() {
if (this.text == null || this.text === '') return;
const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text, { fnNameList: MFM_TAGS });
const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text, { fnNameList: MFM_TAGS });
const validTime = (t: string | null | undefined) => {
if (t == null) return null;
@@ -312,6 +312,10 @@ export default defineComponent({
})];
}
case 'plain': {
return [h('span', genEl(token.children))];
}
default: {
console.error('unrecognized ast type:', token.type);

View File

@@ -390,7 +390,7 @@ if (appearNote.replyId) {
> .article {
padding: 32px;
font-size: 1.1em;
font-size: 1.2em;
> .header {
display: flex;

View File

@@ -27,7 +27,7 @@ const props = defineProps<{
display: flex;
margin: 0;
padding: 0;
overflow: hidden; overflow: clip;
overflow: clip;
font-size: 0.95em;
&.min-width_350px {

View File

@@ -36,7 +36,7 @@ const showContent = $ref(false);
display: flex;
margin: 0;
padding: 0;
overflow: hidden; overflow: clip;
overflow: clip;
font-size: 0.95em;
&.min-width_350px {

View File

@@ -293,7 +293,7 @@ function readPromo() {
position: relative;
transition: box-shadow 0.1s ease;
font-size: 1.05em;
overflow: hidden; overflow: clip;
overflow: clip;
contain: content;
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
@@ -554,6 +554,13 @@ function readPromo() {
&.max-width_500px {
font-size: 0.9em;
> .article {
> .avatar {
width: 50px;
height: 50px;
}
}
}
&.max-width_450px {
@@ -570,8 +577,8 @@ function readPromo() {
> .avatar {
margin: 0 10px 8px 0;
width: 50px;
height: 50px;
width: 46px;
height: 46px;
top: calc(14px + var(--stickyTop, 0px));
}
}
@@ -592,8 +599,6 @@ function readPromo() {
}
&.max-width_300px {
font-size: 0.825em;
> .article {
> .avatar {
width: 44px;

View File

@@ -177,13 +177,7 @@ useTooltip(reactionRef, (showing) => {
&.max-width_500px {
padding: 12px;
font-size: 0.8em;
}
&:after {
content: "";
display: block;
clear: both;
font-size: 0.85em;
}
> .head {

View File

@@ -1,255 +1,234 @@
<template>
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
<template v-if="meta">
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</MkInput>
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #caption>
<span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</MkInput>
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #caption>
<span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
</template>
</MkInput>
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix><i class="fas fa-envelope"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ $ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
</template>
</MkInput>
<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
<I18n :src="$ts.agreeTo">
<template #0>
<a :href="instance.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a>
</template>
</MkInput>
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix><i class="fas fa-envelope"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ $ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
</template>
</MkInput>
<MkSwitch v-if="meta.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
<I18n :src="$ts.agreeTo">
<template #0>
<a :href="meta.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a>
</template>
</I18n>
</MkSwitch>
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
</template>
</I18n>
</MkSwitch>
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
</form>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import getPasswordStrength from 'syuilo-password-strength';
import { toUnicode } from 'punycode/';
import MkButton from './ui/button.vue';
import MkCaptcha from './captcha.vue';
import MkInput from './form/input.vue';
import MkSwitch from './form/switch.vue';
import { host, url } from '@/config';
import * as config from '@/config';
import * as os from '@/os';
import { login } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkInput,
MkSwitch,
MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')),
},
props: {
autoSet: {
type: Boolean,
required: false,
default: false,
},
},
emits: ['signup'],
data() {
return {
host: toUnicode(host),
username: '',
password: '',
retypedPassword: '',
invitationCode: '',
email: '',
url,
usernameState: null,
emailState: null,
passwordStrength: '',
passwordRetypeState: null,
submitting: false,
ToSAgreement: false,
hCaptchaResponse: null,
reCaptchaResponse: null,
};
},
computed: {
meta() {
return this.$instance;
},
shouldDisableSubmitting(): boolean {
return this.submitting ||
this.meta.tosUrl && !this.ToSAgreement ||
this.meta.enableHcaptcha && !this.hCaptchaResponse ||
this.meta.enableRecaptcha && !this.reCaptchaResponse ||
this.passwordRetypeState === 'not-match';
},
shouldShowProfileUrl(): boolean {
return (this.username !== '' &&
this.usernameState !== 'invalid-format' &&
this.usernameState !== 'min-range' &&
this.usernameState !== 'max-range');
},
},
methods: {
onChangeUsername() {
if (this.username === '') {
this.usernameState = null;
return;
}
const err =
!this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
this.username.length < 1 ? 'min-range' :
this.username.length > 20 ? 'max-range' :
null;
if (err) {
this.usernameState = err;
return;
}
this.usernameState = 'wait';
os.api('username/available', {
username: this.username,
}).then(result => {
this.usernameState = result.available ? 'ok' : 'unavailable';
}).catch(err => {
this.usernameState = 'error';
});
},
onChangeEmail() {
if (this.email === '') {
this.emailState = null;
return;
}
this.emailState = 'wait';
os.api('email-address/available', {
emailAddress: this.email,
}).then(result => {
this.emailState = result.available ? 'ok' :
result.reason === 'used' ? 'unavailable:used' :
result.reason === 'format' ? 'unavailable:format' :
result.reason === 'disposable' ? 'unavailable:disposable' :
result.reason === 'mx' ? 'unavailable:mx' :
result.reason === 'smtp' ? 'unavailable:smtp' :
'unavailable';
}).catch(err => {
this.emailState = 'error';
});
},
onChangePassword() {
if (this.password === '') {
this.passwordStrength = '';
return;
}
const strength = getPasswordStrength(this.password);
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
},
onChangePasswordRetype() {
if (this.retypedPassword === '') {
this.passwordRetypeState = null;
return;
}
this.passwordRetypeState = this.password === this.retypedPassword ? 'match' : 'not-match';
},
onSubmit() {
if (this.submitting) return;
this.submitting = true;
os.api('signup', {
username: this.username,
password: this.password,
emailAddress: this.email,
invitationCode: this.invitationCode,
'hcaptcha-response': this.hCaptchaResponse,
'g-recaptcha-response': this.reCaptchaResponse,
}).then(() => {
if (this.meta.emailRequiredForSignup) {
os.alert({
type: 'success',
title: this.$ts._signup.almostThere,
text: this.$t('_signup.emailSent', { email: this.email }),
});
this.$emit('signupEmailPending');
} else {
os.api('signin', {
username: this.username,
password: this.password,
}).then(res => {
this.$emit('signup', res);
if (this.autoSet) {
login(res.i);
}
});
}
}).catch(() => {
this.submitting = false;
this.$refs.hcaptcha?.reset?.();
this.$refs.recaptcha?.reset?.();
os.alert({
type: 'error',
text: this.$ts.somethingHappened,
});
});
},
},
const props = withDefaults(defineProps<{
autoSet?: boolean;
}>(), {
autoSet: false,
});
const emit = defineEmits<{
(ev: 'signup', user: Record<string, any>): void;
(ev: 'signupEmailPending'): void;
}>();
const host = toUnicode(config.host);
let hcaptcha = $ref();
let recaptcha = $ref();
let username: string = $ref('');
let password: string = $ref('');
let retypedPassword: string = $ref('');
let invitationCode: string = $ref('');
let email = $ref('');
let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
let submitting: boolean = $ref(false);
let ToSAgreement: boolean = $ref(false);
let hCaptchaResponse = $ref(null);
let reCaptchaResponse = $ref(null);
const shouldDisableSubmitting = $computed((): boolean => {
return submitting ||
instance.tosUrl && !ToSAgreement ||
instance.enableHcaptcha && !hCaptchaResponse ||
instance.enableRecaptcha && !reCaptchaResponse ||
passwordRetypeState === 'not-match';
});
function onChangeUsername(): void {
if (username === '') {
usernameState = null;
return;
}
{
const err =
!username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
username.length < 1 ? 'min-range' :
username.length > 20 ? 'max-range' :
null;
if (err) {
usernameState = err;
return;
}
}
usernameState = 'wait';
os.api('username/available', {
username,
}).then(result => {
usernameState = result.available ? 'ok' : 'unavailable';
}).catch(() => {
usernameState = 'error';
});
}
function onChangeEmail(): void {
if (email === '') {
emailState = null;
return;
}
emailState = 'wait';
os.api('email-address/available', {
emailAddress: email,
}).then(result => {
emailState = result.available ? 'ok' :
result.reason === 'used' ? 'unavailable:used' :
result.reason === 'format' ? 'unavailable:format' :
result.reason === 'disposable' ? 'unavailable:disposable' :
result.reason === 'mx' ? 'unavailable:mx' :
result.reason === 'smtp' ? 'unavailable:smtp' :
'unavailable';
}).catch(() => {
emailState = 'error';
});
}
function onChangePassword(): void {
if (password === '') {
passwordStrength = '';
return;
}
const strength = getPasswordStrength(password);
passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
}
function onChangePasswordRetype(): void {
if (retypedPassword === '') {
passwordRetypeState = null;
return;
}
passwordRetypeState = password === retypedPassword ? 'match' : 'not-match';
}
function onSubmit(): void {
if (submitting) return;
submitting = true;
os.api('signup', {
username,
password,
emailAddress: email,
invitationCode,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
}).then(() => {
if (instance.emailRequiredForSignup) {
os.alert({
type: 'success',
title: i18n.ts._signup.almostThere,
text: i18n.t('_signup.emailSent', { email }),
});
emit('signupEmailPending');
} else {
os.api('signin', {
username,
password,
}).then(res => {
emit('signup', res);
if (props.autoSet) {
login(res.i);
}
});
}
}).catch(() => {
submitting = false;
hcaptcha.reset?.();
recaptcha.reset?.();
os.alert({
type: 'error',
text: i18n.ts.somethingHappened,
});
});
}
</script>
<style lang="scss" scoped>

View File

@@ -71,7 +71,7 @@ defineExpose({
<style lang="scss" scoped>
.meijqfqm {
position: relative;
overflow: hidden; overflow: clip;
overflow: clip;
display: grid;
place-items: center;

View File

@@ -54,7 +54,7 @@ onMounted(() => {
width: min-content;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
border-radius: 8px;
overflow: hidden; overflow: clip;
overflow: clip;
text-align: center;
pointer-events: none;

View File

@@ -1,5 +1,6 @@
<template>
<button v-if="!link" class="bghgjjyj _button"
<button
v-if="!link" class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full }"
:type="type"
@click="$emit('click', $event)"
@@ -10,7 +11,8 @@
<slot></slot>
</div>
</button>
<MkA v-else class="bghgjjyj _button"
<MkA
v-else class="bghgjjyj _button"
:class="{ inline, primary, gradate, danger, rounded, full }"
:to="to"
@mousedown="onMousedown"
@@ -29,56 +31,56 @@ export default defineComponent({
props: {
type: {
type: String,
required: false
required: false,
},
primary: {
type: Boolean,
required: false,
default: false
default: false,
},
gradate: {
type: Boolean,
required: false,
default: false
default: false,
},
rounded: {
type: Boolean,
required: false,
default: false
default: false,
},
inline: {
type: Boolean,
required: false,
default: false
default: false,
},
link: {
type: Boolean,
required: false,
default: false
default: false,
},
to: {
type: String,
required: false
required: false,
},
autofocus: {
type: Boolean,
required: false,
default: false
default: false,
},
wait: {
type: Boolean,
required: false,
default: false
default: false,
},
danger: {
type: Boolean,
required: false,
default: false
default: false,
},
full: {
type: Boolean,
required: false,
default: false
default: false,
},
},
emits: ['click'],
@@ -127,8 +129,8 @@ export default defineComponent({
window.setTimeout(() => {
if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
}, 2000);
}
}
},
},
});
</script>
@@ -142,13 +144,12 @@ export default defineComponent({
padding: 8px 14px;
text-align: center;
font-weight: normal;
font-size: 0.9em;
line-height: 22px;
font-size: 1em;
box-shadow: none;
text-decoration: none;
background: var(--buttonBg);
border-radius: 5px;
overflow: hidden; overflow: clip;
overflow: clip;
box-sizing: border-box;
transition: background 0.1s ease;

View File

@@ -143,7 +143,7 @@ export default defineComponent({
.ukygtjoj {
position: relative;
overflow: hidden; overflow: clip;
overflow: clip;
contain: content;
&.naked {

View File

@@ -140,7 +140,7 @@ function focusDown() {
width: 100%;
box-sizing: border-box;
white-space: nowrap;
font-size: 0.85em;
font-size: 0.9em;
line-height: 20px;
text-align: left;
overflow: hidden;

View File

@@ -98,7 +98,7 @@ defineExpose({
}
> .header {
$height: 58px;
$height: 46px;
$height-narrow: 42px;
display: flex;
flex-shrink: 0;
@@ -138,6 +138,7 @@ defineExpose({
}
> .body {
flex: 1;
overflow: auto;
background: var(--panel);
}

View File

@@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import { nextTick, onMounted, computed, ref, watch, provide } from 'vue';
import { nextTick, onMounted, watch, provide } from 'vue';
import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { defaultStore } from '@/store';
@@ -57,13 +57,13 @@ const emit = defineEmits<{
provide('modal', true);
const maxHeight = ref<number>();
const fixed = ref(false);
const transformOrigin = ref('center');
const showing = ref(true);
const content = ref<HTMLElement>();
let maxHeight = $ref<number>();
let fixed = $ref(false);
let transformOrigin = $ref('center');
let showing = $ref(true);
let content = $ref<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority);
const type = computed(() => {
const type = $computed(() => {
if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
return 'drawer';
@@ -80,7 +80,7 @@ let contentClicking = false;
const close = () => {
// eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto';
showing.value = false;
showing = false;
emit('close');
};
@@ -89,8 +89,8 @@ const onBgClick = () => {
emit('click');
};
if (type.value === 'drawer') {
maxHeight.value = window.innerHeight / 1.5;
if (type === 'drawer') {
maxHeight = window.innerHeight / 1.5;
}
const keymap = {
@@ -101,22 +101,21 @@ const MARGIN = 16;
const align = () => {
if (props.src == null) return;
if (type.value === 'drawer') return;
if (type.value === 'dialog') return;
if (type === 'drawer') return;
if (type === 'dialog') return;
const popover = content.value!;
if (popover == null) return;
if (content == null) return;
const srcRect = props.src.getBoundingClientRect();
const width = popover.offsetWidth;
const height = popover.offsetHeight;
const width = content!.offsetWidth;
const height = content!.offsetHeight;
let left;
let top;
const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
const x = srcRect.left + (fixed ? 0 : window.pageXOffset);
const y = srcRect.top + (fixed ? 0 : window.pageYOffset);
if (props.anchor.x === 'center') {
left = x + (props.src.offsetWidth / 2) - (width / 2);
@@ -134,7 +133,7 @@ const align = () => {
top = y + props.src.offsetHeight;
}
if (fixed.value) {
if (fixed) {
// 画面から横にはみ出る場合
if (left + width > window.innerWidth) {
left = window.innerWidth - width;
@@ -147,16 +146,16 @@ const align = () => {
if (top + height > (window.innerHeight - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
maxHeight = underSpace;
} else {
maxHeight.value = upperSpace;
maxHeight = upperSpace;
top = (upperSpace + MARGIN) - height;
}
} else {
top = (window.innerHeight - MARGIN) - height;
}
} else {
maxHeight.value = underSpace;
maxHeight = underSpace;
}
} else {
// 画面から横にはみ出る場合
@@ -171,16 +170,16 @@ const align = () => {
if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
maxHeight = underSpace;
} else {
maxHeight.value = upperSpace;
maxHeight = upperSpace;
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
}
} else {
top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
}
} else {
maxHeight.value = underSpace;
maxHeight = underSpace;
}
}
@@ -195,29 +194,29 @@ const align = () => {
let transformOriginX = 'center';
let transformOriginY = 'center';
if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) {
if (top >= srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)) {
transformOriginY = 'top';
} else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) {
} else if ((top + height) <= srcRect.top + (fixed ? 0 : window.pageYOffset)) {
transformOriginY = 'bottom';
}
if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) {
if (left >= srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)) {
transformOriginX = 'left';
} else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) {
} else if ((left + width) <= srcRect.left + (fixed ? 0 : window.pageXOffset)) {
transformOriginX = 'right';
}
transformOrigin.value = `${transformOriginX} ${transformOriginY}`;
transformOrigin = `${transformOriginX} ${transformOriginY}`;
popover.style.left = left + 'px';
popover.style.top = top + 'px';
content.style.left = left + 'px';
content.style.top = top + 'px';
};
const onOpened = () => {
emit('opened');
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
const el = content.value!.children[0];
const el = content!.children[0];
el.addEventListener('mousedown', ev => {
contentClicking = true;
window.addEventListener('mouseup', ev => {
@@ -235,7 +234,7 @@ onMounted(() => {
// eslint-disable-next-line vue/no-mutating-props
props.src.style.pointerEvents = 'none';
}
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
fixed = (type === 'drawer') || (getFixedContainer(props.src) != null);
await nextTick();
@@ -243,10 +242,9 @@ onMounted(() => {
}, { immediate: true });
nextTick(() => {
const popover = content.value;
new ResizeObserver((entries, observer) => {
align();
}).observe(popover!);
}).observe(content!);
});
});
@@ -389,7 +387,7 @@ defineExpose({
left: 0;
width: 100%;
height: 100%;
overflow: hidden; overflow: clip;
overflow: clip;
> .content {
position: fixed;

View File

@@ -30,7 +30,7 @@ export default defineComponent({
props: {
def: {
type: Array,
required: true
required: true,
},
grid: {
type: Boolean,
@@ -64,7 +64,7 @@ export default defineComponent({
box-sizing: border-box;
padding: 10px 16px 10px 8px;
border-radius: 9px;
font-size: 0.95em;
font-size: 0.9em;
&:hover {
text-decoration: none;

View File

@@ -116,7 +116,7 @@ const setPosition = () => {
let top: number;
if (props.targetElement) {
left = (rect.left + window.pageXOffset) + props.innerMargin;
left = (rect.left + props.targetElement.offsetWidth + window.pageXOffset) + props.innerMargin;
top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
} else {
left = props.x + props.innerMargin;

View File

@@ -393,7 +393,7 @@ export default defineComponent({
border-radius: var(--radius);
> .header {
--height: 45px;
--height: 42px;
&.mini {
--height: 38px;

View File

@@ -105,7 +105,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
}
&.active {
color: #fff;
color: var(--fgOnAccent);
background: var(--accent);
}

View File

@@ -4,7 +4,7 @@
<header>
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" class="mk-widget-select">
<template #label>{{ $ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ $t(`_widgets.${widget}`) }}</option>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
</MkSelect>
<MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
@@ -26,79 +26,91 @@
</template>
</XDraggable>
</template>
<component :is="`mkw-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" class="widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)"/>
<component :is="`mkw-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" class="widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent, reactive, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid';
import MkSelect from '@/components/form/select.vue';
import MkButton from '@/components/ui/button.vue';
import { widgets as widgetDefs } from '@/widgets';
import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
XDraggable: defineAsyncComponent(() => import('vuedraggable')),
MkSelect,
MkButton,
},
const XDraggable = defineAsyncComponent(() => import('vuedraggable'));
props: {
widgets: {
type: Array,
required: true,
},
edit: {
type: Boolean,
required: true,
},
},
type Widget = {
name: string;
id: string;
data: Record<string, any>;
};
emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
const props = defineProps<{
widgets: Widget[];
edit: boolean;
}>();
setup(props, context) {
const widgetRefs = reactive({});
const configWidget = (id: string) => {
widgetRefs[id].configure();
};
const widgetAdderSelected = ref(null);
const addWidget = () => {
if (widgetAdderSelected.value == null) return;
const emit = defineEmits<{
(ev: 'updateWidgets', widgets: Widget[]): void;
(ev: 'addWidget', widget: Widget): void;
(ev: 'removeWidget', widget: Widget): void;
(ev: 'updateWidget', widget: Partial<Widget>): void;
(ev: 'exit'): void;
}>();
context.emit('addWidget', {
name: widgetAdderSelected.value,
id: uuid(),
data: {},
});
const widgetRefs = {};
const configWidget = (id: string) => {
widgetRefs[id].configure();
};
const widgetAdderSelected = ref(null);
const addWidget = () => {
if (widgetAdderSelected.value == null) return;
widgetAdderSelected.value = null;
};
const removeWidget = (widget) => {
context.emit('removeWidget', widget);
};
const updateWidget = (id, data) => {
context.emit('updateWidget', { id, data });
};
const widgets_ = computed({
get: () => props.widgets,
set: (value) => {
context.emit('updateWidgets', value);
},
});
emit('addWidget', {
name: widgetAdderSelected.value,
id: uuid(),
data: {},
});
return {
widgetRefs,
configWidget,
widgetAdderSelected,
widgetDefs,
addWidget,
removeWidget,
updateWidget,
widgets_,
};
widgetAdderSelected.value = null;
};
const removeWidget = (widget) => {
emit('removeWidget', widget);
};
const updateWidget = (id, data) => {
emit('updateWidget', { id, data });
};
const widgets_ = computed({
get: () => props.widgets,
set: (value) => {
emit('updateWidgets', value);
},
});
function onContextmenu(widget: Widget, ev: MouseEvent) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection()?.toString() !== '') return;
os.contextMenu([{
type: 'label',
text: i18n.t(`_widgets.${widget.name}`),
}, {
icon: 'fas fa-cog',
text: i18n.ts.settings,
action: () => {
configWidget(widget.id);
},
}], ev);
}
</script>
<style lang="scss" scoped>

View File

@@ -8,7 +8,6 @@ import tooltip from './tooltip';
import hotkey from './hotkey';
import appear from './appear';
import anim from './anim';
import stickyContainer from './sticky-container';
import clickAnime from './click-anime';
import panel from './panel';
import adaptiveBorder from './adaptive-border';
@@ -24,7 +23,6 @@ export default function(app: App) {
app.directive('appear', appear);
app.directive('anim', anim);
app.directive('click-anime', clickAnime);
app.directive('sticky-container', stickyContainer);
app.directive('panel', panel);
app.directive('adaptive-border', adaptiveBorder);
}

View File

@@ -27,8 +27,8 @@ function getClassOrder(width: number, queue: Value): ClassOrder {
...(queue.min ? queue.min.filter(v => width >= v).map(getMinClass) : []),
],
remove: [
...(queue.max ? queue.max.filter(v => width > v).map(getMaxClass) : []),
...(queue.min ? queue.min.filter(v => width < v).map(getMinClass) : []),
...(queue.max ? queue.max.filter(v => width > v).map(getMaxClass) : []),
...(queue.min ? queue.min.filter(v => width < v).map(getMinClass) : []),
]
};
}

View File

@@ -1,17 +0,0 @@
import { Directive } from 'vue';
export default {
mounted(src, binding, vn) {
//const query = binding.value;
const header = src.children[0];
const body = src.children[1];
const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px';
src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
if (body) body.dataset.stickyContainerHeaderHeight = header.offsetHeight.toString();
header.style.setProperty('--stickyTop', currentStickyTop);
header.style.position = 'sticky';
header.style.top = 'var(--stickyTop)';
header.style.zIndex = '1';
},
} as Directive;

View File

@@ -7,10 +7,11 @@ import { popup, alert } from '@/os';
const start = isTouchUsing ? 'touchstart' : 'mouseover';
const end = isTouchUsing ? 'touchend' : 'mouseleave';
const delay = 100;
export default {
mounted(el: HTMLElement, binding, vn) {
const delay = binding.modifiers.noDelay ? 0 : 100;
const self = (el as any)._tooltipDirective_ = {} as any;
self.text = binding.value as string;

View File

@@ -39,401 +39,403 @@ import { reactionPicker } from '@/scripts/reaction-picker';
import { getUrlWithoutLoginId } from '@/scripts/login-id';
import { getAccountFromId } from '@/scripts/get-account-from-id';
console.info(`Misskey v${version}`);
(async () => {
console.info(`Misskey v${version}`);
if (_DEV_) {
console.warn('Development mode!!!');
if (_DEV_) {
console.warn('Development mode!!!');
console.info(`vue ${vueVersion}`);
console.info(`vue ${vueVersion}`);
(window as any).$i = $i;
(window as any).$store = defaultStore;
(window as any).$i = $i;
(window as any).$store = defaultStore;
window.addEventListener('error', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled error',
text: event.message
window.addEventListener('error', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled error',
text: event.message
});
*/
});
*/
window.addEventListener('unhandledrejection', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled promise rejection',
text: event.reason
});
*/
});
}
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;
else location.reload();
});
window.addEventListener('unhandledrejection', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled promise rejection',
text: event.reason
});
*/
});
}
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;
else location.reload();
});
//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
// TODO: いつの日にか消したい
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
window.addEventListener('resize', () => {
//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
// TODO: いつの日にか消したい
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
});
//#endregion
window.addEventListener('resize', () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
});
//#endregion
// If mobile, insert the viewport meta tag
if (['smartphone', 'tablet'].includes(deviceKind)) {
const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
}
//#region Set lang attr
const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
//#region loginId
const params = new URLSearchParams(location.search);
const loginId = params.get('loginId');
if (loginId) {
const target = getUrlWithoutLoginId(location.href);
if (!$i || $i.id !== loginId) {
const account = await getAccountFromId(loginId);
if (account) {
await login(account.token, target);
}
// If mobile, insert the viewport meta tag
if (['smartphone', 'tablet'].includes(deviceKind)) {
const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
}
history.replaceState({ misskey: 'loginId' }, '', target);
}
//#region Set lang attr
const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
//#endregion
//#region loginId
const params = new URLSearchParams(location.search);
const loginId = params.get('loginId');
//#region Fetch user
if ($i && $i.token) {
if (_DEV_) {
console.log('account cache found. refreshing...');
}
if (loginId) {
const target = getUrlWithoutLoginId(location.href);
refreshAccount();
} else {
if (_DEV_) {
console.log('no account cache found.');
}
// 連携ログインの場合用にCookieを参照する
const i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1];
if (i != null && i !== 'null') {
if (_DEV_) {
console.log('signing...');
}
try {
document.body.innerHTML = '<div>Please wait...</div>';
await login(i);
} catch (err) {
// Render the error screen
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
document.body.innerHTML = '<div id="err">Oops!</div>';
}
} else {
if (_DEV_) {
console.log('not signed in');
}
}
}
//#endregion
const fetchInstanceMetaPromise = fetchInstance();
fetchInstanceMetaPromise.then(() => {
localStorage.setItem('v', instance.version);
// Init service worker
initializeSw();
});
const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
defineAsyncComponent(() => import('@/ui/universal.vue')),
);
if (_DEV_) {
app.config.performance = true;
}
app.config.globalProperties = {
$i,
$store: defaultStore,
$instance: instance,
$t: i18n.t,
$ts: i18n.ts,
};
widgets(app);
directives(app);
components(app);
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する
const rootEl = (() => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentEl) {
console.warn('multiple import detected');
return currentEl;
}
const rootEl = document.createElement('div');
rootEl.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(rootEl);
return rootEl;
})();
app.mount(rootEl);
// boot.jsのやつを解除
window.onerror = null;
window.onunhandledrejection = null;
reactionPicker.init();
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
}
// クライアントが更新されたか?
const lastVersion = localStorage.getItem('lastVersion');
if (lastVersion !== version) {
localStorage.setItem('lastVersion', version);
// テーマリビルドするため
localStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
// ログインしてる場合だけ
if ($i) {
popup(defineAsyncComponent(() => import('@/components/updated.vue')), {}, {}, 'closed');
if (!$i || $i.id !== loginId) {
const account = await getAccountFromId(loginId);
if (account) {
await login(account.token, target);
}
}
} catch (err) {
history.replaceState({ misskey: 'loginId' }, '', target);
}
}
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
}, { immediate: localStorage.theme == null });
//#endregion
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
//#region Fetch user
if ($i && $i.token) {
if (_DEV_) {
console.log('account cache found. refreshing...');
}
watch(darkTheme, (theme) => {
if (defaultStore.state.darkMode) {
applyTheme(theme);
}
});
watch(lightTheme, (theme) => {
if (!defaultStore.state.darkMode) {
applyTheme(theme);
}
});
//#region Sync dark mode
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', mql.matches);
}
});
//#endregion
fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
document.documentElement.style.removeProperty('--blur');
refreshAccount();
} else {
document.documentElement.style.setProperty('--blur', 'none');
}
}, { immediate: true });
if (_DEV_) {
console.log('no account cache found.');
}
let reloadDialogShowing = false;
stream.on('_disconnected_', async () => {
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
if (reloadDialogShowing) return;
reloadDialogShowing = true;
const { canceled } = await confirm({
type: 'warning',
title: i18n.ts.disconnectedFromServer,
text: i18n.ts.reloadConfirm,
});
reloadDialogShowing = false;
if (!canceled) {
// 連携ログインの場合用にCookieを参照する
const i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1];
if (i != null && i !== 'null') {
if (_DEV_) {
console.log('signing...');
}
try {
document.body.innerHTML = '<div>Please wait...</div>';
await login(i);
} catch (err) {
// Render the error screen
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
document.body.innerHTML = '<div id="err">Oops!</div>';
}
} else {
if (_DEV_) {
console.log('not signed in');
}
}
}
//#endregion
const fetchInstanceMetaPromise = fetchInstance();
fetchInstanceMetaPromise.then(() => {
localStorage.setItem('v', instance.version);
// Init service worker
initializeSw();
});
const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
defineAsyncComponent(() => import('@/ui/universal.vue')),
);
if (_DEV_) {
app.config.performance = true;
}
app.config.globalProperties = {
$i,
$store: defaultStore,
$instance: instance,
$t: i18n.t,
$ts: i18n.ts,
};
widgets(app);
directives(app);
components(app);
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する
const rootEl = (() => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentEl) {
console.warn('multiple import detected');
return currentEl;
}
const rootEl = document.createElement('div');
rootEl.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(rootEl);
return rootEl;
})();
app.mount(rootEl);
// boot.jsのやつを解除
window.onerror = null;
window.onunhandledrejection = null;
reactionPicker.init();
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
}
// クライアントが更新されたか?
const lastVersion = localStorage.getItem('lastVersion');
if (lastVersion !== version) {
localStorage.setItem('lastVersion', version);
// テーマリビルドするため
localStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
// ログインしてる場合だけ
if ($i) {
popup(defineAsyncComponent(() => import('@/components/updated.vue')), {}, {}, 'closed');
}
}
} catch (err) {
}
}
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
}, { immediate: localStorage.theme == null });
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
watch(darkTheme, (theme) => {
if (defaultStore.state.darkMode) {
applyTheme(theme);
}
});
watch(lightTheme, (theme) => {
if (!defaultStore.state.darkMode) {
applyTheme(theme);
}
});
//#region Sync dark mode
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', mql.matches);
}
});
//#endregion
fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
document.documentElement.style.removeProperty('--blur');
} else {
document.documentElement.style.setProperty('--blur', 'none');
}
}, { immediate: true });
let reloadDialogShowing = false;
stream.on('_disconnected_', async () => {
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
if (reloadDialogShowing) return;
reloadDialogShowing = true;
const { canceled } = await confirm({
type: 'warning',
title: i18n.ts.disconnectedFromServer,
text: i18n.ts.reloadConfirm,
});
reloadDialogShowing = false;
if (!canceled) {
location.reload();
}
}
}
});
stream.on('emojiAdded', emojiData => {
// TODO
//store.commit('instance/set', );
});
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('./plugin').then(({ install }) => {
install(plugin);
});
}
const hotkeys = {
'd': (): void => {
defaultStore.set('darkMode', !defaultStore.state.darkMode);
},
's': search,
};
stream.on('emojiAdded', emojiData => {
// TODO
//store.commit('instance/set', );
});
if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
if ($i.isDeleted) {
alert({
type: 'warning',
text: i18n.ts.accountDeletionInProgress,
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('./plugin').then(({ install }) => {
install(plugin);
});
}
const lastUsed = localStorage.getItem('lastUsed');
if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10);
// 二時間以上前なら
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
toast(i18n.t('welcomeBackWithName', {
name: $i.name || $i.username,
}));
}
}
localStorage.setItem('lastUsed', Date.now().toString());
const hotkeys = {
'd': (): void => {
defaultStore.set('darkMode', !defaultStore.state.darkMode);
},
's': search,
};
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
if ($i.isDeleted) {
alert({
type: 'warning',
text: i18n.ts.accountDeletionInProgress,
});
}
const lastUsed = localStorage.getItem('lastUsed');
if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10);
// 二時間以上前なら
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
toast(i18n.t('welcomeBackWithName', {
name: $i.name || $i.username,
}));
}
}
localStorage.setItem('lastUsed', Date.now().toString());
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
const main = markRaw(stream.useChannel('main', null, 'System'));
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
updateAccount(i);
});
main.on('readAllNotifications', () => {
updateAccount({ hasUnreadNotification: false });
});
main.on('unreadNotification', () => {
updateAccount({ hasUnreadNotification: true });
});
main.on('unreadMention', () => {
updateAccount({ hasUnreadMentions: true });
});
main.on('readAllUnreadMentions', () => {
updateAccount({ hasUnreadMentions: false });
});
main.on('unreadSpecifiedNote', () => {
updateAccount({ hasUnreadSpecifiedNotes: true });
});
main.on('readAllUnreadSpecifiedNotes', () => {
updateAccount({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllMessagingMessages', () => {
updateAccount({ hasUnreadMessagingMessage: false });
});
main.on('unreadMessagingMessage', () => {
updateAccount({ hasUnreadMessagingMessage: true });
sound.play('chatBg');
});
main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false });
});
main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true });
sound.play('antenna');
});
main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false });
});
main.on('readAllChannels', () => {
updateAccount({ hasUnreadChannel: false });
});
main.on('unreadChannel', () => {
updateAccount({ hasUnreadChannel: true });
sound.play('channel');
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
signout();
});
}
const main = markRaw(stream.useChannel('main', null, 'System'));
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
updateAccount(i);
});
main.on('readAllNotifications', () => {
updateAccount({ hasUnreadNotification: false });
});
main.on('unreadNotification', () => {
updateAccount({ hasUnreadNotification: true });
});
main.on('unreadMention', () => {
updateAccount({ hasUnreadMentions: true });
});
main.on('readAllUnreadMentions', () => {
updateAccount({ hasUnreadMentions: false });
});
main.on('unreadSpecifiedNote', () => {
updateAccount({ hasUnreadSpecifiedNotes: true });
});
main.on('readAllUnreadSpecifiedNotes', () => {
updateAccount({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllMessagingMessages', () => {
updateAccount({ hasUnreadMessagingMessage: false });
});
main.on('unreadMessagingMessage', () => {
updateAccount({ hasUnreadMessagingMessage: true });
sound.play('chatBg');
});
main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false });
});
main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true });
sound.play('antenna');
});
main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false });
});
main.on('readAllChannels', () => {
updateAccount({ hasUnreadChannel: false });
});
main.on('unreadChannel', () => {
updateAccount({ hasUnreadChannel: true });
sound.play('channel');
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
signout();
});
}
// shortcut
document.addEventListener('keydown', makeHotkey(hotkeys));
// shortcut
document.addEventListener('keydown', makeHotkey(hotkeys));
})();

View File

@@ -6,7 +6,7 @@ import { i18n } from '@/i18n';
import { ui } from '@/config';
import { unisonReload } from '@/scripts/unison-reload';
export const menuDef = reactive({
export const navbarItemDef = reactive({
notifications: {
title: 'notifications',
icon: 'fas fa-bell',

View File

@@ -3,6 +3,7 @@
import { EventEmitter } from 'eventemitter3';
import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue';
import { pleaseLogin } from '@/scripts/please-login';
import { safeURIDecode } from '@/scripts/safe-uri-decode';
type RouteDef = {
path: string;
@@ -116,7 +117,7 @@ export class Router extends EventEmitter<{
}
if (p.wildcard) {
if (parts.length !== 0) {
props.set(p.name, parts.join('/'));
props.set(p.name, safeURIDecode(parts.join('/')));
parts = [];
}
break pathMatchLoop;
@@ -124,10 +125,12 @@ export class Router extends EventEmitter<{
if (p.startsWith) {
if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop;
props.set(p.name, parts[0].substring(p.startsWith.length));
props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length)));
parts.shift();
} else {
props.set(p.name, parts[0]);
if (parts[0]) {
props.set(p.name, safeURIDecode(parts[0]));
}
parts.shift();
}
}
@@ -137,7 +140,7 @@ export class Router extends EventEmitter<{
if (parts.length !== 0) continue forEachRouteLoop;
if (route.hash != null && hash != null) {
props.set(route.hash, hash);
props.set(route.hash, safeURIDecode(hash));
}
if (route.query != null && queryString != null) {
@@ -147,7 +150,7 @@ export class Router extends EventEmitter<{
for (const q in route.query) {
const as = route.query[q];
if (queryObject[q]) {
props.set(as, queryObject[q]);
props.set(as, safeURIDecode(queryObject[q]));
}
}
}

View File

@@ -1,7 +1,7 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div style="overflow: hidden; overflow: clip;">
<div style="overflow: clip;">
<MkSpacer :content-max="600" :margin-min="20">
<div class="_formRoot znqjceqz">
<div id="debug"></div>

View File

@@ -138,7 +138,7 @@ definePageMetadata(computed(() => ({
.fwhjspax {
text-align: center;
border-radius: 10px;
overflow: hidden; overflow: clip;
overflow: clip;
background-size: cover;
background-position: center center;

View File

@@ -9,7 +9,7 @@
</div>
</div>
<div class="tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button>
@@ -20,7 +20,7 @@
<template v-if="actions">
<template v-for="action in actions">
<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
<button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</template>
</div>

View File

@@ -58,7 +58,7 @@ let view = $ref(null);
let el = $ref(null);
let pageProps = $ref({});
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha;
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha;
let noEmailServer = !instance.enableEmail;
let thereIsUnresolvedAbuseReport = $ref(false);

View File

@@ -355,7 +355,7 @@ async function renderChart() {
plugins: [{
id: 'vLine',
beforeDraw(chart, args, options) {
if (chart.tooltip._active && chart.tooltip._active.length) {
if (chart.tooltip?._active?.length) {
const activePoint = chart.tooltip._active[0];
const ctx = chart.ctx;
const x = activePoint.element.x;
@@ -561,7 +561,7 @@ definePageMetadata({
> .body {
background: var(--panel);
border-radius: var(--radius);
overflow: hidden; overflow: clip;
overflow: clip;
}
}
@@ -620,7 +620,7 @@ definePageMetadata({
> .body {
background: var(--panel);
border-radius: var(--radius);
overflow: hidden; overflow: clip;
overflow: clip;
}
}
}

View File

@@ -57,6 +57,19 @@
</div>
</FormFolder>
<FormFolder class="_formBlock">
<template #label>Active Email Validation</template>
<template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<div class="_formRoot">
<span class="_formBlock">{{ i18n.ts.activeEmailValidationDescription }}</span>
<FormSwitch v-model="enableActiveEmailValidation" class="_formBlock" @update:modelValue="save">
<template #label>Enable</template>
</FormSwitch>
</div>
</FormFolder>
<FormFolder class="_formBlock">
<template #label>Log IP address</template>
<template v-if="enableIpLogging" #suffix>Enabled</template>
@@ -112,6 +125,7 @@ let sensitiveMediaDetectionSensitivity: number = $ref(0);
let setSensitiveFlagAutomatically: boolean = $ref(false);
let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
let enableIpLogging: boolean = $ref(false);
let enableActiveEmailValidation: boolean = $ref(false);
async function init() {
const meta = await os.api('admin/meta');
@@ -128,6 +142,7 @@ async function init() {
setSensitiveFlagAutomatically = meta.setSensitiveFlagAutomatically;
enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
enableIpLogging = meta.enableIpLogging;
enableActiveEmailValidation = meta.enableActiveEmailValidation;
}
function save() {
@@ -144,6 +159,7 @@ function save() {
setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos,
enableIpLogging,
enableActiveEmailValidation,
}).then(() => {
fetchInstance();
});

View File

@@ -110,7 +110,7 @@ definePageMetadata(computed(() => antenna ? {
> .tl {
background: var(--bg);
border-radius: var(--radius);
overflow: hidden; overflow: clip;
overflow: clip;
}
&.min-width_800px {

View File

@@ -1,5 +1,9 @@
<template>
<MkSpacer :content-max="1200">
<MkTab v-model="origin" style="margin-bottom: var(--margin);">
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkTab>
<div v-if="origin === 'local'">
<template v-if="tag == null">
<MkFolder class="_gap" persist-key="explore-pinned-users">
@@ -57,16 +61,17 @@
import { computed, watch } from 'vue';
import XUserList from '@/components/user-list.vue';
import MkFolder from '@/components/ui/folder.vue';
import MkTab from '@/components/tab.vue';
import number from '@/filters/number';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
const props = defineProps<{
origin: 'local' | 'remote';
tag?: string;
}>();
let origin = $ref('local');
let tagsEl = $ref<InstanceType<typeof MkFolder>>();
let tagsLocal = $ref([]);
let tagsRemote = $ref([]);

View File

@@ -5,11 +5,8 @@
<div v-if="tab === 'featured'">
<XFeatured/>
</div>
<div v-else-if="tab === 'localUsers'">
<XUsers origin="local"/>
</div>
<div v-else-if="tab === 'remoteUsers'">
<XUsers origin="remote"/>
<div v-else-if="tab === 'users'">
<XUsers/>
</div>
<div v-else-if="tab === 'search'">
<MkSpacer :content-max="1200">
@@ -75,13 +72,9 @@ const headerTabs = $computed(() => [{
icon: 'fas fa-bolt',
title: i18n.ts.featured,
}, {
key: 'localUsers',
key: 'users',
icon: 'fas fa-users',
title: i18n.ts.users,
}, {
key: 'remoteUsers',
icon: 'fas fa-users',
title: i18n.ts.remote,
}, {
key: 'search',
title: i18n.ts.search,

View File

@@ -0,0 +1,96 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="600">
<FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.domain }}</template>
<template #value>{{ $ts.system }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.scope }}</template>
<template #value>{{ scope.join('/') }}</template>
</MkKeyValue>
</FormSplit>
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
<FormSection v-if="keys">
<template #label>{{ i18n.ts.keys }}</template>
<div class="_formLinks">
<FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
</div>
</FormSection>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue';
import MkKeyValue from '@/components/key-value.vue';
import FormSplit from '@/components/form/split.vue';
const props = defineProps<{
path: string;
}>();
const scope = $computed(() => props.path.split('/'));
let keys = $ref(null);
function fetchKeys() {
os.api('i/registry/keys-with-type', {
scope: scope,
}).then(res => {
keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
});
}
async function createKey() {
const { canceled, result } = await os.form(i18n.ts._registry.createKey, {
key: {
type: 'string',
label: i18n.ts._registry.key,
},
value: {
type: 'string',
multiline: true,
label: i18n.ts.value,
},
scope: {
type: 'string',
label: i18n.ts._registry.scope,
default: scope.join('/'),
},
});
if (canceled) return;
os.apiWithDialog('i/registry/set', {
scope: result.scope.split('/'),
key: result.key,
value: JSON5.parse(result.value),
}).then(() => {
fetchKeys();
});
}
watch(() => props.path, fetchKeys, { immediate: true });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.registry,
icon: 'fas fa-cogs',
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,123 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="600">
<FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo>
<template v-if="value">
<FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.domain }}</template>
<template #value>{{ $ts.system }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.scope }}</template>
<template #value>{{ scope.join('/') }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts._registry.key }}</template>
<template #value>{{ key }}</template>
</MkKeyValue>
</FormSplit>
<FormTextarea v-model="valueForEditor" tall class="_formBlock _monospace">
<template #label>{{ $ts.value }} (JSON)</template>
</FormTextarea>
<MkButton class="_formBlock" primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.updatedAt }}</template>
<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
</MkKeyValue>
<MkButton danger @click="del"><i class="fas fa-trash"></i> {{ $ts.delete }}</MkButton>
</template>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue';
import MkKeyValue from '@/components/key-value.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSplit from '@/components/form/split.vue';
import FormInfo from '@/components/ui/info.vue';
const props = defineProps<{
path: string;
}>();
const scope = $computed(() => props.path.split('/').slice(0, -1));
const key = $computed(() => props.path.split('/').at(-1));
let value = $ref(null);
let valueForEditor = $ref(null);
function fetchValue() {
os.api('i/registry/get-detail', {
scope,
key,
}).then(res => {
value = res;
valueForEditor = JSON5.stringify(res.value, null, '\t');
});
}
async function save() {
try {
JSON5.parse(valueForEditor);
} catch (e) {
os.alert({
type: 'error',
text: i18n.ts.invalidValue,
});
return;
}
os.confirm({
type: 'warning',
text: i18n.ts.saveConfirm,
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('i/registry/set', {
scope,
key,
value: JSON5.parse(valueForEditor),
});
});
}
function del() {
os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('i/registry/remove', {
scope,
key,
});
});
}
watch(() => props.path, fetchValue, { immediate: true });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.registry,
icon: 'fas fa-cogs',
});
</script>
<style lang="scss" scoped>
</style>

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