Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
000f876084 | ||
![]() |
2d11c558fa | ||
![]() |
ac6b02af40 | ||
![]() |
7d91912cfd | ||
![]() |
3c504b4b08 | ||
![]() |
adad4bcfe3 | ||
![]() |
b3e8671dd9 | ||
![]() |
0f8c890761 | ||
![]() |
512e451f24 | ||
![]() |
ca0d53ec5d | ||
![]() |
686a709e87 | ||
![]() |
83fb629f0b | ||
![]() |
35eeeb25e3 | ||
![]() |
19035c676c | ||
![]() |
61ffe7417c | ||
![]() |
7651353f39 | ||
![]() |
3f5b81060f | ||
![]() |
63dc66769f | ||
![]() |
e0fc8cbf8f | ||
![]() |
f9d1bc340e | ||
![]() |
0b269e79fd | ||
![]() |
6159cfd138 | ||
![]() |
6a5bbd335b | ||
![]() |
39e269db8c | ||
![]() |
70fe23a3ce | ||
![]() |
a6a8a7fb85 | ||
![]() |
6641b13b4c | ||
![]() |
5136b05c9b | ||
![]() |
803c2144f4 | ||
![]() |
b69a079514 | ||
![]() |
2aa800cd55 | ||
![]() |
6e61a36d05 | ||
![]() |
f80bf1fb1c | ||
![]() |
d465e85239 | ||
![]() |
deed25a2ff | ||
![]() |
a486716520 | ||
![]() |
2361e11e98 | ||
![]() |
cd1f2adca7 | ||
![]() |
a558767b7a | ||
![]() |
399ce9b999 | ||
![]() |
a94a0b5b0b | ||
![]() |
76faec2115 | ||
![]() |
33c4e57994 | ||
![]() |
bc23496998 | ||
![]() |
d35ad95c18 | ||
![]() |
5facd11592 | ||
![]() |
e1e885d6b2 | ||
![]() |
5b6695114f | ||
![]() |
71dd7f89e9 | ||
![]() |
21331e53fe | ||
![]() |
7afee5977f | ||
![]() |
d195b0dec7 | ||
![]() |
8a95e850ad | ||
![]() |
a4d74d7d7e | ||
![]() |
256e0db36d | ||
![]() |
d593c1358a | ||
![]() |
1ff14d81c1 | ||
![]() |
4369d12eec | ||
![]() |
91cc033eb5 | ||
![]() |
57543e6b44 | ||
![]() |
a1b8cd15c4 | ||
![]() |
73f06e591a | ||
![]() |
6f7cfa82b5 | ||
![]() |
ff97a003d1 | ||
![]() |
53c92e3e23 | ||
![]() |
13d13bc2f6 | ||
![]() |
03744a25ed | ||
![]() |
eac3bf8bff | ||
![]() |
2e1fbb5b16 | ||
![]() |
98b3517d36 | ||
![]() |
dee662705e | ||
![]() |
0da0cc80b9 | ||
![]() |
650187deaf | ||
![]() |
2e565cac2c | ||
![]() |
ac7537278c | ||
![]() |
f9a2e98831 | ||
![]() |
54f789bd55 | ||
![]() |
5ac9d13516 | ||
![]() |
2be1a39d13 | ||
![]() |
f3c5edc852 | ||
![]() |
30704e6de8 | ||
![]() |
41932ac409 | ||
![]() |
9843c596d8 |
50
CHANGELOG.md
50
CHANGELOG.md
@@ -8,6 +8,56 @@
|
||||
|
||||
You should also include the user name that made the change.
|
||||
-->
|
||||
## 13.5.6 (2023/02/10)
|
||||
|
||||
### Improvements
|
||||
- 非ログイン時にMiAuthを踏んだ際にMiAuthであることを表示する
|
||||
- /auth/のUIをアップデート
|
||||
- 利用規約同意UIの調整
|
||||
- クロップ時の質問を分かりやすく
|
||||
|
||||
### Bugfixes
|
||||
- fix: prevent clipping audio plyr's tooltip
|
||||
|
||||
## 13.5.4 (2023/02/09)
|
||||
|
||||
### Improvements
|
||||
- Server: UIのHTML(ノートなどの特別なページを除く)のキャッシュ時間を15秒から30秒に
|
||||
- i/notificationsのレートリミットを緩和
|
||||
|
||||
### Bugfixes
|
||||
- fix(client): validate url to improve security
|
||||
- fix(client): dateの初期値が正常に入らない時がある
|
||||
|
||||
## 13.5.3 (2023/02/09)
|
||||
|
||||
### Improvements
|
||||
- Client: デッキにチャンネルカラムを追加
|
||||
|
||||
## 13.5.2 (2023/02/08)
|
||||
|
||||
### Changes
|
||||
- Revert: perf(client): do not render custom emojis in user names
|
||||
|
||||
### Bugfixes
|
||||
- Client: register_note_view_interruptor not working
|
||||
- Client: ログイントークンの再生成が出来ない
|
||||
|
||||
## 13.5.0 (2023/02/08)
|
||||
|
||||
### Changes
|
||||
- perf(client): do not render custom emojis in user names
|
||||
|
||||
### Improvements
|
||||
- Client: disableShowingAnimatedImagesのデフォルト値をprefers-reduced-motionにする
|
||||
- enhance(client): tweak medialist style
|
||||
|
||||
### Bugfixes
|
||||
- fix docker health check
|
||||
- Client: MkEmojiPickerでもChromeで検索ダイアログで変換確定するとそのまま検索されてしまうのを修正
|
||||
- fix(mfm): default degree not used in rotate
|
||||
- fix(server): validate urls from ap to improve security
|
||||
|
||||
## 13.4.0 (2023/02/05)
|
||||
|
||||
### Improvements
|
||||
|
@@ -8,9 +8,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
|
||||
&& apt-get update \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
build-essential wget ca-certificates \
|
||||
&& wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq \
|
||||
&& chmod +x /usr/bin/yq
|
||||
build-essential
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
@@ -44,7 +42,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ffmpeg tini \
|
||||
ffmpeg tini curl \
|
||||
&& corepack enable \
|
||||
&& groupadd -g "${GID}" misskey \
|
||||
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
|
||||
@@ -54,7 +52,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
USER misskey
|
||||
WORKDIR /misskey
|
||||
|
||||
COPY --from=builder /usr/bin/yq /usr/bin/yq
|
||||
COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules
|
||||
COPY --chown=misskey:misskey --from=builder /misskey/built ./built
|
||||
COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
|
||||
|
@@ -6,16 +6,13 @@ Also, the later tasks are more indefinite and are subject to change as developme
|
||||
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
|
||||
|
||||
- Make the number of type errors zero (backend)
|
||||
- Probably need to switch some libraries to others that make it difficult to reduce type errors
|
||||
- e.g. koa to fastify https://github.com/misskey-dev/misskey/issues/7537
|
||||
- Improve CI
|
||||
- Fix tests
|
||||
- mocha, jest, etc. do not support the combination of `TypeScript + ESM + Path alias`, and the tests currently do not work.
|
||||
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
|
||||
- Add more tests
|
||||
- May need to implement a mechanism that allows for DI
|
||||
- ~~May need to implement a mechanism that allows for DI~~ → Done ✔️
|
||||
- https://github.com/misskey-dev/misskey/pull/9085
|
||||
- Measure coverage
|
||||
- ~~Measure coverage~~ → Done ✔️
|
||||
- https://github.com/misskey-dev/misskey/pull/9081
|
||||
- Improve documentation
|
||||
- Refactoring
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
PORT=$(yq '.port' /misskey/.config/default.yml)
|
||||
PORT=$(grep '^port:' /misskey/.config/default.yml | awk 'NR==1{print $2; exit}')
|
||||
curl -s -S -o /dev/null "http://localhost:${PORT}"
|
||||
|
@@ -1345,5 +1345,6 @@ _deck:
|
||||
tl: "الخيط الزمني"
|
||||
antenna: "الهوائيات"
|
||||
list: "القوائم"
|
||||
channel: "القنوات"
|
||||
mentions: "الإشارات"
|
||||
direct: "مباشرة"
|
||||
|
@@ -1441,5 +1441,6 @@ _deck:
|
||||
tl: "টাইমলাইন"
|
||||
antenna: "অ্যান্টেনা"
|
||||
list: "লিস্ট"
|
||||
channel: "চ্যানেলগুলি"
|
||||
mentions: "উল্লেখসমূহ"
|
||||
direct: "ডাইরেক্ট নোটগুলি"
|
||||
|
@@ -804,4 +804,5 @@ _deck:
|
||||
tl: "Časová osa"
|
||||
antenna: "Antény"
|
||||
list: "Seznamy"
|
||||
channel: "Kanály"
|
||||
mentions: "Zmínění"
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?"
|
||||
suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?"
|
||||
unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?"
|
||||
selectList: "Liste auswählen"
|
||||
selectChannel: "Kanal auswählen"
|
||||
selectAntenna: "Antenne auswählen"
|
||||
selectWidget: "Widget auswählen"
|
||||
editWidgets: "Widgets bearbeiten"
|
||||
@@ -256,6 +257,8 @@ noMoreHistory: "Kein weiterer Verlauf vorhanden"
|
||||
startMessaging: "Neuen Chat erstellen"
|
||||
nUsersRead: "Von {n} Benutzern gelesen"
|
||||
agreeTo: "Ich stimme {0} zu"
|
||||
agreeBelow: "Ich stimme Untenstehendem zu"
|
||||
basicNotesBeforeCreateAccount: "Wichtige Infos"
|
||||
tos: "Nutzungsbedingungen"
|
||||
start: "Anfangen"
|
||||
home: "Startseite"
|
||||
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgef
|
||||
rateLimitExceeded: "Versuchsanzahl überschritten"
|
||||
cropImage: "Bild zuschneiden"
|
||||
cropImageAsk: "Möchtest du das Bild zuschneiden?"
|
||||
cropYes: "Zuschneiden"
|
||||
cropNo: "Unbearbeitet verwenden"
|
||||
file: "Datei"
|
||||
recentNHours: "Letzten {n} Stunden"
|
||||
recentNDays: "Letzten {n} Tage"
|
||||
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes
|
||||
preset: "Vorlage"
|
||||
selectFromPresets: "Aus Vorlagen wählen"
|
||||
achievements: "Errungenschaften"
|
||||
gotInvalidResponseError: "Ungültige Antwort des Servers"
|
||||
gotInvalidResponseErrorDescription: "Eventuell ist der Server momentan nicht erreichbar oder untergeht Wartungsarbeiten. Bitte versuche es später noch einmal."
|
||||
_achievements:
|
||||
earnedAt: "Freigeschaltet am"
|
||||
_types:
|
||||
@@ -1593,12 +1600,15 @@ _permissions:
|
||||
"read:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge lesen"
|
||||
"write:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge bearbeiten"
|
||||
_auth:
|
||||
shareAccessTitle: "Verteilung von App-Berechtigungen"
|
||||
shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?"
|
||||
shareAccessAsk: "Bist du dir sicher, dass du diese Anwendung authorisieren möchtest, auf dein Benutzerkonto zugreifen zu können?"
|
||||
permission: "{name} fordert folgende Berechtigungen"
|
||||
permissionAsk: "Diese Anwendung fordert folgende Berechtigungen"
|
||||
pleaseGoBack: "Bitte kehre zur Anwendung zurück"
|
||||
callback: "Es wird zur Anwendung zurückgekehrt"
|
||||
denied: "Zugriff verweigert"
|
||||
pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren."
|
||||
_antennaSources:
|
||||
all: "Alle Notizen"
|
||||
homeTimeline: "Notizen von Benutzern, denen gefolgt wird"
|
||||
@@ -1869,5 +1879,6 @@ _deck:
|
||||
tl: "Chronik"
|
||||
antenna: "Antennen"
|
||||
list: "Listen"
|
||||
channel: "Kanal"
|
||||
mentions: "Erwähnungen"
|
||||
direct: "Direktnachrichten"
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "Are you sure that you want to unblock this account?"
|
||||
suspendConfirm: "Are you sure that you want to suspend this account?"
|
||||
unsuspendConfirm: "Are you sure that you want to unsuspend this account?"
|
||||
selectList: "Select a list"
|
||||
selectChannel: "Select a channel"
|
||||
selectAntenna: "Select an antenna"
|
||||
selectWidget: "Select a widget"
|
||||
editWidgets: "Edit widgets"
|
||||
@@ -256,6 +257,8 @@ noMoreHistory: "There is no further history"
|
||||
startMessaging: "Start a new chat"
|
||||
nUsersRead: "read by {n}"
|
||||
agreeTo: "I agree to {0}"
|
||||
agreeBelow: "I agree to the below"
|
||||
basicNotesBeforeCreateAccount: "Important notes"
|
||||
tos: "Terms of Service"
|
||||
start: "Begin"
|
||||
home: "Home"
|
||||
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "Could not fetch account information"
|
||||
rateLimitExceeded: "Rate limit exceeded"
|
||||
cropImage: "Crop image"
|
||||
cropImageAsk: "Do you want to crop this image?"
|
||||
cropYes: "Crop"
|
||||
cropNo: "Use as-is"
|
||||
file: "File"
|
||||
recentNHours: "Last {n} hours"
|
||||
recentNDays: "Last {n} days"
|
||||
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "This action cannot be performed temporarily
|
||||
preset: "Preset"
|
||||
selectFromPresets: "Choose from presets"
|
||||
achievements: "Achievements"
|
||||
gotInvalidResponseError: "Invalid server response"
|
||||
gotInvalidResponseErrorDescription: "The server may be unreachable or undergoing maintenance. Please try again later."
|
||||
_achievements:
|
||||
earnedAt: "Unlocked at"
|
||||
_types:
|
||||
@@ -1593,12 +1600,15 @@ _permissions:
|
||||
"read:gallery-likes": "View your list of liked gallery posts"
|
||||
"write:gallery-likes": "Edit your list of liked gallery posts"
|
||||
_auth:
|
||||
shareAccessTitle: "Granting application permissions"
|
||||
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
|
||||
shareAccessAsk: "Are you sure you want to authorize this application to access your account?"
|
||||
permission: "{name} requests the following permissions"
|
||||
permissionAsk: "This application requests the following permissions"
|
||||
pleaseGoBack: "Please go back to the application"
|
||||
callback: "Returning to the application"
|
||||
denied: "Access denied"
|
||||
pleaseLogin: "Please log in to authorize applications."
|
||||
_antennaSources:
|
||||
all: "All notes"
|
||||
homeTimeline: "Notes from followed users"
|
||||
@@ -1869,5 +1879,6 @@ _deck:
|
||||
tl: "Timeline"
|
||||
antenna: "Antennas"
|
||||
list: "List"
|
||||
channel: "Channel"
|
||||
mentions: "Mentions"
|
||||
direct: "Direct notes"
|
||||
|
@@ -56,7 +56,7 @@ reply: "Responder"
|
||||
loadMore: "Ver más"
|
||||
showMore: "Ver más"
|
||||
showLess: "Cerrar"
|
||||
youGotNewFollower: "te ha seguido"
|
||||
youGotNewFollower: "ahora te sigue"
|
||||
receiveFollowRequest: "Recibiste una solicitud de seguimiento"
|
||||
followRequestAccepted: "La solicitud de seguimiento fue aceptada"
|
||||
mention: "Menciones"
|
||||
@@ -129,6 +129,7 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?"
|
||||
suspendConfirm: "¿Quiere suspender esta cuenta?"
|
||||
unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
|
||||
selectList: "Seleccione una lista"
|
||||
selectChannel: "Seleccionar canal"
|
||||
selectAntenna: "Seleccionar antena"
|
||||
selectWidget: "Seleccionar widget"
|
||||
editWidgets: "Editar widgets"
|
||||
@@ -256,6 +257,8 @@ noMoreHistory: "El historial se ha acabado"
|
||||
startMessaging: "Iniciar chat"
|
||||
nUsersRead: "Leído por {n} personas"
|
||||
agreeTo: "De acuerdo con {0}"
|
||||
agreeBelow: "Estoy de acuerdo con lo siguiente"
|
||||
basicNotesBeforeCreateAccount: "Notas básicas"
|
||||
tos: "Términos de uso"
|
||||
start: "Comenzar"
|
||||
home: "Inicio"
|
||||
@@ -939,6 +942,8 @@ cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se
|
||||
preset: "Predefinido"
|
||||
selectFromPresets: "Escoger desde predefinidos"
|
||||
achievements: "Logros"
|
||||
gotInvalidResponseError: "Respuesta del servidor inválida"
|
||||
gotInvalidResponseErrorDescription: "Puede que el servidor esté caído o en mantenimiento. Favor de intentar más tarde"
|
||||
_achievements:
|
||||
earnedAt: "Desbloqueado el"
|
||||
_types:
|
||||
@@ -1593,12 +1598,15 @@ _permissions:
|
||||
"read:gallery-likes": "Ver favoritos de la galería"
|
||||
"write:gallery-likes": "Editar favoritos de la galería"
|
||||
_auth:
|
||||
shareAccessTitle: "Permisos de la aplicación"
|
||||
shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?"
|
||||
shareAccessAsk: "¿Está seguro de que desea autorizar esta aplicación para acceder a su cuenta?"
|
||||
permission: "{name} solicita los siguientes permisos"
|
||||
permissionAsk: "Esta aplicación requiere los siguientes permisos"
|
||||
pleaseGoBack: "Por favor, vuelve a la aplicación"
|
||||
callback: "Volviendo a la aplicación"
|
||||
denied: "Acceso denegado"
|
||||
pleaseLogin: "Se requiere un inicio de sesión para darle permisos a la aplicación"
|
||||
_antennaSources:
|
||||
all: "Todas las notas"
|
||||
homeTimeline: "Notas de los usuarios que sigues"
|
||||
@@ -1869,5 +1877,6 @@ _deck:
|
||||
tl: "Linea de tiempo"
|
||||
antenna: "Antenas"
|
||||
list: "Listas"
|
||||
channel: "Canal"
|
||||
mentions: "Menciones"
|
||||
direct: "Mensaje directo"
|
||||
|
@@ -1541,5 +1541,6 @@ _deck:
|
||||
tl: "Fil"
|
||||
antenna: "Antennes"
|
||||
list: "Listes"
|
||||
channel: "Canaux"
|
||||
mentions: "Mentions"
|
||||
direct: "Direct"
|
||||
|
@@ -1673,5 +1673,6 @@ _deck:
|
||||
tl: "Linimasa"
|
||||
antenna: "Antena"
|
||||
list: "Daftar"
|
||||
channel: "Kanal"
|
||||
mentions: "Sebutan"
|
||||
direct: "Langsung"
|
||||
|
@@ -1044,7 +1044,7 @@ _achievements:
|
||||
flavor: "Grazie per aver usato Misskey!"
|
||||
_noteClipped1:
|
||||
title: "Devo clippare!"
|
||||
description: "Ho raccolto in Clip la prima Nota"
|
||||
description: "Hai raccolto la tua prima Nota in una Clip"
|
||||
_noteFavorited1:
|
||||
title: "Guarda le stelle"
|
||||
description: "Aggiungi una Nota ai preferiti per la prima volta"
|
||||
@@ -1080,7 +1080,7 @@ _achievements:
|
||||
title: "Follow me!"
|
||||
description: "Hai ottenuto 10 profili Follower"
|
||||
_followers50:
|
||||
title: "Follower a frotte"
|
||||
title: "Un gregge di Follower"
|
||||
description: "Hai ottenuto 50 Follower"
|
||||
_followers100:
|
||||
title: "Popolare"
|
||||
@@ -1108,7 +1108,7 @@ _achievements:
|
||||
title: "Caccia al tesoro"
|
||||
description: "Hai trovato un tesoro nascosto"
|
||||
_client30min:
|
||||
title: "Piccola pausa"
|
||||
title: "Piccola grande pausa"
|
||||
description: "Hai passato più di 30 minuti su Misskey"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Ooops!"
|
||||
@@ -1134,7 +1134,7 @@ _achievements:
|
||||
title: "Hello, world!"
|
||||
description: "Hai scritto «Hello world» nel blocco appunti"
|
||||
_open3windows:
|
||||
title: "Finestrato"
|
||||
title: "Apri le finestre!"
|
||||
description: "Hai aperto almeno 3 finestre contemporaneamente"
|
||||
_driveFolderCircularReference:
|
||||
title: "Riferimento circolare"
|
||||
@@ -1170,7 +1170,7 @@ _achievements:
|
||||
_cookieClicked:
|
||||
title: "Clicca il biscotto"
|
||||
description: "Hai giocato a cliccare il cookie"
|
||||
flavor: "Hai autorizzato i cookie?"
|
||||
flavor: "È il sito giusto?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Pubblica un link a Brain Diver"
|
||||
@@ -1195,6 +1195,9 @@ _role:
|
||||
baseRole: "Ruolo di base"
|
||||
useBaseValue: "Eredita dal ruolo base"
|
||||
chooseRoleToAssign: "Seleziona il ruolo da assegnare"
|
||||
iconUrl: "URL dell'icona"
|
||||
asBadge: "Mostra come badge"
|
||||
descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo."
|
||||
canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo"
|
||||
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
|
||||
priority: "Priorità"
|
||||
@@ -1866,5 +1869,6 @@ _deck:
|
||||
tl: "Timeline"
|
||||
antenna: "Antenne"
|
||||
list: "Liste"
|
||||
channel: "Canale"
|
||||
mentions: "Menzioni"
|
||||
direct: "Diretta"
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "ブロック解除しますか?"
|
||||
suspendConfirm: "凍結しますか?"
|
||||
unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
selectChannel: "チャンネルを選択"
|
||||
selectAntenna: "アンテナを選択"
|
||||
selectWidget: "ウィジェットを選択"
|
||||
editWidgets: "ウィジェットを編集"
|
||||
@@ -256,6 +257,8 @@ noMoreHistory: "これより過去の履歴はありません"
|
||||
startMessaging: "チャットを開始"
|
||||
nUsersRead: "{n}人が読みました"
|
||||
agreeTo: "{0}に同意"
|
||||
agreeBelow: "下記に同意する"
|
||||
basicNotesBeforeCreateAccount: "基本的な注意事項"
|
||||
tos: "利用規約"
|
||||
start: "始める"
|
||||
home: "ホーム"
|
||||
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "アカウント情報の取得に失敗しま
|
||||
rateLimitExceeded: "レート制限を超えました"
|
||||
cropImage: "画像のクロップ"
|
||||
cropImageAsk: "画像をクロップしますか?"
|
||||
cropYes: "クロップする"
|
||||
cropNo: "そのまま使う"
|
||||
file: "ファイル"
|
||||
recentNHours: "直近{n}時間"
|
||||
recentNDays: "直近{n}日"
|
||||
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "操作回数が制限を超過するため
|
||||
preset: "プリセット"
|
||||
selectFromPresets: "プリセットから選択"
|
||||
achievements: "実績"
|
||||
gotInvalidResponseError: "サーバーの応答が無効です"
|
||||
gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
|
||||
|
||||
_achievements:
|
||||
earnedAt: "獲得日時"
|
||||
@@ -1625,12 +1632,15 @@ _permissions:
|
||||
"write:gallery-likes": "ギャラリーのいいねを操作する"
|
||||
|
||||
_auth:
|
||||
shareAccessTitle: "アプリへのアクセス許可"
|
||||
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
|
||||
shareAccessAsk: "アカウントへのアクセスを許可しますか?"
|
||||
permission: "{name}は次の権限を要求しています"
|
||||
permissionAsk: "このアプリは次の権限を要求しています"
|
||||
pleaseGoBack: "アプリケーションに戻ってやっていってください"
|
||||
callback: "アプリケーションに戻っています"
|
||||
denied: "アクセスを拒否しました"
|
||||
pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。"
|
||||
|
||||
_antennaSources:
|
||||
all: "全てのノート"
|
||||
@@ -1922,5 +1932,6 @@ _deck:
|
||||
tl: "タイムライン"
|
||||
antenna: "アンテナ"
|
||||
list: "リスト"
|
||||
channel: "チャンネル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト"
|
||||
|
@@ -46,7 +46,7 @@ copyContent: "内容をコピー"
|
||||
copyLink: "リンクをコピー"
|
||||
delete: "ほかす"
|
||||
deleteAndEdit: "ほかして直す"
|
||||
deleteAndEditConfirm: "このノートをほかして書き直すんか?このノートへのリアクション、Renote、返信も全部消えてまうで。"
|
||||
deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのリアクション、Renote、返信も全部消えるんやけどそれでもええん?"
|
||||
addToList: "リストに入れたる"
|
||||
sendMessage: "メッセージを送る"
|
||||
copyRSS: "RSSをコピー"
|
||||
@@ -89,7 +89,7 @@ serverIsDead: "サーバーからの応答がないで。もうちょい待っ
|
||||
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
|
||||
enterListName: "リスト名を入れてや"
|
||||
privacy: "プライバシー"
|
||||
makeFollowManuallyApprove: "自分が認めた人だけがこのアカウントをフォローできるようにする"
|
||||
makeFollowManuallyApprove: "他人のフォローは許可してからや!"
|
||||
defaultNoteVisibility: "もとからの公開範囲"
|
||||
follow: "フォロー"
|
||||
followRequest: "フォローを頼む"
|
||||
@@ -129,6 +129,7 @@ unblockConfirm: "ブロックやめたるってほんまか?"
|
||||
suspendConfirm: "凍結してしもうてええか?"
|
||||
unsuspendConfirm: "解凍するけどええか?"
|
||||
selectList: "リストを選ぶ"
|
||||
selectChannel: "チャンネルを選ぶ"
|
||||
selectAntenna: "アンテナを選ぶ"
|
||||
selectWidget: "ウィジェットを選ぶ"
|
||||
editWidgets: "ウィジェットをいじる"
|
||||
@@ -256,6 +257,8 @@ noMoreHistory: "これより過去の履歴はあらへんで"
|
||||
startMessaging: "チャットやるで"
|
||||
nUsersRead: "{n}人が読んでもうた"
|
||||
agreeTo: "{0}に同意したで"
|
||||
agreeBelow: "下記に同意したる"
|
||||
basicNotesBeforeCreateAccount: "よう読んでやってや"
|
||||
tos: "利用規約"
|
||||
start: "始める"
|
||||
home: "ホーム"
|
||||
@@ -300,7 +303,7 @@ avatar: "アイコン"
|
||||
banner: "バナー"
|
||||
nsfw: "閲覧注意"
|
||||
whenServerDisconnected: "サーバーとの接続が切れたとき"
|
||||
disconnectedFromServer: "サーバーとの通信が切れたで"
|
||||
disconnectedFromServer: "サーバーが機嫌悪いねん"
|
||||
reload: "リロード"
|
||||
doNothing: "何もせんとく"
|
||||
reloadConfirm: "リロードしてええか?"
|
||||
@@ -673,8 +676,8 @@ sentReactionsCount: "リアクションした数やで"
|
||||
receivedReactionsCount: "リアクションされた数"
|
||||
pollVotesCount: "アンケートに投票した数"
|
||||
pollVotedCount: "アンケートに投票された数"
|
||||
yes: "はい"
|
||||
no: "いいえ"
|
||||
yes: "ええで"
|
||||
no: "あかんで"
|
||||
driveFilesCount: "ドライブのファイル数"
|
||||
driveUsage: "ドライブ使用量やで"
|
||||
noCrawle: "クローラーによるインデックスを拒否するで"
|
||||
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "アカウントの取得に失敗したみた
|
||||
rateLimitExceeded: "レート制限が超えたみたいやで"
|
||||
cropImage: "画像のクロップ"
|
||||
cropImageAsk: "画像をクロップしたってええか?"
|
||||
cropYes: "切り抜いたる"
|
||||
cropNo: "切り抜かへん"
|
||||
file: "ファイル"
|
||||
recentNHours: "直近{n}時間"
|
||||
recentNDays: "直近{n}日"
|
||||
@@ -938,6 +943,37 @@ cannotPerformTemporary: "一時的に利用できへんで"
|
||||
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
|
||||
preset: "プリセット"
|
||||
selectFromPresets: "プリセットから選ぶ"
|
||||
achievements: "実績"
|
||||
gotInvalidResponseError: "サーバー黙っとるわ、知らんけど"
|
||||
gotInvalidResponseErrorDescription: "サーバーいま日曜日。またきて月曜日。"
|
||||
_achievements:
|
||||
earnedAt: "貰った日ぃ"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "まいど!"
|
||||
description: "初めてノート投稿したった"
|
||||
_notes10:
|
||||
title: "ノートの天保山"
|
||||
_notes100:
|
||||
title: "ノートの真田山"
|
||||
_notes500:
|
||||
title: "ノートの生駒山"
|
||||
_notes5000:
|
||||
title: "箕面の滝からノート"
|
||||
_login3:
|
||||
flavor: "今日からワシはミスキストやで"
|
||||
_iLoveMisskey:
|
||||
title: "Misskey好きやねん"
|
||||
_foundTreasure:
|
||||
title: "なんでも鑑定団"
|
||||
_client30min:
|
||||
title: "ねんね"
|
||||
_noteDeletedWithin1min:
|
||||
title: "*おおっと*"
|
||||
_open3windows:
|
||||
title: "マド開けすぎ"
|
||||
_driveFolderCircularReference:
|
||||
title: "環状線"
|
||||
_role:
|
||||
new: "ロールの作成"
|
||||
edit: "ロールの編集"
|
||||
@@ -1355,10 +1391,12 @@ _permissions:
|
||||
_auth:
|
||||
shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?"
|
||||
shareAccessAsk: "アカウントのアクセスを許可してもええか?"
|
||||
permission: "{name}に次の権限つけたってやって"
|
||||
permissionAsk: "このアプリは次の権限を要求しとるで"
|
||||
pleaseGoBack: "アプリケーションに戻ってええよ"
|
||||
callback: "アプリケーションに戻っとるで"
|
||||
denied: "アクセスを拒否ったで"
|
||||
pleaseLogin: "アプリにアクセスさせるんやったら、ログインしてや。"
|
||||
_antennaSources:
|
||||
all: "みんなのノート"
|
||||
homeTimeline: "フォローしとるユーザーのノート"
|
||||
@@ -1587,6 +1625,7 @@ _notification:
|
||||
pollEnded: "アンケートの結果が出たみたいや"
|
||||
unreadAntennaNote: "アンテナ {name}"
|
||||
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
|
||||
achievementEarned: "実績を獲得しとるで"
|
||||
_types:
|
||||
all: "すべて"
|
||||
follow: "フォロー"
|
||||
@@ -1628,5 +1667,6 @@ _deck:
|
||||
tl: "タイムライン"
|
||||
antenna: "アンテナ"
|
||||
list: "リスト"
|
||||
channel: "チャンネル"
|
||||
mentions: "あんた宛て"
|
||||
direct: "ダイレクト"
|
||||
|
@@ -1866,5 +1866,6 @@ _deck:
|
||||
tl: "타임라인"
|
||||
antenna: "안테나"
|
||||
list: "리스트"
|
||||
channel: "채널"
|
||||
mentions: "받은 멘션"
|
||||
direct: "다이렉트"
|
||||
|
@@ -1,2 +1,244 @@
|
||||
---
|
||||
_lang_: "ພາສາລາວ"
|
||||
headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍຫມາຍເຫດ"
|
||||
introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນແຫຼ່ງເປີດ, ການບໍລິການ microblogging ກະຈາຍ\nສ້າງ \"ບັນທຶກ\" ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nດ້ວຍ \"ປະຕິກິລິຍາ\", ທ່ານຍັງສາມາດສະແດງຄວາມຮູ້ສຶກຂອງທ່ານຢ່າງໄວວາກ່ຽວກັບບັນທຶກຂອງທຸກໆຄົນ 👍\nມາສຳຫຼວດໂລກໃໝ່! 🚀"
|
||||
poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. <b>Misskey</b> (ເອີ້ນວ່າ \"Misskey instance\")"
|
||||
monthAndDay: "{ເດືອນ}/{ມື້}"
|
||||
search: "ຄົ້ນຫາ"
|
||||
notifications: "ການແຈ້ງເຕືອນ"
|
||||
username: "ຊື່ຜູ້ໃຊ້"
|
||||
password: "ລະຫັດຜ່ານ"
|
||||
forgotPassword: "ລືມລະຫັດຜ່ານ"
|
||||
fetchingAsApObject: "ກຳລັງດຶງຂໍ້ມູນຈາກ fediverse..."
|
||||
ok: "ຕົກລົງ"
|
||||
gotIt: "ເຂົ້າໃຈແລ້ວ!"
|
||||
cancel: "ຍົກເລີກ"
|
||||
noThankYou: "ບໍ່ແມ່ນຕອນນີ້"
|
||||
enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້"
|
||||
renotedBy: "Renoted ໂດຍ {ຜູ້ໃຊ້}"
|
||||
noNotes: "ບໍ່ມີຫມາຍເຫດ"
|
||||
noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ"
|
||||
instance: "ອີນສະແຕນ"
|
||||
settings: "ກຳນົດຄ່າ"
|
||||
basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ"
|
||||
otherSettings: "ການຕັ້ງຄ່າອື່ນໆ"
|
||||
openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ"
|
||||
profile: "ໂພຼຟາຍ"
|
||||
timeline: "ເສັ້ນກຳນົດເວລາ"
|
||||
noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ"
|
||||
login: "ເຂົ້າສູ່ລະບົບ"
|
||||
loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..."
|
||||
logout: "ອອກຈາກລະບົບ"
|
||||
signup: "ລົງທະບຽນ"
|
||||
uploading: "ການອັບໂຫຼດ..."
|
||||
save: "ບັນທຶກ"
|
||||
users: "ຜູ້ໃຊ້ຕ່າງໆ"
|
||||
addUser: "ເພີ່ມຜູ້ໃຊ້"
|
||||
favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ"
|
||||
favorites: "ລາຍການທີ່ມັກ"
|
||||
unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ"
|
||||
favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ"
|
||||
alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ."
|
||||
cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້."
|
||||
pin: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌"
|
||||
unpin: "ຖອດປັກໝຸດອອກຈາກໂປຣໄຟລ໌"
|
||||
copyContent: "ຄັດລອກເນື້ອຫາ"
|
||||
copyLink: "ສຳເນົາລິ້ງ"
|
||||
delete: "ລຶບ"
|
||||
deleteAndEdit: "ລົບແລະແກ້ໄຂ"
|
||||
deleteAndEditConfirm: "ເຈົ້າແນ່ໃຈບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ"
|
||||
addToList: "ເພີ່ມໃສ່ລາຍຊື່"
|
||||
sendMessage: "ສົ່ງຂໍ້ຄວາມ"
|
||||
copyRSS: "ສຳເນົາ RSS"
|
||||
copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້"
|
||||
searchUser: "ຄົ້ນຫາຜູ້ໃຊ້"
|
||||
reply: "ຕອບໄປທີ"
|
||||
loadMore: "ໂຫຼດເພີ່ມເຕີມ"
|
||||
showMore: "ໂຫຼດເພີ່ມເຕີມ"
|
||||
showLess: "ປິດ"
|
||||
youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ"
|
||||
receiveFollowRequest: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ໄດ້ຮັບ"
|
||||
followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້ຍອມຮັບຄໍາຮ້ອງຂໍຂອງທ່ານ"
|
||||
mention: "ໄດ້ກ່າວມາ"
|
||||
mentions: "ກ່າວເຖິງ"
|
||||
directNotes: "ໂດຍກົງຫມາຍເຫດ"
|
||||
importAndExport: "ນໍາເຂົ້າ / ສົ່ງອອກ"
|
||||
import: "ນຳເຂົ້າ"
|
||||
export: "ນຳອອກ"
|
||||
files: "ໄຟລ໌"
|
||||
download: "ດາວໂຫລດ"
|
||||
driveFileDeleteConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບໄຟລ໌ \"{name}\"? ບັນທຶກທີ່ມີໄຟລ໌ແນບນີ້ຈະຖືກລຶບຖິ້ມ"
|
||||
unfollowConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເຊົາຕິດຕາມ {name}?"
|
||||
exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການສົ່ງອອກ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ ແລະມັນຈະຖືກເພີ່ມໃສ່ drive ຂອງທ່ານເມື່ອມັນສຳເລັດແລ້ວ"
|
||||
importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການນໍາເຂົ້າ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ"
|
||||
lists: "ລາຍການ"
|
||||
noLists: "ທ່ານບໍ່ມີລາຍການໃດໆ"
|
||||
note: "ບັນທຶກ"
|
||||
notes: "ບັນທຶກ"
|
||||
following: "ກຳລັງຕິດຕາມ"
|
||||
followers: "ຜູ້ຕິດຕາມ"
|
||||
followsYou: "ຕິດຕາມເຈົ້າ"
|
||||
createList: "ສ້າງລາຍຊື່"
|
||||
manageLists: "ການບໍລິຫານບັນຊີລາຍການ"
|
||||
error: "ຂໍ້ຜິດພາດ"
|
||||
somethingHappened: "ອຸຍ, ມີບາງຢ່າງຜິດພາດ"
|
||||
retry: "ລອງໃຫມ່"
|
||||
pageLoadError: "ເກີດຄວາມຜິດພາດໃນການໂຫລດໜ້ານີ້"
|
||||
pageLoadErrorDescription: "ປົກກະຕິແລ້ວມັນເກີດຈາກຄວາມຜິດພາດເຄືອຂ່າຍ ຫຼື cache ຂອງຕົວທ່ອງເວັບ ລອງລຶບລ້າງແຄດແລ້ວລອງໃໝ່ພາຍຫຼັງສອງສາມນາທີ"
|
||||
serverIsDead: "ເຊີບເວີນີ້ບໍ່ຕອບສະໜອງ ກະລຸນາລໍຖ້າຈັກໜ່ອຍແລ້ວລອງໃໝ່ອີກຄັ້ງ"
|
||||
youShouldUpgradeClient: "ເພື່ອເບິ່ງໜ້ານີ້, ກະລຸນາໂຫຼດຂໍ້ມູນຄືນໃໝ່ເພື່ອອັບເດດລູກຄ້າຂອງທ່ານ"
|
||||
enterListName: "ໃສ່ຊື່ສຳລັບລາຍຊື່"
|
||||
privacy: "ຄວາມເປັນສ່ວນຕົວ"
|
||||
makeFollowManuallyApprove: "ປະຕິບັດຕາມການຮ້ອງຂໍຮຽກຮ້ອງໃຫ້ມີການອະນຸມັດ"
|
||||
defaultNoteVisibility: "ເປັນຄ່າເລີ່ມຕົ້ນ"
|
||||
follow: "ກຳລັງຕິດຕາມ"
|
||||
followRequest: "ສົ່ງການຮ້ອງຂໍປະຕິບຕາມ"
|
||||
followRequests: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍ"
|
||||
unfollow: "ເຊົາຕິດຕາມ"
|
||||
followRequestPending: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ລໍຖ້າຢູ່"
|
||||
enterEmoji: "ປ້ອນອີໂມຈິ"
|
||||
renote: "Renote"
|
||||
unrenote: "ເລີກ Renote"
|
||||
renoted: "ເກັບບັນທຶກໄວ້"
|
||||
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
|
||||
pinnedNote: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
|
||||
pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌"
|
||||
you: "ເຈົ້າ"
|
||||
clickToShow: "ກົດເພື່ອສະແດງໃຫ້ເຫັນ"
|
||||
sensitive: "NSFW"
|
||||
add: "ເພີ່ມ"
|
||||
reaction: "ປະຕິກິລິຍາ"
|
||||
reactions: "ປະຕິກິລິຍາ"
|
||||
mute: "ປີດສຽງ"
|
||||
unmute: "ເປີດສຽງ"
|
||||
block: "ບ໋ອກ"
|
||||
unblock: "ຍົກເລີກກາຮົບລັອກ"
|
||||
suspend: "ລະງັບ"
|
||||
unsuspend: "ເຊົາລະງັບ"
|
||||
selectList: "ເລືອກບັນຊີລາຍການ"
|
||||
selectWidget: "ເລືອກວິກເຈັດ"
|
||||
editWidgets: "ແກ້ໄຂ Widget"
|
||||
editWidgetsExit: "ສຳເລັດແລ້ວ"
|
||||
customEmojis: "ອີໂມຈິແບບກຳນົດເອງ"
|
||||
emoji: "ອີໂມຈິ"
|
||||
emojis: "ອີໂມຈິ"
|
||||
emojiName: "ຊື່ Emoji"
|
||||
emojiUrl: "URL ອີໂມຈິ"
|
||||
addEmoji: "ຕື່ມອີໂມຈິ"
|
||||
flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ"
|
||||
flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ"
|
||||
flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ"
|
||||
flagShowTimelineReplies: "ສະແດງການຕອບກັບໃນທາມລາຍ"
|
||||
flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບຂອງຜູ້ໃຊ້ຕໍ່ກັບບັນທຶກຂອງຜູ້ໃຊ້ອື່ນໃນທາມລາຍຖ້າເປີດໃຊ້ງານ"
|
||||
autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່"
|
||||
addAccount: "ເພີ່ມບັນຊີ"
|
||||
loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ"
|
||||
general: "ທົ່ວໄປ"
|
||||
wallpaper: "ພາບພື້ນຫລັງ"
|
||||
setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ"
|
||||
instances: "ອີນສະແຕນ"
|
||||
instanceInfo: "ອີນສະແຕນ"
|
||||
statistics: "ສະຖິຕິ"
|
||||
clearQueue: "ລ້າງຄິວ"
|
||||
clearCachedFiles: "ລຶບລ້າງແຄສ"
|
||||
editProfile: "ແກ້ໄຂໂປຣໄຟລ໌"
|
||||
done: "ສຳເລັດ"
|
||||
processing: "ກຳລັງປະມວນຜົນ"
|
||||
preview: "ສະແດງເປັນຕົວຢ່າງ"
|
||||
default: "ຄ່າເລີ່ມຕົ້ນ"
|
||||
blocked: "ບລັອກແລ້ວ "
|
||||
all: "ທັງໝົດ"
|
||||
subscribing: "ສະໝັກສະມາຊິກແລັວ"
|
||||
publishing: "ການພິມເຜີຍແຜ່"
|
||||
notResponding: "ບໍ່ຕອບສະໜອງ"
|
||||
instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າງ"
|
||||
instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ"
|
||||
instanceUsers: "ຜູ້ຊົມໃຊ້ຂອງຕົວຢ່າງນີ້"
|
||||
changePassword: "ປ່ຽນລະຫັດຜ່ານ"
|
||||
featured: "ໄຮໄລທ໌"
|
||||
announcements: "ປະກາດ"
|
||||
remove: "ລຶບ"
|
||||
messaging: "ແຊ໋ດ"
|
||||
tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ"
|
||||
start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ"
|
||||
home: "ໜ້າຫຼັກ"
|
||||
images: "ຮູບພາບ"
|
||||
birthday: "ວັນເກີດ"
|
||||
registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
|
||||
location: "ທີ່ຕັ້ງ"
|
||||
theme: "ແທ໋ມ"
|
||||
light: "ສະຫວ່າງ"
|
||||
dark: "ມືດ"
|
||||
lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
|
||||
darkThemes: "ຮູບແບບສີສັນມືດ"
|
||||
fileName: "ຊື່ໄຟລ໌"
|
||||
selectFile: "ເລືອກໄຟລ໌"
|
||||
selectFiles: "ເລືອກໄຟລ໌"
|
||||
nsfw: "NSFW"
|
||||
accept: "ອະນຸຍາດ"
|
||||
pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
|
||||
userList: "ລາຍການ"
|
||||
smtpUser: "ຊື່ຜູ້ໃຊ້"
|
||||
smtpPass: "ລະຫັດຜ່ານ"
|
||||
clearCache: "ລຶບລ້າງແຄສ"
|
||||
user: "ຜູ້ໃຊ້ຕ່າງໆ"
|
||||
searchByGoogle: "ຄົ້ນຫາ"
|
||||
file: "ໄຟລ໌"
|
||||
_email:
|
||||
_follow:
|
||||
title: "ໄດ້ຕິດຕາມທ່ານ"
|
||||
_mfm:
|
||||
mention: "ໄດ້ກ່າວມາ"
|
||||
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
|
||||
emoji: "ອີໂມຈິແບບກຳນົດເອງ"
|
||||
search: "ຄົ້ນຫາ"
|
||||
_theme:
|
||||
keys:
|
||||
mention: "ໄດ້ກ່າວມາ"
|
||||
renote: "Renote"
|
||||
_sfx:
|
||||
note: "ບັນທຶກ"
|
||||
notification: "ການແຈ້ງເຕືອນ"
|
||||
chat: "ແຊ໋ດ"
|
||||
_widgets:
|
||||
profile: "ໂພຼຟາຍ"
|
||||
instanceInfo: "ອີນສະແຕນ"
|
||||
notifications: "ການແຈ້ງເຕືອນ"
|
||||
timeline: "ເສັ້ນກຳນົດເວລາ"
|
||||
_userList:
|
||||
chooseList: "ເລືອກບັນຊີລາຍການ"
|
||||
_cw:
|
||||
show: "ໂຫຼດເພີ່ມເຕີມ"
|
||||
_visibility:
|
||||
home: "ໜ້າຫຼັກ"
|
||||
followers: "ຜູ້ຕິດຕາມ"
|
||||
_profile:
|
||||
username: "ຊື່ຜູ້ໃຊ້"
|
||||
_exportOrImport:
|
||||
followingList: "ກຳລັງຕິດຕາມ"
|
||||
muteList: "ປີດສຽງ"
|
||||
blockingList: "ບ໋ອກ"
|
||||
userLists: "ລາຍການ"
|
||||
_timelines:
|
||||
home: "ໜ້າຫຼັກ"
|
||||
_pages:
|
||||
blocks:
|
||||
image: "ຮູບພາບ"
|
||||
_notification:
|
||||
youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ"
|
||||
_types:
|
||||
follow: "ກຳລັງຕິດຕາມ"
|
||||
mention: "ໄດ້ກ່າວມາ"
|
||||
renote: "Renote"
|
||||
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
|
||||
reaction: "ປະຕິກິລິຍາ"
|
||||
_actions:
|
||||
reply: "ຕອບໄປທີ"
|
||||
renote: "Renote"
|
||||
_deck:
|
||||
_columns:
|
||||
notifications: "ການແຈ້ງເຕືອນ"
|
||||
tl: "ເສັ້ນກຳນົດເວລາ"
|
||||
list: "ລາຍການ"
|
||||
channel: "ຊ່ອງ"
|
||||
mentions: "ກ່າວເຖິງ"
|
||||
|
@@ -1438,5 +1438,6 @@ _deck:
|
||||
tl: "Oś czasu"
|
||||
antenna: "Anteny"
|
||||
list: "Listy"
|
||||
channel: "Kanały"
|
||||
mentions: "Wspomnienia"
|
||||
direct: "Bezpośredni"
|
||||
|
@@ -721,4 +721,5 @@ _deck:
|
||||
tl: "Cronologie"
|
||||
antenna: "Antene"
|
||||
list: "Liste"
|
||||
channel: "Canale"
|
||||
mentions: "Mențiuni"
|
||||
|
@@ -1845,5 +1845,6 @@ _deck:
|
||||
tl: "Лента"
|
||||
antenna: "Антенны"
|
||||
list: "Списки"
|
||||
channel: "Каналы"
|
||||
mentions: "Упоминания"
|
||||
direct: "Личное"
|
||||
|
@@ -1545,5 +1545,6 @@ _deck:
|
||||
tl: "Časová os"
|
||||
antenna: "Antény"
|
||||
list: "Zoznam"
|
||||
channel: "Kanály"
|
||||
mentions: "Zmienky"
|
||||
direct: "Priame poznámky"
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต
|
||||
suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?"
|
||||
unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้"
|
||||
selectList: "เลือกรายการ"
|
||||
selectChannel: "เลือกแชนแนล"
|
||||
selectAntenna: "เลือกเสาอากาศ"
|
||||
selectWidget: "เลือกวิดเจ็ต"
|
||||
editWidgets: "แก้ไขวิดเจ็ต"
|
||||
@@ -939,6 +940,8 @@ cannotPerformTemporaryDescription: "การดําเนินการน
|
||||
preset: "พรีเซ็ต"
|
||||
selectFromPresets: "เลือกจากการพรีเซ็ต"
|
||||
achievements: "ความสำเร็จ"
|
||||
gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง"
|
||||
gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ"
|
||||
_achievements:
|
||||
earnedAt: "ได้รับเมื่อ"
|
||||
_types:
|
||||
@@ -1147,7 +1150,7 @@ _achievements:
|
||||
description: "คุณได้คลิกที่นี่"
|
||||
_justPlainLucky:
|
||||
title: "แค่ลัคกี้ธรรมดา"
|
||||
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที"
|
||||
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที"
|
||||
_setNameToSyuilo:
|
||||
title: "พระเจ้าคอมเพล็กซ์"
|
||||
description: "ตั้งชื่อของคุณเป็น \"syuilo\""
|
||||
@@ -1182,7 +1185,7 @@ _role:
|
||||
description: "คำอธิบายบทบาท"
|
||||
permission: "สิทธิ์ตามบทบาท"
|
||||
descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ"
|
||||
assignTarget: "กำหนดเป้าหมาย"
|
||||
assignTarget: "มอบหมาย"
|
||||
descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง"
|
||||
manual: "ปรับเอง"
|
||||
conditional: "มีเงื่อนไข"
|
||||
@@ -1593,12 +1596,15 @@ _permissions:
|
||||
"read:gallery-likes": "ดูรายการโพสต์ในแกลเลอรีที่ชอบของคุณ"
|
||||
"write:gallery-likes": "แก้ไขรายการโพสต์ในแกลเลอรีที่ชอบของคุณ"
|
||||
_auth:
|
||||
shareAccessTitle: "การให้สิทธิ์แอปพลิเคชัน"
|
||||
shareAccess: "คุณต้องการอนุญาตให้ \"{name}\" เข้าถึงบัญชีนี้เลยมั้ย?"
|
||||
shareAccessAsk: "คุณแน่ใจแล้วจริงๆหรอว่าต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณแน่ใจแล้วหรอ?"
|
||||
permission: "{name} ได้ขอสิทธิ์การเข้าถึงดังต่อไปนี้"
|
||||
permissionAsk: "แอปพลิเคชันนี้ขอสิทธิ์ดังต่อไปนี้"
|
||||
pleaseGoBack: "กรุณากลับไปที่แอปพลิเคชัน"
|
||||
callback: "กำลังกลับไปที่แอปพลิเคชัน"
|
||||
denied: "ปฏิเสธการเข้าใช้"
|
||||
pleaseLogin: "กรุณาเข้าสู่ระบบเพื่ออนุมัติแอปพลิเคชัน"
|
||||
_antennaSources:
|
||||
all: "โน้ตทั้งหมด"
|
||||
homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม"
|
||||
@@ -1869,5 +1875,6 @@ _deck:
|
||||
tl: "ไทม์ไลน์"
|
||||
antenna: "เสาอากาศ"
|
||||
list: "รายการ"
|
||||
channel: "แชนแนล"
|
||||
mentions: "พูดถึง"
|
||||
direct: "ไดเร็ค"
|
||||
|
@@ -1382,8 +1382,8 @@ _tutorial:
|
||||
step1_1: "Ласкаво просимо!"
|
||||
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані."
|
||||
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших."
|
||||
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис."
|
||||
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас."
|
||||
step2_1: "Перш ніж зробити запис або підписатись на когось, заповніть свій профіль."
|
||||
step2_2: "Надання деякої інформації про себе допоможе іншим користувачам вирішити підписатись на вас."
|
||||
step3_1: "Ви успішно налаштували свій обліковий запис?"
|
||||
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
|
||||
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
|
||||
@@ -1689,5 +1689,6 @@ _deck:
|
||||
tl: "Стрічка"
|
||||
antenna: "Антени"
|
||||
list: "Списки"
|
||||
channel: "Канали"
|
||||
mentions: "Згадки"
|
||||
direct: "Особисте"
|
||||
|
@@ -1520,5 +1520,6 @@ _deck:
|
||||
tl: "Bảng tin"
|
||||
antenna: "Trạm phát sóng"
|
||||
list: "Danh sách"
|
||||
channel: "Kênh"
|
||||
mentions: "Lượt nhắc"
|
||||
direct: "Nhắn riêng"
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "确定要解除拉黑吗?"
|
||||
suspendConfirm: "要冻结吗?"
|
||||
unsuspendConfirm: "要解除冻结吗?"
|
||||
selectList: "选择列表"
|
||||
selectChannel: "选择频道"
|
||||
selectAntenna: "选择天线"
|
||||
selectWidget: "选择小工具"
|
||||
editWidgets: "编辑部件"
|
||||
@@ -256,6 +257,8 @@ noMoreHistory: "没有更多的历史记录"
|
||||
startMessaging: "添加聊天"
|
||||
nUsersRead: "{n}人已读"
|
||||
agreeTo: "勾选则表示已阅读并同意{0}"
|
||||
agreeBelow: "同意以下观点"
|
||||
basicNotesBeforeCreateAccount: "基本注意事项"
|
||||
tos: "服务条款"
|
||||
start: "开始"
|
||||
home: "首页"
|
||||
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "获取账户信息失败"
|
||||
rateLimitExceeded: "已超過速率限制"
|
||||
cropImage: "剪裁图像"
|
||||
cropImageAsk: "是否要裁剪图像?"
|
||||
cropYes: "已裁剪"
|
||||
cropNo: "就这样吧!"
|
||||
file: "文件"
|
||||
recentNHours: "最近{n}小时"
|
||||
recentNDays: "最近{n}天"
|
||||
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,
|
||||
preset: "預設值"
|
||||
selectFromPresets: "從預設值中選擇"
|
||||
achievements: "成就"
|
||||
gotInvalidResponseError: "服务器无应答"
|
||||
gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。"
|
||||
_achievements:
|
||||
earnedAt: "达成时间"
|
||||
_types:
|
||||
@@ -1122,7 +1129,7 @@ _achievements:
|
||||
description: "在0点发布一篇帖子"
|
||||
flavor: "嘣 嘣 嘣 Biu——!"
|
||||
_selfQuote:
|
||||
title: "自我提及"
|
||||
title: "自我引用"
|
||||
description: "引用了自己的帖子"
|
||||
_htl20npm:
|
||||
title: "流动的时间线"
|
||||
@@ -1593,12 +1600,15 @@ _permissions:
|
||||
"read:gallery-likes": "读取喜欢的图片"
|
||||
"write:gallery-likes": "操作喜欢的图片"
|
||||
_auth:
|
||||
shareAccessTitle: "应用程序授权许可"
|
||||
shareAccess: "您要授权允许“{name}”访问您的帐户吗?"
|
||||
shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?"
|
||||
permission: "{name}需要以下权限"
|
||||
permissionAsk: "这个应用程序需要以下权限"
|
||||
pleaseGoBack: "请返回到应用程序"
|
||||
callback: "回到应用程序"
|
||||
denied: "拒绝访问"
|
||||
pleaseLogin: "在对应用进行授权许可之前,请先登录"
|
||||
_antennaSources:
|
||||
all: "所有帖子"
|
||||
homeTimeline: "已关注用户的帖子"
|
||||
@@ -1869,5 +1879,6 @@ _deck:
|
||||
tl: "时间线"
|
||||
antenna: "天线"
|
||||
list: "列表"
|
||||
channel: "频道"
|
||||
mentions: "提及"
|
||||
direct: "指定用户"
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "確定解除封鎖此用戶?"
|
||||
suspendConfirm: "確定凍結此帳號?"
|
||||
unsuspendConfirm: "確定解凍此帳號?"
|
||||
selectList: "選擇清單"
|
||||
selectChannel: "選擇頻道"
|
||||
selectAntenna: "選擇天線"
|
||||
selectWidget: "選擇小工具"
|
||||
editWidgets: "編輯小工具"
|
||||
@@ -256,6 +257,8 @@ noMoreHistory: "沒有更多歷史紀錄"
|
||||
startMessaging: "開始聊天"
|
||||
nUsersRead: "{n}人已讀"
|
||||
agreeTo: "我同意{0}"
|
||||
agreeBelow: "同意以下內容"
|
||||
basicNotesBeforeCreateAccount: "基本注意事項"
|
||||
tos: "使用條款"
|
||||
start: "開始"
|
||||
home: "首頁"
|
||||
@@ -326,7 +329,7 @@ connectService: "己連結"
|
||||
disconnectService: "己斷開 "
|
||||
enableLocalTimeline: "開啟本地時間軸"
|
||||
enableGlobalTimeline: "啟用全域時間軸"
|
||||
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審核員仍可以繼續使用。"
|
||||
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審查員仍可以繼續使用。"
|
||||
registration: "註冊"
|
||||
enableRegistration: "開啟新使用者註冊"
|
||||
invite: "邀請"
|
||||
@@ -389,8 +392,8 @@ aboutMisskey: "關於 Misskey"
|
||||
administrator: "管理員"
|
||||
token: "權杖"
|
||||
twoStepAuthentication: "兩階段驗證"
|
||||
moderator: "審核員"
|
||||
moderation: "監察"
|
||||
moderator: "審查員"
|
||||
moderation: "審查"
|
||||
nUsersMentioned: "提到了{n}"
|
||||
securityKey: "安全金鑰"
|
||||
securityKeyName: "金鑰名稱"
|
||||
@@ -607,7 +610,7 @@ testEmail: "測試郵件發送"
|
||||
wordMute: "被靜音的文字"
|
||||
regexpError: "正規表達式錯誤"
|
||||
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
|
||||
instanceMute: "實例的靜音"
|
||||
instanceMute: "被靜音的實例"
|
||||
userSaysSomething: "{name}說了什麼"
|
||||
makeActive: "啟用"
|
||||
display: "檢視"
|
||||
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "取得帳戶資訊失敗"
|
||||
rateLimitExceeded: "已超過速率限制"
|
||||
cropImage: "圖片裁剪"
|
||||
cropImageAsk: "要剪裁圖片嗎?"
|
||||
cropYes: "裁剪"
|
||||
cropNo: "使用原圖"
|
||||
file: "檔案"
|
||||
recentNHours: "過去{n}小時"
|
||||
recentNDays: "過去{n}天"
|
||||
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無
|
||||
preset: "預設值"
|
||||
selectFromPresets: "從預設值中選擇"
|
||||
achievements: "成就"
|
||||
gotInvalidResponseError: "伺服器的回應無效"
|
||||
gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。"
|
||||
_achievements:
|
||||
earnedAt: "獲得日期"
|
||||
_types:
|
||||
@@ -1181,7 +1188,7 @@ _role:
|
||||
name: "角色名稱"
|
||||
description: "角色描述 "
|
||||
permission: "角色的權限"
|
||||
descriptionOfPermission: "<b>審核員</b>執行與審核相關的基本操作。\n<b>管理員</b>能變更實例的全部設定。"
|
||||
descriptionOfPermission: "<b>審查員</b>執行與審查相關的基本操作。\n<b>管理員</b>能變更實例的全部設定"
|
||||
assignTarget: "指派目標"
|
||||
descriptionOfAssignTarget: "<b>手動</b>是以手動管理這個角色包含的人員。\n<b>符合條件</b>是設定條件以自動包含符合條件的使用者。"
|
||||
manual: "手動"
|
||||
@@ -1198,8 +1205,8 @@ _role:
|
||||
iconUrl: "圖示的URL"
|
||||
asBadge: "顯示為徽章"
|
||||
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
|
||||
canEditMembersByModerator: "允許編輯監察員的成員"
|
||||
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
|
||||
canEditMembersByModerator: "允許編輯審查員的成員"
|
||||
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
|
||||
priority: "優先級"
|
||||
_priority:
|
||||
low: "低"
|
||||
@@ -1236,7 +1243,7 @@ _role:
|
||||
or: "~或~"
|
||||
not: "~否"
|
||||
_sensitiveMediaDetection:
|
||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審查。 伺服器的負荷會稍微增加。"
|
||||
sensitivity: "檢測敏感度"
|
||||
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
|
||||
setSensitiveFlagAutomatically: "設定 NSFW 旗標"
|
||||
@@ -1593,12 +1600,15 @@ _permissions:
|
||||
"read:gallery-likes": "讀取喜歡的圖片"
|
||||
"write:gallery-likes": "操作喜歡的圖片"
|
||||
_auth:
|
||||
shareAccessTitle: "應用程式的存取權限"
|
||||
shareAccess: "要授權「“{name}”」存取您的帳戶嗎?"
|
||||
shareAccessAsk: "您確定要授權這個應用程式使用您的帳戶嗎?"
|
||||
permission: "{name}要求以下的權限"
|
||||
permissionAsk: "此應用程式需要以下權限"
|
||||
pleaseGoBack: "請返回至應用程式"
|
||||
callback: "回到應用程式"
|
||||
denied: "拒絕訪問"
|
||||
pleaseLogin: "必須登入以提供應用程式的存取權限。"
|
||||
_antennaSources:
|
||||
all: "全部貼文"
|
||||
homeTimeline: "來自已追隨使用者的貼文"
|
||||
@@ -1869,5 +1879,6 @@ _deck:
|
||||
tl: "時間軸"
|
||||
antenna: "天線"
|
||||
list: "清單"
|
||||
channel: "頻道"
|
||||
mentions: "提及"
|
||||
direct: "指定使用者"
|
||||
|
14
package.json
14
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.4.0",
|
||||
"version": "13.5.6",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@7.24.3",
|
||||
"packageManager": "pnpm@7.27.0",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
@@ -19,7 +19,7 @@
|
||||
"start": "cd packages/backend && node ./built/boot/index.js",
|
||||
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
|
||||
"init": "pnpm migrate",
|
||||
"migrate": "cd packages/backend && pnpm typeorm migration:run -d ormconfig.js",
|
||||
"migrate": "cd packages/backend && pnpm migrate",
|
||||
"migrateandstart": "pnpm migrate && pnpm start",
|
||||
"gulp": "pnpm exec gulp build",
|
||||
"watch": "pnpm dev",
|
||||
@@ -28,8 +28,8 @@
|
||||
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||
"cy:run": "pnpm cypress run",
|
||||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
"jest": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand",
|
||||
"jest-and-coverage": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand",
|
||||
"jest": "cd packages/backend && pnpm jest",
|
||||
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
||||
"test": "pnpm jest",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"format": "pnpm exec gulp format",
|
||||
@@ -54,8 +54,8 @@
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.5.1",
|
||||
"eslint": "8.33.0",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { loadConfig } from './built/config.js';
|
||||
import { entities } from './built/postgre.js';
|
||||
import { entities } from './built/postgres.js';
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
|
@@ -23,9 +23,9 @@
|
||||
"@tensorflow/tfjs-node": "4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "4.11.0",
|
||||
"@bull-board/fastify": "4.11.0",
|
||||
"@bull-board/ui": "4.11.0",
|
||||
"@bull-board/api": "4.11.1",
|
||||
"@bull-board/fastify": "4.11.1",
|
||||
"@bull-board/ui": "4.11.1",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@fastify/accepts": "4.1.0",
|
||||
"@fastify/cookie": "8.3.0",
|
||||
@@ -34,9 +34,9 @@
|
||||
"@fastify/multipart": "7.4.0",
|
||||
"@fastify/static": "6.8.0",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.3.1",
|
||||
"@nestjs/core": "9.3.1",
|
||||
"@nestjs/testing": "9.3.1",
|
||||
"@nestjs/common": "9.3.7",
|
||||
"@nestjs/core": "9.3.7",
|
||||
"@nestjs/testing": "9.3.7",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"accepts": "1.3.8",
|
||||
@@ -46,7 +46,7 @@
|
||||
"aws-sdk": "2.1295.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.4",
|
||||
"bull": "4.10.2",
|
||||
"bull": "4.10.3",
|
||||
"cacheable-lookup": "6.1.0",
|
||||
"cbor": "8.1.0",
|
||||
"chalk": "5.2.0",
|
||||
@@ -90,7 +90,7 @@
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.3.0",
|
||||
"pureimage": "0.3.15",
|
||||
"pureimage": "0.3.17",
|
||||
"qrcode": "1.5.1",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
@@ -111,12 +111,12 @@
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"systeminformation": "5.17.8",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typeorm": "0.3.12",
|
||||
"typescript": "4.9.5",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
@@ -128,10 +128,10 @@
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.4.1",
|
||||
"@jest/globals": "29.4.2",
|
||||
"@redocly/openapi-core": "1.0.0-beta.123",
|
||||
"@swc/cli": "0.1.61",
|
||||
"@swc/core": "1.3.32",
|
||||
"@swc/core": "1.3.34",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.1",
|
||||
@@ -145,11 +145,11 @@
|
||||
"@types/ioredis": "4.28.10",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "20.0.1",
|
||||
"@types/jsdom": "21.1.0",
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.5",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node": "18.13.0",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/oauth": "0.9.1",
|
||||
@@ -174,13 +174,13 @@
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.4.1",
|
||||
"jest-mock": "29.4.1"
|
||||
"jest": "29.4.2",
|
||||
"jest-mock": "29.4.2"
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import { DataSource } from 'typeorm';
|
||||
import { createRedisConnection } from '@/redis.js';
|
||||
import { DI } from './di-symbols.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { createPostgreDataSource } from './postgre.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@@ -18,7 +18,7 @@ const $config: Provider = {
|
||||
const $db: Provider = {
|
||||
provide: DI.db,
|
||||
useFactory: async (config) => {
|
||||
const db = createPostgreDataSource(config);
|
||||
const db = createPostgresDataSource(config);
|
||||
return await db.initialize();
|
||||
},
|
||||
inject: [DI.config],
|
||||
|
@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
|
||||
const ACHIEVEMENT_TYPES = [
|
||||
export const ACHIEVEMENT_TYPES = [
|
||||
'notes1',
|
||||
'notes10',
|
||||
'notes100',
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Brackets, ObjectLiteral } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class QueryService {
|
||||
@@ -32,7 +32,7 @@ export class QueryService {
|
||||
) {
|
||||
}
|
||||
|
||||
public makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
|
||||
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
|
||||
if (sinceId && untilId) {
|
||||
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
|
||||
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
|
||||
|
@@ -48,6 +48,10 @@ export class ApImageService {
|
||||
throw new Error('invalid image: url not privided');
|
||||
}
|
||||
|
||||
if (!image.url.startsWith('https://')) {
|
||||
throw new Error('invalid image: unexpected shcema of url: ' + image.url);
|
||||
}
|
||||
|
||||
this.logger.info(`Creating the Image: ${image.url}`);
|
||||
|
||||
const instance = await this.metaService.fetch();
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { CacheableRemoteUser } from '@/models/entities/User.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
@@ -18,6 +17,7 @@ import { PollService } from '@/core/PollService.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { MessagingService } from '@/core/MessagingService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
@@ -32,7 +32,6 @@ import { ApQuestionService } from './ApQuestionService.js';
|
||||
import { ApImageService } from './ApImageService.js';
|
||||
import type { Resolver } from '../ApResolverService.js';
|
||||
import type { IObject, IPost } from '../type.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApNoteService {
|
||||
@@ -133,6 +132,16 @@ export class ApNoteService {
|
||||
const note: IPost = object;
|
||||
|
||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
|
||||
if (note.id && !note.id.startsWith('https://')) {
|
||||
throw new Error('unexpected shcema of note.id: ' + note.id);
|
||||
}
|
||||
|
||||
const url = getOneApHrefNullable(note.url);
|
||||
|
||||
if (url && !url.startsWith('https://')) {
|
||||
throw new Error('unexpected shcema of note url: ' + url);
|
||||
}
|
||||
|
||||
this.logger.info(`Creating the Note: ${note.id}`);
|
||||
|
||||
@@ -307,7 +316,7 @@ export class ApNoteService {
|
||||
apEmojis,
|
||||
poll,
|
||||
uri: note.id,
|
||||
url: getOneApHrefNullable(note.url),
|
||||
url: url,
|
||||
}, silent);
|
||||
}
|
||||
|
||||
|
@@ -252,6 +252,12 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
||||
if (url && !url.startsWith('https://')) {
|
||||
throw new Error('unexpected shcema of person url: ' + url);
|
||||
}
|
||||
|
||||
// Create user
|
||||
let user: IRemoteUser;
|
||||
try {
|
||||
@@ -283,7 +289,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
await transactionalEntityManager.save(new UserProfile({
|
||||
userId: user.id,
|
||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||
url: getOneApHrefNullable(person.url),
|
||||
url: url,
|
||||
fields,
|
||||
birthday: bday ? bday[0] : null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
@@ -425,6 +431,12 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
||||
if (url && !url.startsWith('https://')) {
|
||||
throw new Error('unexpected shcema of person url: ' + url);
|
||||
}
|
||||
|
||||
const updates = {
|
||||
lastFetchedAt: new Date(),
|
||||
inbox: person.inbox,
|
||||
@@ -459,7 +471,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
}
|
||||
|
||||
await this.userProfilesRepository.update({ userId: exist.id }, {
|
||||
url: getOneApHrefNullable(person.url),
|
||||
url: url,
|
||||
fields,
|
||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||
birthday: bday ? bday[0] : null,
|
||||
|
@@ -20,6 +20,7 @@ type PackOptions = {
|
||||
withUser?: boolean,
|
||||
};
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
|
||||
@Injectable()
|
||||
export class DriveFileEntityService {
|
||||
@@ -82,7 +83,9 @@ export class DriveFileEntityService {
|
||||
|
||||
// リモートかつメディアプロキシ
|
||||
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||
return proxiedUrl(file.uri);
|
||||
if (!(mode === 'static' && file.type.startsWith('video'))) {
|
||||
return proxiedUrl(file.uri);
|
||||
}
|
||||
}
|
||||
|
||||
// リモートかつ期限切れはローカルプロキシを試みる
|
||||
@@ -91,20 +94,19 @@ export class DriveFileEntityService {
|
||||
|
||||
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
|
||||
const url = `${this.config.url}/files/${key}`;
|
||||
if (mode === 'avatar') return proxiedUrl(url);
|
||||
if (mode === 'avatar') return proxiedUrl(file.uri);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/avif', 'image/svg+xml'].includes(file.type);
|
||||
|
||||
if (mode === 'static') {
|
||||
return file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null);
|
||||
}
|
||||
|
||||
const url = file.webpublicUrl ?? file.url;
|
||||
|
||||
if (mode === 'avatar') return proxiedUrl(url);
|
||||
if (mode === 'static') {
|
||||
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null);
|
||||
}
|
||||
if (mode === 'avatar') {
|
||||
return proxiedUrl(url);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
* The getter will return a .bind version of the function
|
||||
* and memoize the result against a symbol on the instance
|
||||
*/
|
||||
export function bindThis(target, key, descriptor) {
|
||||
export function bindThis(target: any, key: string, descriptor: any) {
|
||||
let fn = descriptor.value;
|
||||
|
||||
if (typeof fn !== 'function') {
|
||||
@@ -34,7 +34,7 @@ export function bindThis(target, key, descriptor) {
|
||||
});
|
||||
return boundFn;
|
||||
},
|
||||
set(value) {
|
||||
set(value: any) {
|
||||
fn = value;
|
||||
},
|
||||
};
|
||||
|
@@ -45,7 +45,7 @@ export default class Logger {
|
||||
}
|
||||
|
||||
const time = dateFormat(new Date(), 'HH:mm:ss');
|
||||
const worker = cluster.isPrimary ? '*' : cluster.worker.id;
|
||||
const worker = cluster.isPrimary ? '*' : cluster.worker!.id;
|
||||
const l =
|
||||
level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
|
||||
level === 'warning' ? chalk.yellow('WARN') :
|
||||
|
@@ -51,7 +51,7 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
|
||||
bg.addColorStop(0, bgColors[0]);
|
||||
bg.addColorStop(1, bgColors[1]);
|
||||
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillStyle = bg as any;
|
||||
ctx.beginPath();
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
|
@@ -11,10 +11,9 @@ export class I18n<T extends Record<string, any>> {
|
||||
|
||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
@bindThis
|
||||
public t(key: string, args?: Record<string, any>): string {
|
||||
try {
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.locale as any) as string;
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
|
@@ -197,7 +197,7 @@ export const entities = [
|
||||
|
||||
const log = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export function createPostgreDataSource(config: Config) {
|
||||
export function createPostgresDataSource(config: Config) {
|
||||
return new DataSource({
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
@@ -1,3 +1,4 @@
|
||||
import { IncomingMessage } from 'node:http';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import fastifyAccepts from '@fastify/accepts';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
@@ -19,6 +20,7 @@ import { QueryService } from '@/core/QueryService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
|
||||
@@ -97,7 +99,8 @@ export class ActivityPubServerService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.queueService.inbox(request.body, signature);
|
||||
// TODO: request.bodyのバリデーション?
|
||||
this.queueService.inbox(request.body as IActivity, signature);
|
||||
|
||||
reply.code(202);
|
||||
}
|
||||
@@ -413,20 +416,21 @@ export class ActivityPubServerService {
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.addConstraintStrategy({
|
||||
// addConstraintStrategy の型定義がおかしいため
|
||||
(fastify.addConstraintStrategy as any)({
|
||||
name: 'apOrHtml',
|
||||
storage() {
|
||||
const store = {};
|
||||
const store = {} as any;
|
||||
return {
|
||||
get(key) {
|
||||
get(key: string) {
|
||||
return store[key] ?? null;
|
||||
},
|
||||
set(key, value) {
|
||||
set(key: string, value: any) {
|
||||
store[key] = value;
|
||||
},
|
||||
};
|
||||
},
|
||||
deriveConstraint(request, ctx) {
|
||||
deriveConstraint(request: IncomingMessage) {
|
||||
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
|
||||
const isAp = typeof accepted === 'string' && !accepted.match(/html/);
|
||||
return isAp ? 'ap' : 'html';
|
||||
@@ -536,6 +540,7 @@ export class ActivityPubServerService {
|
||||
return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair)));
|
||||
} else {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -255,8 +255,21 @@ export class FileServerService {
|
||||
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
|
||||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
|
||||
|
||||
if (
|
||||
'emoji' in request.query ||
|
||||
'avatar' in request.query ||
|
||||
'static' in request.query ||
|
||||
'preview' in request.query ||
|
||||
'badge' in request.query
|
||||
) {
|
||||
if (!isConvertibleImage) {
|
||||
// 画像でないなら404でお茶を濁す
|
||||
throw new StatusError('Unexpected mime', 404);
|
||||
}
|
||||
}
|
||||
|
||||
let image: IImageStreamable | null = null;
|
||||
if (('emoji' in request.query || 'avatar' in request.query) && isConvertibleImage) {
|
||||
if ('emoji' in request.query || 'avatar' in request.query) {
|
||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
@@ -277,16 +290,11 @@ export class FileServerService {
|
||||
type: 'image/webp',
|
||||
};
|
||||
}
|
||||
} else if ('static' in request.query && isConvertibleImage) {
|
||||
} else if ('static' in request.query) {
|
||||
image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280);
|
||||
} else if ('preview' in request.query && isConvertibleImage) {
|
||||
} else if ('preview' in request.query) {
|
||||
image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200);
|
||||
} else if ('badge' in request.query) {
|
||||
if (!isConvertibleImage) {
|
||||
// 画像でないなら404でお茶を濁す
|
||||
throw new StatusError('Unexpected mime', 404);
|
||||
}
|
||||
|
||||
const mask = sharp(file.path)
|
||||
.resize(96, 96, {
|
||||
fit: 'inside',
|
||||
|
@@ -166,6 +166,7 @@ export class ServerService {
|
||||
return 'Verify succeeded!';
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -34,7 +34,7 @@ export class RateLimiterService {
|
||||
const min = (): void => {
|
||||
const minIntervalLimiter = new Limiter({
|
||||
id: `${actor}:${limitation.key}:min`,
|
||||
duration: limitation.minInterval * factor,
|
||||
duration: limitation.minInterval! * factor,
|
||||
max: 1,
|
||||
db: this.redisClient,
|
||||
});
|
||||
@@ -62,8 +62,8 @@ export class RateLimiterService {
|
||||
const max = (): void => {
|
||||
const limiter = new Limiter({
|
||||
id: `${actor}:${limitation.key}`,
|
||||
duration: limitation.duration * factor,
|
||||
max: limitation.max / factor,
|
||||
duration: limitation.duration! * factor,
|
||||
max: limitation.max! / factor,
|
||||
db: this.redisClient,
|
||||
});
|
||||
|
||||
|
@@ -10,9 +10,9 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
@@ -131,7 +131,7 @@ export class SigninApiService {
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: request.ip,
|
||||
headers: request.headers,
|
||||
headers: request.headers as any,
|
||||
success: false,
|
||||
});
|
||||
|
||||
|
@@ -25,7 +25,7 @@ export class SigninService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) {
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) {
|
||||
setImmediate(async () => {
|
||||
// Append signin history
|
||||
const record = await this.signinsRepository.insert({
|
||||
@@ -33,7 +33,7 @@ export class SigninService {
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: request.ip,
|
||||
headers: request.headers,
|
||||
headers: request.headers as any,
|
||||
success: true,
|
||||
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
@@ -41,25 +41,11 @@ export class SigninService {
|
||||
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
|
||||
});
|
||||
|
||||
if (redirect) {
|
||||
//#region Cookie
|
||||
reply.setCookie('igi', user.token!, {
|
||||
path: '/',
|
||||
// SEE: https://github.com/koajs/koa/issues/974
|
||||
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
|
||||
secure: this.config.url.startsWith('https'),
|
||||
httpOnly: false,
|
||||
});
|
||||
//#endregion
|
||||
|
||||
reply.redirect(this.config.url);
|
||||
} else {
|
||||
reply.code(200);
|
||||
return {
|
||||
id: user.id,
|
||||
i: user.token,
|
||||
};
|
||||
}
|
||||
reply.code(200);
|
||||
return {
|
||||
id: user.id,
|
||||
i: user.token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -146,6 +146,7 @@ export class SignupApiService {
|
||||
`To complete signup, please click this link: ${link}`);
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
const { account, secret } = await this.signupService.signup({
|
||||
@@ -162,7 +163,7 @@ export class SignupApiService {
|
||||
token: secret,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,7 +196,7 @@ export class SignupApiService {
|
||||
|
||||
return this.signinService.signin(request, reply, account as ILocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ export const paramDef = {
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
target: { type: 'string', enum: ['manual', 'conditional'] },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
|
@@ -28,7 +28,7 @@ export const paramDef = {
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
target: { type: 'string', enum: ['manual', 'conditional'] },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { getJsonSchema } from '@/core/chart/core.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
|
||||
import { schema } from '@/core/chart/charts/entities/per-user-notes.js';
|
||||
import { schema } from '@/core/chart/charts/entities/per-user-pv.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['charts', 'users'],
|
||||
|
@@ -27,7 +27,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
return {
|
||||
params: Object.entries(ep.params.properties ?? {}).map(([k, v]) => ({
|
||||
name: k,
|
||||
type: v.type.charAt(0).toUpperCase() + v.type.slice(1),
|
||||
type: v.type ? v.type.charAt(0).toUpperCase() + v.type.slice(1) : 'string',
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { AchievementService } from '@/core/AchievementService.js';
|
||||
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
@@ -10,7 +10,7 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
name: { type: 'string', enum: ACHIEVEMENT_TYPES },
|
||||
},
|
||||
required: ['name'],
|
||||
} as const;
|
||||
|
@@ -15,8 +15,8 @@ export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: 60000,
|
||||
max: 15,
|
||||
duration: 30000,
|
||||
max: 30,
|
||||
},
|
||||
|
||||
kind: 'read:notifications',
|
||||
|
@@ -155,7 +155,7 @@ export class ClientServerService {
|
||||
});
|
||||
|
||||
serverAdapter.setBasePath(bullBoardPath);
|
||||
fastify.register(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
|
||||
(fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
|
||||
//#endregion
|
||||
|
||||
fastify.register(fastifyView, {
|
||||
@@ -337,7 +337,7 @@ export class ClientServerService {
|
||||
|
||||
const renderBase = async (reply: FastifyReply) => {
|
||||
const meta = await this.metaService.fetch();
|
||||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
reply.header('Cache-Control', 'public, max-age=30');
|
||||
return await reply.view('base', {
|
||||
img: meta.bannerUrl,
|
||||
title: meta.name ?? 'Misskey',
|
||||
@@ -372,6 +372,7 @@ export class ClientServerService {
|
||||
return feed.atom1();
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -384,6 +385,7 @@ export class ClientServerService {
|
||||
return feed.rss2();
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -396,6 +398,7 @@ export class ClientServerService {
|
||||
return feed.json1();
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -35,7 +35,8 @@ html
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css')
|
||||
//- https://github.com/misskey-dev/misskey/issues/9842
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.2.0')
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
|
||||
if !config.clientManifestExists
|
||||
|
@@ -11,7 +11,7 @@ import FormData from 'form-data';
|
||||
import { DataSource } from 'typeorm';
|
||||
import got, { RequestError } from 'got';
|
||||
import loadConfig from '../src/config/load.js';
|
||||
import { entities } from '../src/postgre.js';
|
||||
import { entities } from '@/postgres.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
|
@@ -12,7 +12,7 @@
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.12.4",
|
||||
"@tabler/icons-webfont": "2.1.2",
|
||||
"@tabler/icons-webfont": "2.2.0",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/compiler-sfc": "3.2.47",
|
||||
"autobind-decorator": "2.4.0",
|
||||
@@ -23,7 +23,7 @@
|
||||
"canvas-confetti": "1.6.0",
|
||||
"chart.js": "4.2.0",
|
||||
"chartjs-adapter-date-fns": "3.0.0",
|
||||
"chartjs-chart-matrix": "1.3.0",
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.0",
|
||||
"compare-versions": "5.0.1",
|
||||
@@ -44,7 +44,7 @@
|
||||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.12.1",
|
||||
"rollup": "3.14.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.9.0",
|
||||
"sass": "1.58.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.149.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
@@ -74,7 +74,7 @@
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node": "18.13.0",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.8.0",
|
||||
"@types/seedrandom": "3.0.4",
|
||||
@@ -83,8 +83,8 @@
|
||||
"@types/uuid": "9.0.0",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"@vue/runtime-core": "3.2.47",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.5.1",
|
||||
|
@@ -61,8 +61,6 @@ export async function signout() {
|
||||
} catch (err) {}
|
||||
//#endregion
|
||||
|
||||
document.cookie = 'igi=; path=/';
|
||||
|
||||
if (accounts.length > 0) login(accounts[0].token);
|
||||
else unisonReload('/');
|
||||
}
|
||||
|
@@ -107,6 +107,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.iconFrame {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
padding: 6px;
|
||||
|
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="container">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
|
||||
<img ref="imgEl" :src="imgUrl" style="display: none;" crossorigin="anonymous" @load="onImageLoad">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button class="_button" :class="$style.root" @click="toggle">
|
||||
<button class="_button" :class="$style.root" @mousedown="toggle">
|
||||
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
|
||||
<span v-if="!modelValue" :class="$style.label">{{ label }}</span>
|
||||
</button>
|
||||
|
@@ -28,8 +28,8 @@
|
||||
</template>
|
||||
</MkSelect>
|
||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
<div v-if="actions" :class="$style.buttons">
|
||||
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
|
||||
@@ -82,6 +82,8 @@ const props = withDefaults(defineProps<{
|
||||
showOkButton?: boolean;
|
||||
showCancelButton?: boolean;
|
||||
cancelableByBgClick?: boolean;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
}>(), {
|
||||
type: 'info',
|
||||
showOkButton: true,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()">
|
||||
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
|
||||
<div ref="emojisEl" class="emojis">
|
||||
<section class="result">
|
||||
<div v-if="searchResultCustom.length > 0" class="body">
|
||||
@@ -327,6 +327,11 @@ function paste(event: ClipboardEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function onEnter(ev: KeyboardEvent) {
|
||||
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
||||
done();
|
||||
}
|
||||
|
||||
function done(query?: string): boolean | void {
|
||||
if (query == null) query = q.value;
|
||||
if (query == null || typeof query !== 'string') return;
|
||||
|
@@ -42,7 +42,7 @@ import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number;
|
||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search';
|
||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@@ -6,15 +6,14 @@
|
||||
<span>{{ $ts.clickToShow }}</span>
|
||||
</div>
|
||||
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
|
||||
<audio
|
||||
ref="audioEl"
|
||||
class="audio"
|
||||
:src="media.url"
|
||||
:title="media.name"
|
||||
controls
|
||||
preload="metadata"
|
||||
@volumechange="volumechange"
|
||||
/>
|
||||
<VuePlyr :options="{ volume: 0.5 }">
|
||||
<audio controls preload="metadata">
|
||||
<source
|
||||
:src="media.url"
|
||||
:type="media.type"
|
||||
/>
|
||||
</audio>
|
||||
</VuePlyr>
|
||||
</div>
|
||||
<a
|
||||
v-else class="download"
|
||||
@@ -31,7 +30,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import VuePlyr from 'vue-plyr';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import 'vue-plyr/dist/vue-plyr.css';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
media: misskey.entities.DriveFile;
|
||||
@@ -55,7 +56,11 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
// overflow: clip;
|
||||
|
||||
--plyr-color-main: var(--accent);
|
||||
--plyr-audio-controls-background: var(--bg);
|
||||
--plyr-audio-controls-color: var(--accentLighten);
|
||||
|
||||
> .download,
|
||||
> .sensitive {
|
||||
@@ -93,10 +98,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
> .audio {
|
||||
.audio {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
border-radius: 8px;
|
||||
// overflow: clip;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,22 +1,23 @@
|
||||
<template>
|
||||
<div v-if="hide" class="qjewsnkg" @click="hide = false">
|
||||
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
||||
<div class="text">
|
||||
<div class="wrapper">
|
||||
<div v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ $ts.sensitive }}</b>
|
||||
<span style="display: block;">{{ $ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="gqnyydlz">
|
||||
<div v-else :class="$style.visible">
|
||||
<a
|
||||
:class="$style.imageContainer"
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
>
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :cover="false"/>
|
||||
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
|
||||
<div v-if="image.type === 'image/gif'" :class="$style.gif">GIF</div>
|
||||
</a>
|
||||
<button v-tooltip="$ts.hide" class="_button hide" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
<button v-tooltip="$ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,82 +50,77 @@ watch(() => props.image, () => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qjewsnkg {
|
||||
<style lang="scss" module>
|
||||
.hidden {
|
||||
position: relative;
|
||||
|
||||
> .bg {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
|
||||
> .text {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> .wrapper {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gqnyydlz {
|
||||
.hiddenText {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hiddenTextWrapper {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.visible {
|
||||
position: relative;
|
||||
//box-shadow: 0 0 0 1px var(--divider) inset;
|
||||
background: var(--bg);
|
||||
--c: rgb(0 0 0 / 2%);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
> .hide {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--accentedBg);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
color: var(--accent);
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
.hide {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--accentedBg);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
color: var(--accent);
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
> i {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.imageContainer {
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
> a {
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
> .gif {
|
||||
background-color: var(--fg);
|
||||
border-radius: 6px;
|
||||
color: var(--accentLighten);
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
left: 12px;
|
||||
opacity: .5;
|
||||
padding: 0 6px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.gif {
|
||||
background-color: var(--fg);
|
||||
border-radius: 6px;
|
||||
color: var(--accentLighten);
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
left: 12px;
|
||||
opacity: .5;
|
||||
padding: 0 6px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="hoawjimk">
|
||||
<div>
|
||||
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
|
||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
|
||||
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length">
|
||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
|
||||
<div ref="gallery" :class="[$style.medias, count <= 4 ? $style['n' + count] : $style.nMany]">
|
||||
<template v-for="media in mediaList.filter(media => previewable(media))">
|
||||
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
|
||||
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
|
||||
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :class="$style.media" :video="media"/>
|
||||
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" :class="$style.media" class="image" :data-id="media.id" :image="media" :raw="raw"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,6 +32,7 @@ const props = defineProps<{
|
||||
|
||||
const gallery = ref(null);
|
||||
const pswpZIndex = os.claimZIndex('middle');
|
||||
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||
|
||||
onMounted(() => {
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
@@ -122,82 +123,64 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hoawjimk {
|
||||
> .gird-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
padding-top: 56.25% // 16:9;
|
||||
.medias {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
|
||||
// for webkit
|
||||
height: 100%;
|
||||
|
||||
&.n1 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
&.n2 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
&.n3 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-columns: 1fr 0.5fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
> .media:nth-child(1) {
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
> div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
|
||||
> * {
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&[data-count="1"] {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
&[data-count="2"] {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
&[data-count="3"] {
|
||||
grid-template-columns: 1fr 0.5fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
> *:nth-child(1) {
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
> *:nth-child(3) {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-count="4"] {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
> *:nth-child(1) {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
> *:nth-child(2) {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
> *:nth-child(3) {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
> *:nth-child(4) {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
> .media:nth-child(3) {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
&.n4 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
&.nMany {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
> .media {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
overflow: hidden; // clipにするとバグる
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
|
||||
<vue-plyr>
|
||||
<VuePlyr :options="{ volume: 0.5 }">
|
||||
<video
|
||||
controls
|
||||
:data-poster="video.thumbnailUrl"
|
||||
@@ -17,7 +17,7 @@
|
||||
:type="video.type"
|
||||
/>
|
||||
</video>
|
||||
</vue-plyr>
|
||||
</VuePlyr>
|
||||
<i class="ti ti-eye-off" @click="hide = true"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -48,7 +48,7 @@
|
||||
<div :class="$style.text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else :class="$style.translated">
|
||||
|
@@ -215,7 +215,7 @@ useTooltip(reactionRef, (showing) => {
|
||||
border-radius: 100%;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 0 0 3px var(--panel);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
|
||||
|
@@ -50,13 +50,13 @@
|
||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="tou">
|
||||
<I18n :src="i18n.ts.agreeTo">
|
||||
<template #0>
|
||||
<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
<MkSwitch v-model="ToSAgreement" class="tou">
|
||||
<template #label>{{ i18n.ts.agreeBelow }}</template>
|
||||
</MkSwitch>
|
||||
<ul style="margin: 0; padding-left: 2em;">
|
||||
<li v-if="instance.tosUrl"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a></li>
|
||||
<li><a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }}</a></li>
|
||||
</ul>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
@@ -1,11 +1,19 @@
|
||||
<template>
|
||||
<span v-if="!link" v-user-preview="preview ? user.id : undefined" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: $store.state.squareAvatars }]" class="_noSelect" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<span v-if="!link" v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: $store.state.squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<img :class="$style.inner" :src="url" decoding="async"/>
|
||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||
<template v-if="user.isCat">
|
||||
<div :class="$style.earLeft"/>
|
||||
<div :class="$style.earRight"/>
|
||||
</template>
|
||||
</span>
|
||||
<MkA v-else v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: $store.state.squareAvatars }]" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
|
||||
<MkA v-else v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: $store.state.squareAvatars }]" :style="{ color }" :title="acct(user)" :to="userPage(user)" :target="target">
|
||||
<img :class="$style.inner" :src="url" decoding="async"/>
|
||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||
<template v-if="user.isCat">
|
||||
<div :class="$style.earLeft"/>
|
||||
<div :class="$style.earRight"/>
|
||||
</template>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
@@ -110,32 +118,41 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||
}
|
||||
|
||||
.cat {
|
||||
&:before, &:after {
|
||||
background: #df548f;
|
||||
border: solid 4px currentColor;
|
||||
box-sizing: border-box;
|
||||
content: '';
|
||||
> .earLeft,
|
||||
> .earRight {
|
||||
contain: strict;
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
background: currentColor;
|
||||
|
||||
&::before {
|
||||
contain: strict;
|
||||
content: '';
|
||||
display: block;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
margin: 20%;
|
||||
background: #df548f;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
> .earLeft {
|
||||
border-radius: 0 75% 75%;
|
||||
transform: rotate(37.5deg) skew(30deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
> .earRight {
|
||||
border-radius: 75% 0 75% 75%;
|
||||
transform: rotate(-37.5deg) skew(-30deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:before {
|
||||
> .earLeft {
|
||||
animation: earwiggleleft 1s infinite;
|
||||
}
|
||||
|
||||
&:after {
|
||||
> .earRight {
|
||||
animation: earwiggleright 1s infinite;
|
||||
}
|
||||
}
|
||||
|
@@ -195,7 +195,7 @@ export default defineComponent({
|
||||
return h(MkSparkle, {}, genEl(token.children));
|
||||
}
|
||||
case 'rotate': {
|
||||
const degrees = parseFloat(token.props.args.deg) ?? '90';
|
||||
const degrees = parseFloat(token.props.args.deg ?? '90');
|
||||
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
|
||||
break;
|
||||
}
|
||||
|
@@ -35,6 +35,9 @@ export const apiWithDialog = ((
|
||||
} else if (err.code.startsWith('TOO_MANY')) {
|
||||
title = i18n.ts.youCannotCreateAnymore;
|
||||
text = `${i18n.ts.error}: ${err.id}`;
|
||||
} else if (err.message.startsWith('Unexpected token')) {
|
||||
title = i18n.ts.gotInvalidResponseError;
|
||||
text = i18n.ts.gotInvalidResponseErrorDescription;
|
||||
}
|
||||
alert({
|
||||
type: 'error',
|
||||
@@ -168,6 +171,8 @@ export function confirm(props: {
|
||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||
title?: string | null;
|
||||
text?: string | null;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
}): Promise<{ canceled: boolean }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
popup(MkDialog, {
|
||||
|
@@ -73,7 +73,13 @@
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-gap: 12px;">
|
||||
<div :class="$style.patronsWithIcon">
|
||||
<div v-for="patron in patronsWithIcon" :class="$style.patronWithIcon">
|
||||
<img :src="patron.icon" :class="$style.patronIcon">
|
||||
<span :class="$style.patronName">{{ patron.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-gap: 12px;">
|
||||
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
|
||||
</div>
|
||||
<p>{{ i18n.ts._aboutMisskey.morePatrons }}</p>
|
||||
@@ -99,6 +105,14 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const patronsWithIcon = [{
|
||||
name: 'カイヤン',
|
||||
icon: 'https://misskey-hub.net/patrons/a2820716883e408cb87773e377ce7c8d.jpg',
|
||||
}, {
|
||||
name: 'だれかさん',
|
||||
icon: 'https://misskey-hub.net/patrons/f7409b5e5a88477a9b9d740c408de125.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
'まっちゃとーにゅ',
|
||||
'mametsuko',
|
||||
@@ -352,4 +366,27 @@ definePageMetadata({
|
||||
.contributorUsername {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.patronsWithIcon {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
|
||||
.patronWithIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: var(--buttonBg);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.patronIcon {
|
||||
width: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.patronName {
|
||||
margin-left: 12px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -29,7 +29,7 @@
|
||||
<MkInput v-model="ad.ratio" type="number">
|
||||
<template #label>{{ i18n.ts.ratio }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.expiresAt" type="date">
|
||||
<MkInput v-model="ad.expiresAt" type="datetime-local">
|
||||
<template #label>{{ i18n.ts.expiration }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
@@ -61,7 +61,12 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
let ads: any[] = $ref([]);
|
||||
|
||||
os.api('admin/ad/list').then(adsResponse => {
|
||||
ads = adsResponse;
|
||||
ads = adsResponse.map(r => {
|
||||
return {
|
||||
...r,
|
||||
expiresAt: new Date(r.expiresAt).toISOString().slice(0, 16),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function add() {
|
||||
|
@@ -1,60 +1,66 @@
|
||||
<template>
|
||||
<section class="">
|
||||
<div class="">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
|
||||
<div class="">
|
||||
<h2>{{ app.name }}</h2>
|
||||
<p class="id">{{ app.id }}</p>
|
||||
<p class="description">{{ app.description }}</p>
|
||||
</div>
|
||||
<div class="">
|
||||
<h2>{{ $ts._auth.permissionAsk }}</h2>
|
||||
<ul>
|
||||
<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="">
|
||||
<MkButton inline @click="cancel">{{ $ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ $ts.accept }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div v-if="app.permission.length > 0">
|
||||
<p>{{ $t('_auth.permission', { name }) }}</p>
|
||||
<ul>
|
||||
<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>{{ i18n.t('_auth.shareAccess', { name: `${name} (${app.id})` }) }}</div>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { AuthSession } from 'misskey-js/built/entities';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
props: ['session'],
|
||||
computed: {
|
||||
name(): string {
|
||||
const el = document.createElement('div');
|
||||
el.textContent = this.app.name;
|
||||
return el.innerHTML;
|
||||
},
|
||||
app(): any {
|
||||
return this.session.app;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
os.api('auth/deny', {
|
||||
token: this.session.token,
|
||||
}).then(() => {
|
||||
this.$emit('denied');
|
||||
});
|
||||
},
|
||||
const props = defineProps<{
|
||||
session: AuthSession;
|
||||
}>();
|
||||
|
||||
accept() {
|
||||
os.api('auth/accept', {
|
||||
token: this.session.token,
|
||||
}).then(() => {
|
||||
this.$emit('accepted');
|
||||
});
|
||||
},
|
||||
},
|
||||
const emit = defineEmits<{
|
||||
(event: 'accepted'): void;
|
||||
(event: 'denied'): void;
|
||||
}>();
|
||||
|
||||
const app = $computed(() => props.session.app);
|
||||
|
||||
const name = $computed(() => {
|
||||
const el = document.createElement('div');
|
||||
el.textContent = app.name;
|
||||
return el.innerHTML;
|
||||
});
|
||||
|
||||
function cancel() {
|
||||
os.api('auth/deny', {
|
||||
token: props.session.token,
|
||||
}).then(() => {
|
||||
emit('denied');
|
||||
});
|
||||
}
|
||||
|
||||
function accept() {
|
||||
os.api('auth/accept', {
|
||||
token: props.session.token,
|
||||
}).then(() => {
|
||||
emit('accepted');
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.buttons {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,91 +1,105 @@
|
||||
<template>
|
||||
<div v-if="$i && fetching" class="">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
<div v-else-if="$i">
|
||||
<XForm
|
||||
v-if="state == 'waiting'"
|
||||
ref="form"
|
||||
class="form"
|
||||
:session="session"
|
||||
@denied="state = 'denied'"
|
||||
@accepted="accepted"
|
||||
/>
|
||||
<div v-if="state == 'denied'" class="denied">
|
||||
<h1>{{ $ts._auth.denied }}</h1>
|
||||
</div>
|
||||
<div v-if="state == 'accepted'" class="accepted">
|
||||
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : $ts.allowed }}</h1>
|
||||
<p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
<div v-if="state == 'fetch-session-error'" class="error">
|
||||
<p>{{ $ts.somethingHappened }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="signin">
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs" /></template>
|
||||
<MkSpacer :content-max="500">
|
||||
<div v-if="state == 'fetch-session-error'">
|
||||
<p>{{ i18n.ts.somethingHappened }}</p>
|
||||
</div>
|
||||
<div v-else-if="$i && !session">
|
||||
<MkLoading />
|
||||
</div>
|
||||
<div v-else-if="$i && session">
|
||||
<XForm
|
||||
v-if="state == 'waiting'"
|
||||
class="form"
|
||||
:session="session"
|
||||
@denied="state = 'denied'"
|
||||
@accepted="accepted"
|
||||
/>
|
||||
<div v-if="state == 'denied'">
|
||||
<h1>{{ i18n.ts._auth.denied }}</h1>
|
||||
</div>
|
||||
<div v-if="state == 'accepted' && session">
|
||||
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : i18n.ts.allowed }}</h1>
|
||||
<p v-if="session.app.callbackUrl">{{ i18n.ts._auth.callback }}
|
||||
<MkEllipsis />
|
||||
</p>
|
||||
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
||||
<MkSignin @login="onLogin" />
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import XForm from './auth.form.vue';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import * as os from '@/os';
|
||||
import { login } from '@/account';
|
||||
import { $i, login } from '@/account';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { AuthSession } from 'misskey-js/built/entities';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XForm,
|
||||
MkSignin,
|
||||
},
|
||||
props: ['token'],
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
session: null,
|
||||
fetching: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$i) return;
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
}>();
|
||||
|
||||
// Fetch session
|
||||
os.api('auth/session/show', {
|
||||
token: this.token,
|
||||
}).then(session => {
|
||||
this.session = session;
|
||||
this.fetching = false;
|
||||
let state = $ref<'waiting' | 'accepted' | 'fetch-session-error' | 'denied' | null>(null);
|
||||
let session = $ref<AuthSession | null>(null);
|
||||
|
||||
// 既に連携していた場合
|
||||
if (this.session.app.isAuthorized) {
|
||||
os.api('auth/accept', {
|
||||
token: this.session.token,
|
||||
}).then(() => {
|
||||
this.accepted();
|
||||
});
|
||||
} else {
|
||||
this.state = 'waiting';
|
||||
}
|
||||
}).catch(error => {
|
||||
this.state = 'fetch-session-error';
|
||||
this.fetching = false;
|
||||
function accepted() {
|
||||
state = 'accepted';
|
||||
if (session && session.app.callbackUrl) {
|
||||
const url = new URL(session.app.callbackUrl);
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
location.href = `${session.app.callbackUrl}?token=${session.token}`;
|
||||
}
|
||||
}
|
||||
|
||||
function onLogin(res) {
|
||||
login(res.i);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!$i) return;
|
||||
|
||||
try {
|
||||
session = await os.api('auth/session/show', {
|
||||
token: props.token,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
accepted() {
|
||||
this.state = 'accepted';
|
||||
if (this.session.app.callbackUrl) {
|
||||
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
|
||||
}
|
||||
}, onLogin(res) {
|
||||
login(res.i);
|
||||
},
|
||||
},
|
||||
|
||||
// 既に連携していた場合
|
||||
if (session.app.isAuthorized) {
|
||||
await os.api('auth/accept', {
|
||||
token: session.token,
|
||||
});
|
||||
accepted();
|
||||
} else {
|
||||
state = 'waiting';
|
||||
}
|
||||
} catch (e) {
|
||||
state = 'fetch-session-error';
|
||||
}
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts._auth.shareAccessTitle,
|
||||
icon: 'ti ti-apps',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
<style lang="scss" module>
|
||||
.loginMessage {
|
||||
text-align: center;
|
||||
margin: 8px 0 24px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _margin" fixed/>
|
||||
<MkPostForm v-if="$i" :channel="channel" class="post-form _panel _margin" fixed/>
|
||||
|
||||
<XTimeline :key="channelId" class="_margin" src="channel" :channel="channelId" @before="before" @after="after"/>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import XTimeline from '@/components/MkTimeline.vue';
|
||||
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||
import * as os from '@/os';
|
||||
|
@@ -1,41 +1,40 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-if="$i">
|
||||
<div v-if="state == 'waiting'" class="waiting">
|
||||
<div class="">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs" /></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-if="$i">
|
||||
<div v-if="state == 'waiting'">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="state == 'denied'" class="denied">
|
||||
<div class="">
|
||||
<div v-if="state == 'denied'">
|
||||
<p>{{ i18n.ts._auth.denied }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="state == 'accepted'" class="accepted">
|
||||
<div class="">
|
||||
<div v-else-if="state == 'accepted'" class="accepted">
|
||||
<p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="">
|
||||
<div v-if="name" class="">{{ $t('_auth.shareAccess', { name: name }) }}</div>
|
||||
<div v-else class="">{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<div class="">
|
||||
<p>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||
<ul>
|
||||
<li v-for="p in _permissions" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="">
|
||||
<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
|
||||
<div v-else>
|
||||
<div v-if="_permissions.length > 0">
|
||||
<p v-if="name">{{ $t('_auth.permission', { name }) }}</p>
|
||||
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||
<ul>
|
||||
<li v-for="p in _permissions" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="name">{{ $t('_auth.shareAccess', { name }) }}</div>
|
||||
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="signin">
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div v-else>
|
||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -45,6 +44,7 @@ import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i, login } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = defineProps<{
|
||||
session: string;
|
||||
@@ -54,7 +54,7 @@ const props = defineProps<{
|
||||
permission: string; // コンマ区切り
|
||||
}>();
|
||||
|
||||
const _permissions = props.permission.split(',');
|
||||
const _permissions = props.permission ? props.permission.split(',') : [];
|
||||
|
||||
let state = $ref<string | null>(null);
|
||||
|
||||
@@ -70,7 +70,7 @@ async function accept(): Promise<void> {
|
||||
state = 'accepted';
|
||||
if (props.callback) {
|
||||
const cbUrl = new URL(props.callback);
|
||||
if (!['http:', 'https:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||
cbUrl.searchParams.set('session', props.session);
|
||||
location.href = cbUrl.href;
|
||||
}
|
||||
@@ -83,8 +83,27 @@ function deny(): void {
|
||||
function onLogin(res): void {
|
||||
login(res.i);
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: 'MiAuth',
|
||||
icon: 'ti ti-apps',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss" module>
|
||||
.buttons {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.loginMessage {
|
||||
text-align: center;
|
||||
margin: 8px 0 24px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -150,6 +150,8 @@ function changeAvatar(ev) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('cropImageAsk'),
|
||||
okText: i18n.ts.cropYes,
|
||||
cancelText: i18n.ts.cropNo,
|
||||
});
|
||||
|
||||
if (!canceled) {
|
||||
@@ -174,6 +176,8 @@ function changeBanner(ev) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('cropImageAsk'),
|
||||
okText: i18n.ts.cropYes,
|
||||
cancelText: i18n.ts.cropNo,
|
||||
});
|
||||
|
||||
if (!canceled) {
|
||||
|
@@ -91,7 +91,7 @@ function regenerateToken() {
|
||||
type: 'password',
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/regenerate_token', {
|
||||
os.api('i/regenerate-token', {
|
||||
password: password,
|
||||
});
|
||||
});
|
||||
|
@@ -29,7 +29,7 @@ import { noteVisibilities } from 'misskey-js';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
@@ -69,14 +69,14 @@ async function init() {
|
||||
...(visibleAccts ? visibleAccts.split(',').map(Acct.parse) : []),
|
||||
]
|
||||
// TypeScriptの指示通りに変換する
|
||||
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
|
||||
.map(q => os.api('users/show', q)
|
||||
.then(user => {
|
||||
visibleUsers.push(user);
|
||||
}, () => {
|
||||
console.error(`Invalid user query: ${JSON.stringify(q)}`);
|
||||
}),
|
||||
),
|
||||
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
|
||||
.map(q => os.api('users/show', q)
|
||||
.then(user => {
|
||||
visibleUsers.push(user);
|
||||
}, () => {
|
||||
console.error(`Invalid user query: ${JSON.stringify(q)}`);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,13 +120,13 @@ async function init() {
|
||||
if (fileIds) {
|
||||
await Promise.all(
|
||||
fileIds.split(',')
|
||||
.map(fileId => os.api('drive/files/show', { fileId })
|
||||
.then(file => {
|
||||
files.push(file);
|
||||
}, () => {
|
||||
console.error(`Failed to fetch a file ${fileId}`);
|
||||
}),
|
||||
),
|
||||
.map(fileId => os.api('drive/files/show', { fileId })
|
||||
.then(file => {
|
||||
files.push(file);
|
||||
}, () => {
|
||||
console.error(`Failed to fetch a file ${fileId}`);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<MkSpacer :content-max="800">
|
||||
<div ref="rootEl" v-hotkey.global="keymap">
|
||||
<XTutorial v-if="$i && $store.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
|
||||
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
|
||||
<MkPostForm v-if="$store.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
|
||||
|
||||
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||
<div :class="$style.tl">
|
||||
@@ -24,7 +24,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, computed, watch } from 'vue';
|
||||
import XTimeline from '@/components/MkTimeline.vue';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import { scroll } from '@/scripts/scroll';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
|
@@ -168,7 +168,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
},
|
||||
disableShowingAnimatedImages: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
default: matchMedia('(prefers-reduced-motion)').matches,
|
||||
},
|
||||
emojiStyle: {
|
||||
where: 'device',
|
||||
|
@@ -127,13 +127,11 @@ hr {
|
||||
}
|
||||
|
||||
.ti {
|
||||
vertical-align: -40%;
|
||||
vertical-align: -12%;
|
||||
line-height: 1em;
|
||||
|
||||
&:before {
|
||||
display: inline-block;
|
||||
font-size: 165%;
|
||||
width: 0.74em;
|
||||
font-size: 128%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +154,13 @@ hr {
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
._nowrap {
|
||||
white-space: pre !important;
|
||||
word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
._ghost {
|
||||
@extend ._noSelect;
|
||||
pointer-events: none;
|
||||
|
@@ -35,7 +35,7 @@
|
||||
<i class="icon ti ti-pencil ti-fw"></i><span class="text">{{ i18n.ts.note }}</span>
|
||||
</button>
|
||||
<button v-click-anime class="item _button account" @click="openAccountMenu">
|
||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text _nowrap" :user="$i"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,20 +168,25 @@ function more() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 30px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
margin-top: 16px;
|
||||
|
||||
> .avatar {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
width: 32px;
|
||||
aspect-ratio: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .text {
|
||||
display: block;
|
||||
flex-shrink: 1;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -45,7 +45,7 @@
|
||||
<i class="icon ti ti-pencil ti-fw"></i><span class="text">{{ i18n.ts.note }}</span>
|
||||
</button>
|
||||
<button v-click-anime v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="item _button account" @click="openAccountMenu">
|
||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text _nowrap" :user="$i"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -217,20 +217,25 @@ function more(ev: MouseEvent) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 30px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
margin-top: 16px;
|
||||
|
||||
> .avatar {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
width: 32px;
|
||||
aspect-ratio: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .text {
|
||||
display: block;
|
||||
flex-shrink: 1;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -146,6 +146,7 @@ const addColumn = async (ev) => {
|
||||
'tl',
|
||||
'antenna',
|
||||
'list',
|
||||
'channel',
|
||||
'mentions',
|
||||
'direct',
|
||||
];
|
||||
|
71
packages/frontend/src/ui/deck/channel-column.vue
Normal file
71
packages/frontend/src/ui/deck/channel-column.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.channelId">
|
||||
<div style="padding: 8px; text-align: center;">
|
||||
<MkButton primary gradate rounded inline @click="post"><i class="ti ti-pencil"></i></MkButton>
|
||||
</div>
|
||||
<XTimeline ref="timeline" src="channel" :channel="column.channelId" @after="() => emit('loaded')"/>
|
||||
</template>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn, Column } from './deck-store';
|
||||
import XTimeline from '@/components/MkTimeline.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'loaded'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let timeline = $shallowRef<InstanceType<typeof XTimeline>>();
|
||||
|
||||
if (props.column.channelId == null) {
|
||||
setChannel();
|
||||
}
|
||||
|
||||
async function setChannel() {
|
||||
const channels = await os.api('channels/followed');
|
||||
const { canceled, result: channel } = await os.select({
|
||||
title: i18n.ts.selectChannel,
|
||||
items: channels.map(x => ({
|
||||
value: x, text: x.name,
|
||||
})),
|
||||
default: props.column.channelId,
|
||||
});
|
||||
if (canceled) return;
|
||||
updateColumn(props.column.id, {
|
||||
channelId: channel.id,
|
||||
name: channel.name,
|
||||
});
|
||||
}
|
||||
|
||||
function post() {
|
||||
os.post({
|
||||
channel: {
|
||||
id: props.column.channelId,
|
||||
},
|
||||
instant: true,
|
||||
});
|
||||
}
|
||||
|
||||
const menu = [{
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.selectChannel,
|
||||
action: setChannel,
|
||||
}];
|
||||
</script>
|
@@ -6,6 +6,7 @@
|
||||
<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XChannelColumn v-else-if="column.type === 'channel'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
@@ -17,6 +18,7 @@ import XMainColumn from './main-column.vue';
|
||||
import XTlColumn from './tl-column.vue';
|
||||
import XAntennaColumn from './antenna-column.vue';
|
||||
import XListColumn from './list-column.vue';
|
||||
import XChannelColumn from './channel-column.vue';
|
||||
import XNotificationsColumn from './notifications-column.vue';
|
||||
import XWidgetsColumn from './widgets-column.vue';
|
||||
import XMentionsColumn from './mentions-column.vue';
|
||||
|
@@ -14,7 +14,7 @@ type ColumnWidget = {
|
||||
|
||||
export type Column = {
|
||||
id: string;
|
||||
type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'list' | 'mentions' | 'direct';
|
||||
type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'channel' | 'list' | 'mentions' | 'direct';
|
||||
name: string | null;
|
||||
width: number;
|
||||
widgets?: ColumnWidget[];
|
||||
@@ -22,6 +22,7 @@ export type Column = {
|
||||
flexible?: boolean;
|
||||
antennaId?: string;
|
||||
listId?: string;
|
||||
channelId?: string;
|
||||
includingTypes?: typeof notificationTypes[number][];
|
||||
tl?: 'home' | 'local' | 'social' | 'global';
|
||||
};
|
||||
|
@@ -142,10 +142,10 @@ mainRouter.on('change', () => {
|
||||
document.documentElement.style.overflowY = 'scroll';
|
||||
|
||||
if (window.innerWidth > 1024) {
|
||||
const tempUI = miLocalStorage.getItem('ui_temp')
|
||||
const tempUI = miLocalStorage.getItem('ui_temp');
|
||||
if (tempUI) {
|
||||
miLocalStorage.setItem('ui', tempUI)
|
||||
miLocalStorage.removeItem('ui_temp')
|
||||
miLocalStorage.setItem('ui', tempUI);
|
||||
miLocalStorage.removeItem('ui_temp');
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<XPostForm class="_panel mkw-postForm data-cy-mkw-postForm" :fixed="true" :autofocus="false"/>
|
||||
<MkPostForm class="_panel mkw-post-form data-cy-mkw-postForm" :fixed="true" :autofocus="false"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
|
||||
const name = 'postForm';
|
||||
|
||||
|
@@ -12,8 +12,8 @@
|
||||
"misskey-js": "0.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.61",
|
||||
"@typescript-eslint/parser": "5.51.0",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.62",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"typescript": "4.9.5"
|
||||
|
1098
pnpm-lock.yaml
generated
1098
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user