Compare commits
43 commits
l10n_devel
...
develop
Author | SHA1 | Date | |
---|---|---|---|
Mizah | a858ee31c6 | ||
Mizah | 0bd7ed8191 | ||
Mizah | ee574ae154 | ||
Mizah | a505f36252 | ||
Mizah | a516383c66 | ||
Mizah | 1613dafc39 | ||
Mizah | 8457fa9b3b | ||
Mizah | 2e51e779e7 | ||
Mizah | 7f6b486976 | ||
Mizah | 8c1508fae4 | ||
Mizah | aeb568664d | ||
Mizah | ecb990fb77 | ||
a11b77a415 | |||
1bc4f400c0 | |||
458c72c153 | |||
6bd3ed2074 | |||
31e5f0bd09 | |||
e0a83e9c9e | |||
1496700b37 | |||
00cbf9fe80 | |||
cf09aa21f0 | |||
9f7d41eb47 | |||
4a62051ce7 | |||
3a421837bf | |||
a4c5ce1413 | |||
e75b62f3f5 | |||
5b60ae810b | |||
98b4717c45 | |||
8a4ce16e90 | |||
794cb9ffe2 | |||
0b976064ca | |||
bca690f256 | |||
f1eb17f66c | |||
b1c82213a3 | |||
a896c39dbf | |||
6718a54f6f | |||
d57b8bf2e2 | |||
224bbd486f | |||
724dea8136 | |||
ceb60d61b0 | |||
17d9aca5a7 | |||
7fc8a2a7b0 | |||
a96f09cee3 |
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,8 +1,10 @@
|
|||
## 2024.10.2
|
||||
## 2024.11.0
|
||||
|
||||
### General
|
||||
- Feat: コンテンツの表示にログインを必須にできるように
|
||||
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
|
||||
- Enhance: 依存関係の更新
|
||||
- Enhance: l10nの更新
|
||||
|
||||
### Client
|
||||
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
|
||||
|
@ -16,21 +18,34 @@
|
|||
- 認証するアカウントを切り替えられるように
|
||||
- Enhance: Self-XSS防止用の警告を追加
|
||||
- Enhance: カタルーニャ語 (ca-ES) に対応
|
||||
- Enhance: 個別お知らせページではMetaタグを出力するように
|
||||
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
||||
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
||||
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
|
||||
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
|
||||
- Fix: リンク切れを修正
|
||||
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
|
||||
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
|
||||
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
|
||||
|
||||
### Server
|
||||
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
|
||||
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
|
||||
- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように
|
||||
- Fix: フォロワーへのメッセージの絵文字をemojisに含めるように
|
||||
- Fix: Nested proxy requestsを検出した際にブロックするように
|
||||
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
|
||||
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
|
||||
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
|
||||
- Fix: ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
|
||||
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
||||
- Fix: User Webhookテスト機能のMock Payloadを修正
|
||||
|
||||
### Misskey.js
|
||||
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
||||
|
|
|
@ -83,6 +83,10 @@ One should not add property that has defined before by other implementation, or
|
|||
## Reviewers guide
|
||||
Be willing to comment on the good points and not just the things you want fixed 💯
|
||||
|
||||
読んでおくといいやつ
|
||||
- https://blog.lacolaco.net/posts/1e2cf439b3c2/
|
||||
- https://konifar-zatsu.hatenadiary.jp/entry/2024/11/05/192421
|
||||
|
||||
### Review perspective
|
||||
- Scope
|
||||
- Are the goals of the PR clear?
|
||||
|
|
|
@ -8,37 +8,37 @@ search: "Cercar"
|
|||
notifications: "Notificacions"
|
||||
username: "Nom d'usuari"
|
||||
password: "Contrasenya"
|
||||
initialPasswordForSetup: "Contrasenya inicial per fer la primera configuració "
|
||||
initialPasswordForSetup: "Contrasenya inicial per la configuració inicial"
|
||||
initialPasswordIsIncorrect: "La contrasenya no és correcta."
|
||||
initialPasswordForSetupDescription: "Fes servir la contrasenya que has fet servir al fitxer de configuració, si tu mateix has instal·lat Misskey.\nSi fas servir una empresa d'allotjament de Misskey, fes servir la contrasenya que t'han donat.\nSi no has posat cap contrasenya deixar l'espai en blanc."
|
||||
forgotPassword: "Restableix la contrasenya "
|
||||
fetchingAsApObject: "Cercant al Fediverse..."
|
||||
forgotPassword: "Contrasenya oblidada"
|
||||
fetchingAsApObject: "Cercant en el Fediverse..."
|
||||
ok: "OK"
|
||||
gotIt: "D'acord "
|
||||
gotIt: "Ho he entès!"
|
||||
cancel: "Cancel·lar"
|
||||
noThankYou: "No, gràcies"
|
||||
enterUsername: "Introdueix el teu nom d'usuari"
|
||||
renotedBy: "Impulsat per {user}"
|
||||
noNotes: "Cap nota"
|
||||
noNotifications: "Cap notificació"
|
||||
instance: "Instància "
|
||||
instance: "Servidor"
|
||||
settings: "Preferències"
|
||||
notificationSettings: "Configurar les notificacions"
|
||||
notificationSettings: "Paràmetres de notificacions"
|
||||
basicSettings: "Configuració bàsica"
|
||||
otherSettings: "Altres configuracions"
|
||||
openInWindow: "Obrir en una finestra nova"
|
||||
otherSettings: "Configuració avançada"
|
||||
openInWindow: "Obrir en una nova finestra"
|
||||
profile: "Perfil"
|
||||
timeline: "Línia de temps"
|
||||
noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia."
|
||||
login: "Iniciar sessió"
|
||||
loggingIn: "Iniciar la sessió "
|
||||
loggingIn: "Identificant-se"
|
||||
logout: "Tancar la sessió"
|
||||
signup: "Registrar-se"
|
||||
uploading: "Pujant..."
|
||||
save: "Desa"
|
||||
users: "Usuaris"
|
||||
addUser: "Afegir un usuari"
|
||||
favorite: "Afegeix als preferits"
|
||||
favorite: "Afegir a preferits"
|
||||
favorites: "Favorits"
|
||||
unfavorite: "Eliminar dels preferits"
|
||||
favorited: "Afegit als preferits."
|
||||
|
@ -50,26 +50,26 @@ copyContent: "Copiar el contingut"
|
|||
copyLink: "Copiar l'enllaç"
|
||||
copyLinkRenote: "Copiar l'enllaç de la renota"
|
||||
delete: "Elimina"
|
||||
deleteAndEdit: "Eliminar i editar"
|
||||
deleteAndEdit: "Elimina i edita"
|
||||
deleteAndEditConfirm: "Segur que vols eliminar aquesta publicació i editar-la? Perdràs totes les reaccions, impulsos i respostes."
|
||||
addToList: "Afegir a una llista"
|
||||
addToAntenna: "Afegir a una antena"
|
||||
addToAntenna: "Afegir a l'antena"
|
||||
sendMessage: "Enviar un missatge"
|
||||
copyRSS: "Copiar RSS"
|
||||
copyUsername: "Copiar nom d'usuari"
|
||||
copyUserId: "Copiar ID d'usuari"
|
||||
copyNoteId: "Copiar ID de la nota"
|
||||
copyFileId: "Copiar ID de l'arxiu"
|
||||
copyFolderId: "Copiar ID de la carpeta"
|
||||
copyProfileUrl: "Copiar adreça URL del perfil"
|
||||
copyNoteId: "Copiar ID de nota"
|
||||
copyFileId: "Copiar ID d'arxiu"
|
||||
copyFolderId: "Copiar ID de carpeta"
|
||||
copyProfileUrl: "Copiar URL del perfil"
|
||||
searchUser: "Cercar un usuari"
|
||||
searchThisUsersNotes: "Cercar les publicacions de l'usuari"
|
||||
reply: "Respon"
|
||||
searchThisUsersNotes: "Cerca les publicacions de l'usuari"
|
||||
reply: "Respondre"
|
||||
loadMore: "Carregar més"
|
||||
showMore: "Veure més"
|
||||
showLess: "Mostrar menys"
|
||||
showLess: "Mostra menys"
|
||||
youGotNewFollower: "t'ha seguit"
|
||||
receiveFollowRequest: "Has rebut una sol·licitud de seguiment"
|
||||
receiveFollowRequest: "Sol·licitud de seguiment rebuda"
|
||||
followRequestAccepted: "Sol·licitud de seguiment acceptada"
|
||||
mention: "Menció"
|
||||
mentions: "Mencions"
|
||||
|
@ -78,25 +78,25 @@ importAndExport: "Importar / Exportar"
|
|||
import: "Importar"
|
||||
export: "Exporta"
|
||||
files: "Fitxers"
|
||||
download: "Descarregar"
|
||||
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer també seran esborrades."
|
||||
unfollowConfirm: "Segur que vols deixar de seguir a {name}?"
|
||||
exportRequested: "Has sol·licitat una exportació de dades. Això pot trigar una estona. S'afegirà a la teva unitat de disc un cop estigui completada."
|
||||
importRequested: "Has sol·licitat una importació de dades. Això pot trigar una estona."
|
||||
download: "Baixar"
|
||||
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer adjunt també se suprimiran."
|
||||
unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?"
|
||||
exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà a la teva unitat un cop completat."
|
||||
importRequested: "Has sol·licitat una importació. Això pot trigar una estona."
|
||||
lists: "Llistes"
|
||||
noLists: "No tens cap llista"
|
||||
note: "Nota"
|
||||
notes: "Notes"
|
||||
following: "Segueixes "
|
||||
following: "Seguint"
|
||||
followers: "Seguidors"
|
||||
followsYou: "Et segueix"
|
||||
createList: "Crear llista"
|
||||
manageLists: "Gestionar les llistes"
|
||||
error: "Error"
|
||||
somethingHappened: "S'ha produït un error"
|
||||
retry: "Torna-ho a provar"
|
||||
retry: "Torna-ho a intentar"
|
||||
pageLoadError: "S'ha produït un error en carregar la pàgina"
|
||||
pageLoadErrorDescription: "Això normalment és a causa d'errors a la xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar un temps."
|
||||
pageLoadErrorDescription: "Això normalment es deu a errors de xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar una estona."
|
||||
serverIsDead: "Aquest servidor no respon. Espera una estona i torna-ho a provar."
|
||||
youShouldUpgradeClient: "Per veure aquesta pàgina, actualitzeu-la per actualitzar el vostre client."
|
||||
enterListName: "Introdueix un nom per a la llista"
|
||||
|
@ -104,52 +104,52 @@ privacy: "Privadesa"
|
|||
makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació"
|
||||
defaultNoteVisibility: "Visibilitat per defecte"
|
||||
follow: "Seguint"
|
||||
followRequest: "Enviar sol·licitud de seguiment"
|
||||
followRequest: "Enviar la sol·licitud de seguiment"
|
||||
followRequests: "Sol·licituds de seguiment"
|
||||
unfollow: "Deixar de seguir"
|
||||
followRequestPending: "Sol·licituds de seguiment pendents"
|
||||
enterEmoji: "Introduir un emoji"
|
||||
renote: "Impulsar "
|
||||
renote: "Impulsa"
|
||||
unrenote: "Anul·la l'impuls"
|
||||
renoted: "S'ha impulsat"
|
||||
renotedToX: "Impulsat per {name}."
|
||||
cantRenote: "No es pot impulsar aquesta publicació"
|
||||
cantReRenote: "No es pot impulsar un impuls."
|
||||
cantReRenote: "No es pot impulsar l'impuls."
|
||||
quote: "Cita"
|
||||
inChannelRenote: "Impulsar només a un canal"
|
||||
inChannelQuote: "Citar només a un canal"
|
||||
renoteToChannel: "Impulsar a un canal"
|
||||
renoteToOtherChannel: "Impulsar a un altre canal"
|
||||
inChannelRenote: "Renotar només al Canal"
|
||||
inChannelQuote: "Citar només al Canal"
|
||||
renoteToChannel: "Impulsa a un canal"
|
||||
renoteToOtherChannel: "Impulsa a un altre canal"
|
||||
pinnedNote: "Nota fixada"
|
||||
pinned: "Fixar al perfil"
|
||||
you: "Tu"
|
||||
clickToShow: "Fes clic per mostrar"
|
||||
sensitive: "Sensible"
|
||||
sensitive: "NSFW"
|
||||
add: "Afegir"
|
||||
reaction: "Reacció "
|
||||
reaction: "Reaccions"
|
||||
reactions: "Reaccions"
|
||||
emojiPicker: "Selector d'emojis"
|
||||
pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb qui vols reaccionar"
|
||||
pinnedEmojisSettingDescription: "Selecciona quins emojis vols deixar fixats i es mostrin en obrir el selector d'emojis"
|
||||
emojiPickerDisplay: "Mostrar el selector d'emojis"
|
||||
emojiPicker: "Selecció d'emojis"
|
||||
pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
|
||||
pinnedEmojisSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
|
||||
emojiPickerDisplay: "Visualitza el selector d'emojis"
|
||||
overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció"
|
||||
overwriteFromPinnedEmojis: "Sobreescriu els emojis fixats al panel de reaccions"
|
||||
overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats"
|
||||
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
|
||||
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
|
||||
attachCancel: "Eliminar el fitxer adjunt"
|
||||
deleteFile: "Esborrar l'arxiu "
|
||||
markAsSensitive: "Marcar com a sensible"
|
||||
markAsSensitive: "Marcar com a NSFW"
|
||||
unmarkAsSensitive: "Deixar de marcar com a sensible"
|
||||
enterFileName: "Defineix nom del fitxer"
|
||||
mute: "Silencia"
|
||||
unmute: "Deixa de silenciar"
|
||||
renoteMute: "Silenciar impulsos"
|
||||
renoteUnmute: "Treure el silenci dels impulsos"
|
||||
renoteMute: "Silenciar Renotes"
|
||||
renoteUnmute: "Treure el silenci de les renotes"
|
||||
block: "Bloqueja"
|
||||
unblock: "Desbloqueja"
|
||||
suspend: "Suspèn"
|
||||
unsuspend: "Deixa de suspendre"
|
||||
blockConfirm: "Vols bloquejar-lo?"
|
||||
blockConfirm: "Vols bloquejar?"
|
||||
unblockConfirm: "Vols desbloquejar-lo?"
|
||||
suspendConfirm: "Estàs segur que vols suspendre aquest compte?"
|
||||
unsuspendConfirm: "Estàs segur que vols treure la suspensió d'aquest compte?"
|
||||
|
@ -175,11 +175,11 @@ youCanCleanRemoteFilesCache: "Pots netejar la memòria cau fent clic al botó de
|
|||
cacheRemoteSensitiveFiles: "Posar a la memòria cau arxius remots sensibles"
|
||||
cacheRemoteSensitiveFilesDescription: "Quan aquesta opció és desactiva, els arxius remots sensibles es carregant directament del servidor d'origen sense que es guardin a la memòria cau."
|
||||
flagAsBot: "Marca aquest compte com a bot"
|
||||
flagAsBotDescription: "Activa aquesta opció si el compte el controla un programa. Si s'activa, actuarà com un senyal per altres desenvolupadors per prevenir cadenes d'interacció sense fi i ajustar els paràmetres interns de Misskey pe tractar el compte com un bot."
|
||||
flagAsBotDescription: "Marca aquest compte com a bot"
|
||||
flagAsCat: "Marca aquest compte com a gat"
|
||||
flagAsCatDescription: "Activeu aquesta opció per marcar aquest compte com a gat."
|
||||
flagShowTimelineReplies: "Mostra les respostes a la línia de temps"
|
||||
flagShowTimelineRepliesDescription: "Mostra les respostes dels usuaris a les notes d'altres usuaris a la línia de temps."
|
||||
flagShowTimelineRepliesDescription: "Mostra les respostes a la línia de temps"
|
||||
autoAcceptFollowed: "Aprova automàticament les sol·licituds de seguiment dels usuaris que segueixes"
|
||||
addAccount: "Afegeix un compte"
|
||||
reloadAccountsList: "Recarregar la llista de contactes"
|
||||
|
@ -204,7 +204,7 @@ selectUser: "Selecciona usuari/a"
|
|||
recipient: "Destinatari"
|
||||
annotation: "Comentaris"
|
||||
federation: "Federació"
|
||||
instances: "Instàncies "
|
||||
instances: "Servidors"
|
||||
registeredAt: "Registrat a"
|
||||
latestRequestReceivedAt: "Última petició rebuda"
|
||||
latestStatus: "Últim estat"
|
||||
|
@ -213,7 +213,7 @@ charts: "Gràfics"
|
|||
perHour: "Per hora"
|
||||
perDay: "Per dia"
|
||||
stopActivityDelivery: "Deixa d'enviar activitats"
|
||||
blockThisInstance: "Bloca aquesta instància "
|
||||
blockThisInstance: "Deixa d'enviar activitats"
|
||||
silenceThisInstance: "Silencia aquesta instància "
|
||||
mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instància "
|
||||
operations: "Accions"
|
||||
|
@ -228,7 +228,7 @@ network: "Xarxa"
|
|||
disk: "Disc"
|
||||
instanceInfo: "Informació del fitxer d'instal·lació"
|
||||
statistics: "Estadístiques"
|
||||
clearQueue: "Esborra la cua de feina"
|
||||
clearQueue: "Esborrar la cua"
|
||||
clearQueueConfirmTitle: "Esteu segur que voleu esborrar la cua?"
|
||||
clearQueueConfirmText: "Les notes no lliurades que quedin a la cua no es federaran. Normalment aquesta operació no és necessària."
|
||||
clearCachedFiles: "Esborra la memòria cau"
|
||||
|
@ -254,7 +254,7 @@ processing: "S'està processant..."
|
|||
preview: "Vista prèvia"
|
||||
default: "Per defecte"
|
||||
defaultValueIs: "Per defecte: {value}"
|
||||
noCustomEmojis: "No hi ha emojis personalitzats"
|
||||
noCustomEmojis: "Cap emoji personalitzat"
|
||||
noJobs: "No hi ha feines"
|
||||
federating: "Federant"
|
||||
blocked: "Bloquejat"
|
||||
|
@ -268,11 +268,11 @@ instanceFollowers: "Seguidors del servidor"
|
|||
instanceUsers: "Usuaris del servidor"
|
||||
changePassword: "Canvia la contrasenya"
|
||||
security: "Seguretat"
|
||||
retypedNotMatch: "Les entrades no coincideix"
|
||||
retypedNotMatch: "L'entrada no coincideix"
|
||||
currentPassword: "Contrasenya actual"
|
||||
newPassword: "Contrasenya nova"
|
||||
newPasswordRetype: "Contrasenya nova (repeteix-la)"
|
||||
attachFile: "Afegeix un arxiu"
|
||||
newPasswordRetype: "Contrasenya nou (repeteix-la)"
|
||||
attachFile: "Adjunta fitxers"
|
||||
more: "Més"
|
||||
featured: "Destacat"
|
||||
usernameOrUserId: "Nom o ID d'usuari"
|
||||
|
@ -282,25 +282,25 @@ announcements: "Anuncis"
|
|||
imageUrl: "URL de la imatge"
|
||||
remove: "Eliminar"
|
||||
removed: "Eliminat"
|
||||
removeAreYouSure: "Segur que vols esborrar «{x}»?"
|
||||
deleteAreYouSure: "Segur que vols esborrar «{x}»?"
|
||||
resetAreYouSure: "Segur que vols restablir-ho?"
|
||||
areYouSure: "Estàs segur?"
|
||||
removeAreYouSure: "Segur que voleu retirar «{x}»?"
|
||||
deleteAreYouSure: "Segur que voleu retirar «{x}»?"
|
||||
resetAreYouSure: "Segur que voleu restablir-ho?"
|
||||
areYouSure: "Està segur?"
|
||||
saved: "S'ha desat"
|
||||
messaging: "Xat"
|
||||
upload: "Puja"
|
||||
keepOriginalUploading: "Guarda la imatge original"
|
||||
keepOriginalUploadingDescription: "Guarda la imatge pujada sense modificar. Si està desactivat, es generarà una versió per visualitzar a la web en pujar la imatge."
|
||||
fromDrive: "Des del Disc"
|
||||
keepOriginalUploadingDescription: "Guarda la imatge pujada com hi és. Si està apagat, una versió per a la visualització a la xarxa serà generada quan sigui pujada."
|
||||
fromDrive: "Des de la unitat"
|
||||
fromUrl: "Des d'un enllaç"
|
||||
uploadFromUrl: "Carrega des d'un enllaç"
|
||||
uploadFromUrlDescription: "Enllaç del fitxer que vols carregar"
|
||||
uploadFromUrlRequested: "Càrrega sol·licitada"
|
||||
uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot trigar un temps"
|
||||
uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot prendre un temps"
|
||||
explore: "Explora"
|
||||
messageRead: "Vist"
|
||||
noMoreHistory: "No hi ha res més per veure"
|
||||
startMessaging: "Comença a xatejar"
|
||||
noMoreHistory: "No hi resta més per veure"
|
||||
startMessaging: "Començar a xatejar"
|
||||
nUsersRead: "Vist per {n}"
|
||||
agreeTo: "Accepto que {0}"
|
||||
agree: "Hi estic d'acord"
|
||||
|
@ -312,7 +312,7 @@ home: "Inici"
|
|||
remoteUserCaution: "Ja que aquest usuari resideix a una instància remota, la informació mostrada es podria trobar incompleta."
|
||||
activity: "Activitat"
|
||||
images: "Imatges"
|
||||
image: "Imatge"
|
||||
image: "Imatges"
|
||||
birthday: "Aniversari"
|
||||
yearsOld: "{age} anys"
|
||||
registeredDate: "Data de registre"
|
||||
|
@ -327,10 +327,10 @@ darkThemes: "Temes foscos"
|
|||
syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu"
|
||||
drive: "Unitat"
|
||||
fileName: "Nom del Fitxer"
|
||||
selectFile: "Selecciona un fitxer"
|
||||
selectFile: "Selecciona fitxers"
|
||||
selectFiles: "Selecciona fitxers"
|
||||
selectFolder: "Selecció de carpeta"
|
||||
selectFolders: "Selecció de carpetes"
|
||||
selectFolders: "Selecció de carpeta"
|
||||
fileNotSelected: "Cap fitxer seleccionat"
|
||||
renameFile: "Canvia el nom del fitxer"
|
||||
folderName: "Nom de la carpeta"
|
||||
|
@ -359,9 +359,9 @@ reload: "Actualitza"
|
|||
doNothing: "Ignora"
|
||||
reloadConfirm: "Vols recarregar?"
|
||||
watch: "Veure"
|
||||
unwatch: "Deixa de veure"
|
||||
unwatch: "Deixar de veure"
|
||||
accept: "Acceptar"
|
||||
reject: "Denega"
|
||||
reject: "Denegar"
|
||||
normal: "Normal"
|
||||
instanceName: "Nom del servidor"
|
||||
instanceDescription: "Descripció del servidor"
|
||||
|
@ -382,7 +382,7 @@ enableLocalTimeline: "Activa la línia de temps local"
|
|||
enableGlobalTimeline: "Activa la línia de temps global"
|
||||
disablingTimelinesInfo: "Fins i tot si aquestes línies de temps són desactivades, els administradors i els moderadors poden continuar visualitzant per conveniència."
|
||||
registration: "Registre"
|
||||
enableRegistration: "Permet el registre de nous usuaris"
|
||||
enableRegistration: "Permet els registres d'usuaris"
|
||||
invite: "Convida"
|
||||
driveCapacityPerLocalAccount: "Capacitat del disc per usuaris locals"
|
||||
driveCapacityPerRemoteAccount: "Capacitat del disc per usuaris remots"
|
||||
|
@ -393,20 +393,20 @@ basicInfo: "Informació bàsica"
|
|||
pinnedUsers: "Usuaris fixats"
|
||||
pinnedUsersDescription: "Llista d'usuaris, separats per salts de línia, que seran fixats a la pestanya \"Explorar\"."
|
||||
pinnedPages: "Pàgines fixades"
|
||||
pinnedPagesDescription: "Escriu les adreces de les pàgines que vols fixar a la pàgina d'inici d'aquesta instància. Separades per salts de línia."
|
||||
pinnedPagesDescription: "Escriu els camins de les pàgines que vols fixar a la pàgina d'inici d'aquesta instància. Separades per salts de línia."
|
||||
pinnedClipId: "ID del retall fixat"
|
||||
pinnedNotes: "Nota fixada"
|
||||
hcaptcha: "hCaptcha"
|
||||
enableHcaptcha: "Activa hCaptcha"
|
||||
enableHcaptcha: "Activar hCaptcha"
|
||||
hcaptchaSiteKey: "Clau del lloc"
|
||||
hcaptchaSecretKey: "Clau secreta"
|
||||
mcaptcha: "mCaptcha"
|
||||
enableMcaptcha: "Activa mCaptcha"
|
||||
enableMcaptcha: "Activar mCaptcha"
|
||||
mcaptchaSiteKey: "Clau del lloc"
|
||||
mcaptchaSecretKey: "Clau secreta"
|
||||
mcaptchaInstanceUrl: "Adreça URL del servidor mCaptcha"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "Activa reCAPTCHA"
|
||||
enableRecaptcha: "Activar reCAPTCHA"
|
||||
recaptchaSiteKey: "Clau del lloc"
|
||||
recaptchaSecretKey: "Clau secreta"
|
||||
turnstile: "Turnstile"
|
||||
|
@ -448,14 +448,14 @@ aboutMisskey: "Quant a Misskey"
|
|||
administrator: "Administrador/a"
|
||||
token: "Codi de verificació"
|
||||
2fa: "Autenticació de doble factor"
|
||||
setupOf2fa: "Configura l'autenticació de doble factor"
|
||||
setupOf2fa: "Configurar l'autenticació de doble factor"
|
||||
totp: "Aplicació d'autenticació"
|
||||
totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'autenticació"
|
||||
moderator: "Moderador/a"
|
||||
moderation: "Moderació"
|
||||
moderationNote: "Nota de moderació "
|
||||
moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors."
|
||||
addModerationNote: "Afegeix una nota de moderació "
|
||||
addModerationNote: "Afegir una nota de moderació "
|
||||
moderationLogs: "Registre de moderació "
|
||||
nUsersMentioned: "{n} usuaris mencionats"
|
||||
securityKeyAndPasskey: "Clau de seguretat / Clau de pas"
|
||||
|
@ -471,13 +471,13 @@ reduceUiAnimation: "Redueix les animacions de la interfície"
|
|||
share: "Comparteix"
|
||||
notFound: "No s'ha trobat"
|
||||
notFoundDescription: "No es troba cap pàgina que correspongui a aquesta adreça"
|
||||
uploadFolder: "Carpeta per defecte on desar els arxius pujats"
|
||||
uploadFolder: "Carpeta per defecte per pujades"
|
||||
markAsReadAllNotifications: "Marca totes les notificacions com a llegides"
|
||||
markAsReadAllUnreadNotes: "Marca-ho tot com a llegit"
|
||||
markAsReadAllTalkMessages: "Marcar tots els missatges com llegits"
|
||||
help: "Ajuda"
|
||||
inputMessageHere: "Escriu aquí el teu missatge "
|
||||
close: "Tanca"
|
||||
close: "Tancar"
|
||||
invites: "Convida"
|
||||
members: "Membres"
|
||||
transfer: "Transferir"
|
||||
|
@ -508,7 +508,7 @@ normalPassword: "Bona contrasenya"
|
|||
strongPassword: "Contrasenya segura"
|
||||
passwordMatched: "Correcte!"
|
||||
passwordNotMatched: "No coincideix"
|
||||
signinWith: "Inicia sessió amb {x}"
|
||||
signinWith: "Inicia sessió amb amb {x}"
|
||||
signinFailed: "Autenticació sense èxit. Intenta-ho un altre cop utilitzant la contrasenya i el nom correctes."
|
||||
or: "O"
|
||||
language: "Idioma"
|
||||
|
@ -591,7 +591,7 @@ chooseEmoji: "Tria un emoji"
|
|||
unableToProcess: "L'operació no pot ser completada "
|
||||
recentUsed: "Utilitzat recentment"
|
||||
install: "Instal·lació "
|
||||
uninstall: "Desinstal·la"
|
||||
uninstall: "Desinstal·lar "
|
||||
installedApps: "Aplicacions autoritzades "
|
||||
nothing: "No hi ha res per veure aquí "
|
||||
installedDate: "Data d'instal·lació"
|
||||
|
@ -608,13 +608,13 @@ output: "Sortida"
|
|||
script: "Script"
|
||||
disablePagesScript: "Desactivar AiScript a les pàgines "
|
||||
updateRemoteUser: "Actualitzar la informació de l'usuari remot"
|
||||
unsetUserAvatar: "Desactiva l'avatar "
|
||||
unsetUserAvatar: "Desactivar l'avatar "
|
||||
unsetUserAvatarConfirm: "Segur que vols desactivar l'avatar?"
|
||||
unsetUserBanner: "Desactiva el bàner "
|
||||
unsetUserBanner: "Desactivar el bàner "
|
||||
unsetUserBannerConfirm: "Segur que vols desactivar el bàner?"
|
||||
deleteAllFiles: "Esborra tots els arxius"
|
||||
deleteAllFiles: "Esborrar tots els arxius"
|
||||
deleteAllFilesConfirm: "Segur que vols esborrar tots els arxius?"
|
||||
removeAllFollowing: "Deixa de seguir tots els usuaris seguits"
|
||||
removeAllFollowing: "Deixar de seguir tots els usuaris seguits"
|
||||
removeAllFollowingDescription: "El fet d'executar això, et farà deixar de seguir a tots els usuaris de {host}. Si us plau, executa això si l'amfitrió, per exemple, ja no existeix."
|
||||
userSuspended: "Aquest usuari ha sigut suspès"
|
||||
userSilenced: "Aquest usuari està sent silenciat"
|
||||
|
@ -1183,8 +1183,8 @@ currentAnnouncements: "Informes actuals"
|
|||
pastAnnouncements: "Informes passats"
|
||||
youHaveUnreadAnnouncements: "Tens informes per llegir."
|
||||
useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
|
||||
replies: "Respon"
|
||||
renotes: "Impulsar "
|
||||
replies: "Respondre"
|
||||
renotes: "Impulsa"
|
||||
loadReplies: "Mostrar les respostes"
|
||||
loadConversation: "Mostrar la conversació "
|
||||
pinnedList: "Llista fixada"
|
||||
|
@ -1299,7 +1299,6 @@ yourNameContainsProhibitedWordsDescription: "Si de veritat vols fer servir aques
|
|||
thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autor requereix l'inici de sessió per poder veure"
|
||||
lockdown: "Bloquejat"
|
||||
pleaseSelectAccount: "Seleccionar un compte"
|
||||
availableRoles: "Roles disponibles "
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut"
|
||||
requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació."
|
||||
|
|
|
@ -331,7 +331,6 @@ selectFile: "Select a file"
|
|||
selectFiles: "Select files"
|
||||
selectFolder: "Select a folder"
|
||||
selectFolders: "Select folders"
|
||||
fileNotSelected: "No file selected"
|
||||
renameFile: "Rename file"
|
||||
folderName: "Folder name"
|
||||
createFolder: "Create a folder"
|
||||
|
@ -1299,7 +1298,6 @@ yourNameContainsProhibitedWordsDescription: "If you wish to use this name, pleas
|
|||
thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view"
|
||||
lockdown: "Lockdown"
|
||||
pleaseSelectAccount: "Select an account"
|
||||
availableRoles: "Available roles"
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "Require sign-in to view contents"
|
||||
requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information."
|
||||
|
@ -2732,9 +2730,3 @@ _embedCodeGen:
|
|||
generateCode: "Generate embed code"
|
||||
codeGenerated: "The code has been generated"
|
||||
codeGeneratedDescription: "Paste the generated code into your website to embed the content."
|
||||
_selfXssPrevention:
|
||||
warning: "WARNING"
|
||||
title: "\"Paste something on this screen\" is all a scam."
|
||||
description1: "If you paste something here, a malicious user could hijack your account or steal your personal information."
|
||||
description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window."
|
||||
description3: "For more information, please refer to this. {link}"
|
||||
|
|
|
@ -42,7 +42,7 @@ favorite: "즐겨찾기"
|
|||
favorites: "즐겨찾기"
|
||||
unfavorite: "즐겨찾기에서 제거"
|
||||
favorited: "즐겨찾기에 등록했습니다."
|
||||
alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다."
|
||||
alreadyFavorited: "이미 즐겨찾기에 등록했습니다."
|
||||
cantFavorite: "즐겨찾기에 등록하지 못했습니다."
|
||||
pin: "프로필에 고정"
|
||||
unpin: "프로필에서 고정 해제"
|
||||
|
@ -947,9 +947,6 @@ oneHour: "1시간"
|
|||
oneDay: "1일"
|
||||
oneWeek: "일주일"
|
||||
oneMonth: "1개월"
|
||||
threeMonths: "3개월"
|
||||
oneYear: "1년"
|
||||
threeDays: "3일"
|
||||
reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다."
|
||||
failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다"
|
||||
rateLimitExceeded: "요청 제한 횟수를 초과하였습니다"
|
||||
|
@ -1289,29 +1286,13 @@ signinWithPasskey: "패스키로 로그인"
|
|||
unknownWebAuthnKey: "등록되지 않은 패스키입니다."
|
||||
passkeyVerificationFailed: "패스키 검증을 실패했습니다."
|
||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
|
||||
messageToFollower: "팔로워에게 보낼 메시지"
|
||||
messageToFollower: "팔로워에 보낼 메시지"
|
||||
target: "대상"
|
||||
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
|
||||
prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
|
||||
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
|
||||
yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
|
||||
yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
|
||||
thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해야 볼 수 있도록 설정되어 있습니다."
|
||||
lockdown: "잠금"
|
||||
pleaseSelectAccount: "계정을 선택해주세요."
|
||||
availableRoles: "사용 가능한 역할"
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
|
||||
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
||||
requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페이지에 삽입, 노트 인용을 지원하지 않는 서버에서 볼 수 없게 됩니다."
|
||||
requireSigninToViewContentsDescription3: "원격 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다."
|
||||
makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 볼 수 있도록 설정하기"
|
||||
makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다.비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
|
||||
makeNotesHiddenBefore: "과거 노트 비공개로 전환하기"
|
||||
makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
|
||||
mayNotEffectForFederatedNotes: "원격 서버에 연합된 노트에는 효과가 없을 수도 있습니다."
|
||||
notesHavePassedSpecifiedPeriod: "지정한 시간이 경과된 노트"
|
||||
notesOlderThanSpecifiedDateAndTime: "지정된 날짜 및 시간 이전의 노트"
|
||||
_abuseUserReport:
|
||||
forward: "전달"
|
||||
forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
|
||||
|
@ -2176,11 +2157,8 @@ _auth:
|
|||
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
|
||||
pleaseGoBack: "앱으로 돌아가서 시도해 주세요"
|
||||
callback: "앱으로 돌아갑니다"
|
||||
accepted: "접근 권한이 부여되었습니다."
|
||||
denied: "접근이 거부되었습니다"
|
||||
scopeUser: "다음 사용자로 활동하고 있습니다."
|
||||
pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오."
|
||||
byClickingYouWillBeRedirectedToThisUrl: "접근을 허용하면 자동으로 다음 URL로 이동합니다."
|
||||
_antennaSources:
|
||||
all: "모든 노트"
|
||||
homeTimeline: "팔로우중인 유저의 노트"
|
||||
|
@ -2732,9 +2710,3 @@ _embedCodeGen:
|
|||
generateCode: "임베디드 코드를 만들기"
|
||||
codeGenerated: "코드를 만들었습니다."
|
||||
codeGeneratedDescription: "만들어진 코드를 웹 사이트에 붙여서 사용하세요."
|
||||
_selfXssPrevention:
|
||||
warning: "경고"
|
||||
title: "“이 화면에 뭔가를 붙여넣어라\"는 것은 모두 사기입니다."
|
||||
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
||||
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
|
||||
description3: "자세한 내용은 여기를 확인해 주세요. {link}"
|
||||
|
|
|
@ -8,9 +8,6 @@ search: "Tìm kiếm"
|
|||
notifications: "Thông báo"
|
||||
username: "Tên người dùng"
|
||||
password: "Mật khẩu"
|
||||
initialPasswordForSetup: "Mật khẩu ban đầu để thiết lập"
|
||||
initialPasswordIsIncorrect: "Mật khẩu ban đầu đã nhập sai"
|
||||
initialPasswordForSetupDescription: "Nếu bạn tự cài đặt Misskey, hãy sử dụng mật khẩu ban đầu của bạn đã nhập trong tệp cấu hình.\nNếu bạn đang sử dụng dịch vụ nào đó giống như dịch vụ lưu trữ của Misskey, hãy sử dụng mật khẩu ban đầu được cung cấp.\nNếu bạn chưa đặt mật khẩu ban đầu, vui lòng để trống và tiếp tục."
|
||||
forgotPassword: "Quên mật khẩu"
|
||||
fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..."
|
||||
ok: "Đồng ý"
|
||||
|
|
|
@ -1299,7 +1299,6 @@ yourNameContainsProhibitedWordsDescription: "用户名内含有违禁词。若
|
|||
thisContentsAreMarkedAsSigninRequiredByAuthor: "根据发帖者的设定,需要登录才能显示"
|
||||
lockdown: "锁定"
|
||||
pleaseSelectAccount: "请选择帐户"
|
||||
availableRoles: "可用角色"
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "需要登录才能显示内容"
|
||||
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
|
||||
|
|
|
@ -8,8 +8,8 @@ search: "搜尋"
|
|||
notifications: "通知"
|
||||
username: "使用者名稱"
|
||||
password: "密碼"
|
||||
initialPasswordForSetup: "啟動初始設定的密碼"
|
||||
initialPasswordIsIncorrect: "啟動初始設定的密碼錯誤。"
|
||||
initialPasswordForSetup: "初始設定用的密碼"
|
||||
initialPasswordIsIncorrect: "初始設定用的密碼錯誤。"
|
||||
initialPasswordForSetupDescription: "如果您自己安裝了 Misskey,請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼,請將其留空並繼續。"
|
||||
forgotPassword: "忘記密碼"
|
||||
fetchingAsApObject: "從聯邦宇宙取得中..."
|
||||
|
@ -1299,7 +1299,6 @@ yourNameContainsProhibitedWordsDescription: "名稱中包含禁止使用的字
|
|||
thisContentsAreMarkedAsSigninRequiredByAuthor: "作者將其設定為需要登入才能顯示。"
|
||||
lockdown: "鎖定"
|
||||
pleaseSelectAccount: "請選擇帳戶"
|
||||
availableRoles: "可用角色"
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "須登入以顯示內容"
|
||||
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2024.10.2-alpha.1",
|
||||
"version": "2024.11.0-alpha.0",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -406,8 +406,10 @@ export class MfmService {
|
|||
mention: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
const { username, host, acct } = node.props;
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
|
||||
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
|
||||
a.setAttribute('href', remoteUserInfo
|
||||
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
|
||||
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
|
||||
a.className = 'u-url mention';
|
||||
a.textContent = acct;
|
||||
return a;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
||||
import type { MiWebhook, WebhookEventTypes, webhookEventTypes } from '@/models/Webhook.js';
|
||||
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -35,6 +35,7 @@ import type {
|
|||
} from './QueueModule.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
import { type UserWebhookPayload } from './UserWebhookService.js';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
|
@ -468,10 +469,10 @@ export class QueueService {
|
|||
* @see UserWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public userWebhookDeliver(
|
||||
public userWebhookDeliver<T extends WebhookEventTypes>(
|
||||
webhook: MiWebhook,
|
||||
type: typeof webhookEventTypes[number],
|
||||
content: unknown,
|
||||
type: T,
|
||||
content: UserWebhookPayload<T>,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: UserWebhookDeliverJobData = {
|
||||
|
|
|
@ -6,11 +6,23 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { type WebhooksRepository } from '@/models/_.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
export type UserWebhookPayload<T extends WebhookEventTypes> =
|
||||
T extends 'note' | 'reply' | 'renote' |'mention' ? {
|
||||
note: Packed<'Note'>,
|
||||
} :
|
||||
T extends 'follow' | 'unfollow' ? {
|
||||
user: Packed<'UserDetailedNotMe'>,
|
||||
} :
|
||||
T extends 'followed' ? {
|
||||
user: Packed<'UserLite'>,
|
||||
} : never;
|
||||
|
||||
@Injectable()
|
||||
export class UserWebhookService implements OnApplicationShutdown {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWeb
|
|||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
|
||||
|
@ -306,10 +306,10 @@ export class WebhookTestService {
|
|||
* - 送信対象イベント(on)に関する設定
|
||||
*/
|
||||
@bindThis
|
||||
public async testUserWebhook(
|
||||
public async testUserWebhook<T extends WebhookEventTypes>(
|
||||
params: {
|
||||
webhookId: MiWebhook['id'],
|
||||
type: WebhookEventTypes,
|
||||
type: T,
|
||||
override?: Partial<Omit<MiWebhook, 'id'>>,
|
||||
},
|
||||
sender: MiUser | null,
|
||||
|
@ -321,7 +321,7 @@ export class WebhookTestService {
|
|||
}
|
||||
|
||||
const webhook = webhooks[0];
|
||||
const send = (contents: unknown) => {
|
||||
const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
|
||||
const merged = {
|
||||
...webhook,
|
||||
...params.override,
|
||||
|
@ -329,7 +329,7 @@ export class WebhookTestService {
|
|||
|
||||
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||
// また、Jobの試行回数も1回だけ.
|
||||
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||
this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
|
||||
};
|
||||
|
||||
const dummyNote1 = generateDummyNote({
|
||||
|
@ -361,33 +361,40 @@ export class WebhookTestService {
|
|||
|
||||
switch (params.type) {
|
||||
case 'note': {
|
||||
send(toPackedNote(dummyNote1));
|
||||
send('note', { note: toPackedNote(dummyNote1) });
|
||||
break;
|
||||
}
|
||||
case 'reply': {
|
||||
send(toPackedNote(dummyReply1));
|
||||
send('reply', { note: toPackedNote(dummyReply1) });
|
||||
break;
|
||||
}
|
||||
case 'renote': {
|
||||
send(toPackedNote(dummyRenote1));
|
||||
send('renote', { note: toPackedNote(dummyRenote1) });
|
||||
break;
|
||||
}
|
||||
case 'mention': {
|
||||
send(toPackedNote(dummyMention1));
|
||||
send('mention', { note: toPackedNote(dummyMention1) });
|
||||
break;
|
||||
}
|
||||
case 'follow': {
|
||||
send(toPackedUserDetailedNotMe(dummyUser1));
|
||||
send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
|
||||
break;
|
||||
}
|
||||
case 'followed': {
|
||||
send(toPackedUserLite(dummyUser2));
|
||||
send('followed', { user: toPackedUserLite(dummyUser2) });
|
||||
break;
|
||||
}
|
||||
case 'unfollow': {
|
||||
send(toPackedUserDetailedNotMe(dummyUser3));
|
||||
send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
|
||||
break;
|
||||
}
|
||||
// まだ実装されていない (#9485)
|
||||
case 'reaction': return;
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _exhaustiveAssertion: never = params.type;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
*/
|
||||
|
||||
export function sqlLikeEscape(s: string) {
|
||||
return s.replace(/([%_])/g, '\\$1');
|
||||
return s.replace(/([\\%_])/g, '\\$1');
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { IActivity } from '@/core/activitypub/type.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
|
||||
|
@ -486,6 +487,16 @@ export class ActivityPubServerService {
|
|||
return;
|
||||
}
|
||||
|
||||
// リモートだったらリダイレクト
|
||||
if (user.host != null) {
|
||||
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
|
||||
reply.code(500);
|
||||
return;
|
||||
}
|
||||
reply.redirect(user.uri, 301);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
|
||||
|
@ -654,19 +665,20 @@ export class ActivityPubServerService {
|
|||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
id: userId,
|
||||
host: IsNull(),
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
return await this.userInfo(request, reply, user);
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
const acct = Acct.parse(request.params.acct);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
usernameLower: request.params.user.toLowerCase(),
|
||||
host: IsNull(),
|
||||
usernameLower: acct.username,
|
||||
host: acct.host ?? IsNull(),
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -465,6 +465,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const newName = updates.name === undefined ? user.name : updates.name;
|
||||
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
|
||||
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
|
||||
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
|
||||
|
||||
if (newName != null) {
|
||||
let hasProhibitedWords = false;
|
||||
|
@ -494,6 +495,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
]);
|
||||
}
|
||||
|
||||
if (newFollowedMessage != null) {
|
||||
const tokens = mfm.parse(newFollowedMessage);
|
||||
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
|
||||
}
|
||||
|
||||
updates.emojis = emojis;
|
||||
updates.tags = tags;
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.activeUsersChart.read(me);
|
||||
|
||||
await this.noteEntityService.packMany(timeline, me);
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
|
||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
||||
|
|
|
@ -42,13 +42,26 @@ import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
|
|||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type {
|
||||
AnnouncementsRepository,
|
||||
ChannelsRepository,
|
||||
ClipsRepository,
|
||||
FlashsRepository,
|
||||
GalleryPostsRepository,
|
||||
MiMeta,
|
||||
NotesRepository,
|
||||
PagesRepository,
|
||||
ReversiGamesRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||
import { FeedService } from './FeedService.js';
|
||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||
|
@ -103,6 +116,9 @@ export class ClientServerService {
|
|||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
@Inject(DI.announcementsRepository)
|
||||
private announcementsRepository: AnnouncementsRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
@ -112,6 +128,7 @@ export class ClientServerService {
|
|||
private clipEntityService: ClipEntityService,
|
||||
private channelEntityService: ChannelEntityService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private announcementEntityService: AnnouncementEntityService,
|
||||
private urlPreviewService: UrlPreviewService,
|
||||
private feedService: FeedService,
|
||||
private roleService: RoleService,
|
||||
|
@ -776,6 +793,24 @@ export class ClientServerService {
|
|||
return await renderBase(reply);
|
||||
}
|
||||
});
|
||||
|
||||
// 個別お知らせページ
|
||||
fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
|
||||
const announcement = await this.announcementsRepository.findOneBy({
|
||||
id: request.params.announcementId,
|
||||
});
|
||||
|
||||
if (announcement) {
|
||||
const _announcement = await this.announcementEntityService.pack(announcement);
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('announcement', {
|
||||
announcement: _announcement,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region noindex pages
|
||||
|
|
21
packages/backend/src/server/web/views/announcement.pug
Normal file
21
packages/backend/src/server/web/views/announcement.pug
Normal file
|
@ -0,0 +1,21 @@
|
|||
extends ./base
|
||||
|
||||
block vars
|
||||
- const title = announcement.title;
|
||||
- const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
|
||||
- const url = `${config.url}/announcements/${announcement.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content=description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= description)
|
||||
meta(property='og:url' content= url)
|
||||
if announcement.imageUrl
|
||||
meta(property='og:image' content=announcement.imageUrl)
|
||||
meta(property='twitter:card' content='summary_large_image')
|
|
@ -2,6 +2,7 @@ block vars
|
|||
|
||||
block loadClientEntry
|
||||
- const entry = config.frontendEntry;
|
||||
- const baseUrl = config.url;
|
||||
|
||||
doctype html
|
||||
|
||||
|
@ -32,7 +33,7 @@ html
|
|||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||
link(rel='manifest' href='/manifest.json')
|
||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
|
||||
link(rel='prefetch' href=serverErrorImageUrl)
|
||||
link(rel='prefetch' href=infoImageUrl)
|
||||
link(rel='prefetch' href=notFoundImageUrl)
|
||||
|
|
|
@ -230,6 +230,7 @@ describe('Webリソース', () => {
|
|||
path: path('xxxxxxxxxx'),
|
||||
type: HTML,
|
||||
}));
|
||||
test.todo('HTMLとしてGETできる。(リモートユーザーでもリダイレクトせず)');
|
||||
});
|
||||
|
||||
describe.each([
|
||||
|
@ -249,6 +250,7 @@ describe('Webリソース', () => {
|
|||
path: path('xxxxxxxxxx'),
|
||||
accept,
|
||||
}));
|
||||
test.todo('はオリジナルにリダイレクトされる。(リモートユーザー)');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { beforeAll, describe, jest } from '@jest/globals';
|
||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
|
@ -122,7 +122,7 @@ describe('WebhookTestService', () => {
|
|||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('note');
|
||||
expect((calls[2] as any).id).toBe('dummy-note-1');
|
||||
expect((calls[2] as UserWebhookPayload<'note'>).note.id).toBe('dummy-note-1');
|
||||
});
|
||||
|
||||
test('reply', async () => {
|
||||
|
@ -131,7 +131,7 @@ describe('WebhookTestService', () => {
|
|||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('reply');
|
||||
expect((calls[2] as any).id).toBe('dummy-reply-1');
|
||||
expect((calls[2] as UserWebhookPayload<'reply'>).note.id).toBe('dummy-reply-1');
|
||||
});
|
||||
|
||||
test('renote', async () => {
|
||||
|
@ -140,7 +140,7 @@ describe('WebhookTestService', () => {
|
|||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('renote');
|
||||
expect((calls[2] as any).id).toBe('dummy-renote-1');
|
||||
expect((calls[2] as UserWebhookPayload<'renote'>).note.id).toBe('dummy-renote-1');
|
||||
});
|
||||
|
||||
test('mention', async () => {
|
||||
|
@ -149,7 +149,7 @@ describe('WebhookTestService', () => {
|
|||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('mention');
|
||||
expect((calls[2] as any).id).toBe('dummy-mention-1');
|
||||
expect((calls[2] as UserWebhookPayload<'mention'>).note.id).toBe('dummy-mention-1');
|
||||
});
|
||||
|
||||
test('follow', async () => {
|
||||
|
@ -158,7 +158,7 @@ describe('WebhookTestService', () => {
|
|||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('follow');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-1');
|
||||
expect((calls[2] as UserWebhookPayload<'follow'>).user.id).toBe('dummy-user-1');
|
||||
});
|
||||
|
||||
test('followed', async () => {
|
||||
|
@ -167,7 +167,7 @@ describe('WebhookTestService', () => {
|
|||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('followed');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-2');
|
||||
expect((calls[2] as UserWebhookPayload<'followed'>).user.id).toBe('dummy-user-2');
|
||||
});
|
||||
|
||||
test('unfollow', async () => {
|
||||
|
@ -176,7 +176,7 @@ describe('WebhookTestService', () => {
|
|||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('unfollow');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-3');
|
||||
expect((calls[2] as UserWebhookPayload<'unfollow'>).user.id).toBe('dummy-user-3');
|
||||
});
|
||||
|
||||
describe('NoSuchWebhookError', () => {
|
||||
|
|
|
@ -22,23 +22,66 @@ type Account = Misskey.entities.MeDetailed & { token: string };
|
|||
const accountData = miLocalStorage.getItem('account');
|
||||
|
||||
// TODO: 外部からはreadonlyに
|
||||
/**
|
||||
* Reactive state for the current account. "I" as in "I am logged in".
|
||||
* Initialized from local storage if available, otherwise null.
|
||||
*
|
||||
* @type {Account | null}
|
||||
*/
|
||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||
|
||||
/**
|
||||
* Whether the current account is a moderator.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
|
||||
|
||||
/**
|
||||
* Whether the current account is an administrator.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
export const iAmAdmin = $i != null && $i.isAdmin;
|
||||
|
||||
/**
|
||||
* Whether it is necessary to sign in; checks if the current
|
||||
* account is null and throws an error if so.
|
||||
*
|
||||
* @throws {Error} If the current account is null
|
||||
* @returns {Account} The current account
|
||||
*/
|
||||
export function signinRequired() {
|
||||
if ($i == null) throw new Error('signin required');
|
||||
return $i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the current number of notes from the current account.
|
||||
*
|
||||
* Note: This appears to only be used for the "notes1" achievement.
|
||||
*
|
||||
* Also, separating it like this might cause counts to get out-of-sync.
|
||||
*/
|
||||
export let notesCount = $i == null ? 0 : $i.notesCount;
|
||||
|
||||
/**
|
||||
* Increments the number of notes by one.
|
||||
*
|
||||
* Documentation TODO: What about $i.notesCount? Why not increment that?
|
||||
*/
|
||||
export function incNotesCount() {
|
||||
notesCount++;
|
||||
}
|
||||
|
||||
export async function signout() {
|
||||
if (!$i) return;
|
||||
|
||||
// If we're not signed in, there's nothing to do.
|
||||
if (!$i) {
|
||||
// Error log:
|
||||
console.error('signout() called when not signed in');
|
||||
return;
|
||||
}
|
||||
|
||||
waiting();
|
||||
miLocalStorage.removeItem('account');
|
||||
|
|
|
@ -15,7 +15,7 @@ import { updateI18n, i18n } from '@/i18n.js';
|
|||
import { $i, refreshAccount, login } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
|
||||
import { reloadChannel } from '@/scripts/unison-reload.js';
|
||||
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||
|
@ -185,6 +185,10 @@ export async function common(createVue: () => App<Element>) {
|
|||
}
|
||||
});
|
||||
|
||||
watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
|
||||
updateDeviceKind(kind);
|
||||
}, { immediate: true });
|
||||
|
||||
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
||||
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||
}, { immediate: true });
|
||||
|
|
|
@ -160,7 +160,7 @@ async function deleteAntenna() {
|
|||
function addUser() {
|
||||
os.selectUser({ includeSelf: true }).then(user => {
|
||||
users.value = users.value.trim();
|
||||
users.value += '\n@' + Misskey.acct.toString(user as any);
|
||||
users.value += '\n@' + Misskey.acct.toString(user);
|
||||
users.value = users.value.trim();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
||||
const props = defineProps<{
|
||||
channel: Record<string, any>;
|
||||
channel: Misskey.entities.Channel;
|
||||
}>();
|
||||
|
||||
const getLastReadedAt = (): number | null => {
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:withOkButton="true"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.cropImage }}</template>
|
||||
<template #default="{ width, height }">
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
|
||||
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')">
|
||||
<template #header>:{{ emoji.name }}:</template>
|
||||
<template #default>
|
||||
<MkSpacer>
|
||||
|
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</MkSelect>
|
||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
<div v-if="actions" :class="$style.buttons">
|
||||
|
@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
|
|||
text: string;
|
||||
primary?: boolean,
|
||||
danger?: boolean,
|
||||
callback: (...args: any[]) => void;
|
||||
callback: (...args: unknown[]) => void;
|
||||
}[];
|
||||
showOkButton?: boolean;
|
||||
showCancelButton?: boolean;
|
||||
|
|
|
@ -157,7 +157,7 @@ const ilFilesObserver = new IntersectionObserver(
|
|||
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
|
||||
);
|
||||
|
||||
const sortModeSelect = ref('+createdAt');
|
||||
const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
|
||||
|
||||
watch(folder, () => emit('cd', folder.value));
|
||||
watch(sortModeSelect, () => {
|
||||
|
@ -198,7 +198,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
|
|||
removeFolder(folderId);
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent): any {
|
||||
function onDragover(ev: DragEvent) {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
||||
|
@ -243,7 +243,7 @@ function onDragleave() {
|
|||
draghover.value = false;
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent): any {
|
||||
function onDrop(ev: DragEvent) {
|
||||
draghover.value = false;
|
||||
|
||||
if (!ev.dataTransfer) return;
|
||||
|
@ -332,7 +332,7 @@ function createFolder() {
|
|||
title: i18n.ts.createFolder,
|
||||
placeholder: i18n.ts.folderName,
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
if (canceled || name == null) return;
|
||||
misskeyApi('drive/folders/create', {
|
||||
name: name,
|
||||
parentId: folder.value ? folder.value.id : undefined,
|
||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:scroll="false"
|
||||
:withOkButton="false"
|
||||
@close="cancel()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
|||
elm.title = getEmojiName(emoji);
|
||||
}
|
||||
|
||||
function nestedChosen(emoji: any, ev: MouseEvent) {
|
||||
function nestedChosen(emoji: string, ev: MouseEvent) {
|
||||
emit('chosen', emoji, ev);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
|||
elm.title = getEmojiName(emoji);
|
||||
}
|
||||
|
||||
function chosen(emoji: any, ev?: MouseEvent) {
|
||||
function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
|
||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
|
|||
// 最近使った絵文字更新
|
||||
if (!pinned.value?.includes(key)) {
|
||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji: any) => emoji !== key);
|
||||
recents = recents.filter((emoji) => emoji !== key);
|
||||
recents.unshift(key);
|
||||
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ export type Extension = {
|
|||
author: string;
|
||||
description?: string;
|
||||
permissions?: string[];
|
||||
config?: Record<string, any>;
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
} | {
|
||||
type: 'theme';
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@click="cancel()"
|
||||
@ok="ok()"
|
||||
@close="cancel()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ title }}
|
||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
|
||||
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
@ -53,7 +53,7 @@ import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
|
|||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number | null;
|
||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
||||
type?: InputHTMLAttributes['type'];
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@ -64,8 +64,8 @@ const props = defineProps<{
|
|||
mfmAutocomplete?: boolean | SuggestionType[],
|
||||
autocapitalize?: string;
|
||||
spellcheck?: boolean;
|
||||
inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
|
||||
step?: any;
|
||||
inputmode?: InputHTMLAttributes['inputmode'];
|
||||
step?: InputHTMLAttributes['step'];
|
||||
datalist?: string[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
|
|
@ -118,7 +118,7 @@ import { hms } from '@/filters/hms.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
|
||||
import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
|
||||
import hasAudio from '@/scripts/media-has-audio.js';
|
||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
|
@ -334,27 +334,22 @@ function togglePlayPause() {
|
|||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (isFullscreenNotSupported && videoEl.value) {
|
||||
if (playerEl.value == null || videoEl.value == null) return;
|
||||
if (isFullscreen.value) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
videoEl.value.webkitExitFullscreen();
|
||||
exitFullscreen({
|
||||
videoEl: videoEl.value,
|
||||
});
|
||||
isFullscreen.value = false;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
videoEl.value.webkitEnterFullscreen();
|
||||
requestFullscreen({
|
||||
videoEl: videoEl.value,
|
||||
playerEl: playerEl.value,
|
||||
options: {
|
||||
navigationUI: 'hide',
|
||||
},
|
||||
});
|
||||
isFullscreen.value = true;
|
||||
}
|
||||
} else if (playerEl.value) {
|
||||
if (isFullscreen.value) {
|
||||
document.exitFullscreen();
|
||||
isFullscreen.value = false;
|
||||
} else {
|
||||
playerEl.value.requestFullscreen({ navigationUI: 'hide' });
|
||||
isFullscreen.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function togglePictureInPicture() {
|
||||
|
@ -454,8 +449,10 @@ watch(loop, (to) => {
|
|||
});
|
||||
|
||||
watch(hide, (to) => {
|
||||
if (to && isFullscreen.value) {
|
||||
document.exitFullscreen();
|
||||
if (videoEl.value && to && isFullscreen.value) {
|
||||
exitFullscreen({
|
||||
videoEl: videoEl.value,
|
||||
});
|
||||
isFullscreen.value = false;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -227,7 +227,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const inTimeline = inject<boolean>('inTimeline', false);
|
||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(false));
|
||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
||||
const inChannel = inject('inChannel', null);
|
||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
|
@ -292,15 +292,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
|
|||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||
*/
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
|
||||
if (mutedWords == null) return false;
|
||||
|
||||
if (mutedWords != null) {
|
||||
if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
|
||||
if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
|
||||
if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
|
||||
}
|
||||
|
||||
if (checkOnly) return false;
|
||||
|
||||
if (inTimeline && !tl_withSensitive.value && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
|
||||
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
||||
return 'sensitiveMute';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
|
||||
const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap);
|
||||
|
||||
function ok() {
|
||||
emit('done', {
|
||||
|
|
|
@ -39,7 +39,7 @@ import number from '@/filters/number.js';
|
|||
import XValue from '@/components/MkObjectView.value.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
value: any;
|
||||
value: unknown;
|
||||
}>();
|
||||
|
||||
const collapsed = reactive({});
|
||||
|
@ -50,19 +50,19 @@ if (isObject(props.value)) {
|
|||
}
|
||||
}
|
||||
|
||||
function isObject(v): boolean {
|
||||
function isObject(v: unknown): v is Record<PropertyKey, unknown> {
|
||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||
}
|
||||
|
||||
function isArray(v): boolean {
|
||||
function isArray(v: unknown): v is unknown[] {
|
||||
return Array.isArray(v);
|
||||
}
|
||||
|
||||
function isEmpty(v): boolean {
|
||||
function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] {
|
||||
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
||||
}
|
||||
|
||||
function collapsable(v): boolean {
|
||||
function collapsable(v: unknown): boolean {
|
||||
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:buttonsLeft="buttonsLeft"
|
||||
:buttonsRight="buttonsRight"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
<template v-if="pageMetadata">
|
||||
|
@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/scripts/popout.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { openingWindowsCount } from '@/os.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import { useRouterFactory } from '@/router/supplier.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
|
||||
|
@ -48,7 +48,7 @@ const props = defineProps<{
|
|||
initialPath: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
|
@ -58,7 +58,7 @@ const windowRouter = routerFactory(props.initialPath);
|
|||
const contents = shallowRef<HTMLElement | null>(null);
|
||||
const pageMetadata = ref<null | PageMetadata>(null);
|
||||
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const history = ref<{ path: string; key: any; }[]>([{
|
||||
const history = ref<{ path: string; key: string; }[]>([{
|
||||
path: windowRouter.getCurrentPath(),
|
||||
key: windowRouter.getCurrentKey(),
|
||||
}]);
|
||||
|
|
|
@ -19,7 +19,7 @@ defineProps<{
|
|||
items: MenuItem[];
|
||||
align?: 'center' | string;
|
||||
width?: number;
|
||||
src?: any;
|
||||
src?: HTMLElement | null;
|
||||
returnFocusTo?: HTMLElement | null;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -129,25 +129,13 @@ import { miLocalStorage } from '@/local-storage.js';
|
|||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
reply?: Misskey.entities.Note;
|
||||
renote?: Misskey.entities.Note;
|
||||
channel?: Misskey.entities.Channel; // TODO
|
||||
mention?: Misskey.entities.User;
|
||||
specified?: Misskey.entities.UserDetailed;
|
||||
initialText?: string;
|
||||
initialCw?: string;
|
||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||
initialFiles?: Misskey.entities.DriveFile[];
|
||||
initialLocalOnly?: boolean;
|
||||
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||
initialNote?: Misskey.entities.Note;
|
||||
instant?: boolean;
|
||||
const props = withDefaults(defineProps<PostFormProps & {
|
||||
fixed?: boolean;
|
||||
autofocus?: boolean;
|
||||
freezeAfterPosted?: boolean;
|
||||
|
@ -955,8 +943,8 @@ function showActions(ev: MouseEvent) {
|
|||
action.handler({
|
||||
text: text.value,
|
||||
cw: cw.value,
|
||||
}, (key, value: any) => {
|
||||
if (typeof key !== 'string') return;
|
||||
}, (key, value) => {
|
||||
if (typeof key !== 'string' || typeof value !== 'string') return;
|
||||
if (key === 'text') { text.value = value; }
|
||||
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
|
||||
});
|
||||
|
@ -1120,7 +1108,7 @@ defineExpose({
|
|||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
.submitInner {
|
||||
> .submitInner {
|
||||
outline: 2px solid var(--MI_THEME-fgOnAccent);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
@ -1135,13 +1123,13 @@ defineExpose({
|
|||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
> .inner {
|
||||
> .submitInner {
|
||||
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
> .inner {
|
||||
> .submitInner {
|
||||
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js';
|
|||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any[];
|
||||
modelValue: Misskey.entities.DriveFile[];
|
||||
detachMediaFn?: (id: string) => void;
|
||||
}>();
|
||||
|
||||
const mock = inject<boolean>('mock', false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any[]): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void;
|
||||
(ev: 'detach', id: string): void;
|
||||
(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
|
||||
(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
|
||||
|
@ -113,7 +113,7 @@ async function rename(file) {
|
|||
});
|
||||
}
|
||||
|
||||
async function describe(file) {
|
||||
async function describe(file: Misskey.entities.DriveFile) {
|
||||
if (mock) return;
|
||||
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
|
||||
|
|
|
@ -11,23 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
reply?: Misskey.entities.Note;
|
||||
renote?: Misskey.entities.Note;
|
||||
channel?: any; // TODO
|
||||
mention?: Misskey.entities.User;
|
||||
specified?: Misskey.entities.UserDetailed;
|
||||
initialText?: string;
|
||||
initialCw?: string;
|
||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||
initialFiles?: Misskey.entities.DriveFile[];
|
||||
initialLocalOnly?: boolean;
|
||||
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||
initialNote?: Misskey.entities.Note;
|
||||
const props = withDefaults(defineProps<PostFormProps & {
|
||||
instant?: boolean;
|
||||
fixed?: boolean;
|
||||
autofocus?: boolean;
|
||||
|
|
|
@ -24,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="T extends unknown">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
value: any;
|
||||
modelValue: T;
|
||||
value: T;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: T): void;
|
||||
}>();
|
||||
|
||||
const checked = computed(() => props.modelValue === props.value);
|
||||
|
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { getEmojiName } from '@@/js/emojilist.js';
|
||||
import MkTooltip from './MkTooltip.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
|
@ -30,7 +31,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
|||
defineProps<{
|
||||
showing: boolean;
|
||||
reaction: string;
|
||||
users: any[]; // TODO
|
||||
users: Misskey.entities.UserLite[];
|
||||
count: number;
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
|
|
@ -277,7 +277,7 @@ async function onSubmit(): Promise<void> {
|
|||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
if (res && res.ok) {
|
||||
if (res.status === 204 || instance.emailRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
|
@ -295,6 +295,8 @@ async function onSubmit(): Promise<void> {
|
|||
await login(resJson.token);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onSignupApiError();
|
||||
}
|
||||
|
||||
submitting.value = false;
|
||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:width="500"
|
||||
:height="600"
|
||||
@close="onClose"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.signup }}</template>
|
||||
|
||||
|
|
|
@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
<script lang="ts">
|
||||
export type SuperMenuDef = {
|
||||
title?: string;
|
||||
items: ({
|
||||
type: 'a';
|
||||
href: string;
|
||||
target?: string;
|
||||
icon?: string;
|
||||
text: string;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
} | {
|
||||
type: 'button';
|
||||
icon?: string;
|
||||
text: string;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
action: (ev: MouseEvent) => void;
|
||||
} | {
|
||||
type: 'link';
|
||||
to: string;
|
||||
icon?: string;
|
||||
text: string;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
})[];
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
def: any[];
|
||||
def: SuperMenuDef[];
|
||||
grid?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
@ -43,6 +43,7 @@ const props = withDefaults(defineProps<{
|
|||
}>(), {
|
||||
withRenotes: true,
|
||||
withReplies: false,
|
||||
withSensitive: true,
|
||||
onlyFiles: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:okButtonDisabled="false"
|
||||
:canClose="false"
|
||||
@close="dialog?.close()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
@ok="ok()"
|
||||
>
|
||||
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
|
||||
|
|
|
@ -180,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
|
|||
sensitive.value = info.sensitive ?? false;
|
||||
});
|
||||
|
||||
function adjustTweetHeight(message: any) {
|
||||
function adjustTweetHeight(message: MessageEvent) {
|
||||
if (message.origin !== 'https://platform.twitter.com') return;
|
||||
const embed = message.data?.['twttr.embed'];
|
||||
if (embed?.method !== 'twttr.private.resize') return;
|
||||
|
@ -193,14 +193,16 @@ function openPlayer(): void {
|
|||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
|
||||
url: requestUrl.href,
|
||||
}, {
|
||||
// TODO
|
||||
closed: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).addEventListener('message', adjustTweetHeight);
|
||||
window.addEventListener('message', adjustTweetHeight);
|
||||
|
||||
onUnmounted(() => {
|
||||
(window as any).removeEventListener('message', adjustTweetHeight);
|
||||
window.removeEventListener('message', adjustTweetHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="dialog"
|
||||
:width="400"
|
||||
@close="dialog?.close()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="announcement" #header>:{{ announcement.title }}:</template>
|
||||
<template v-else #header>New announcement</template>
|
||||
|
@ -62,9 +62,16 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
|
||||
type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
|
||||
|
||||
const props = defineProps<{
|
||||
user: Misskey.entities.User,
|
||||
announcement?: Misskey.entities.Announcement,
|
||||
announcement?: Required<AdminAnnouncementType>,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
|
||||
|
@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info');
|
|||
const display = ref(props.announcement ? props.announcement.display : 'dialog');
|
||||
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
async function done() {
|
||||
const params = {
|
||||
title: title.value,
|
||||
|
@ -88,7 +90,7 @@ async function done() {
|
|||
display: display.value,
|
||||
needConfirmationToRead: needConfirmationToRead.value,
|
||||
userId: props.user.id,
|
||||
};
|
||||
} satisfies Misskey.entities.AdminAnnouncementsCreateRequest;
|
||||
|
||||
if (props.announcement) {
|
||||
await os.apiWithDialog('admin/announcements/update', {
|
||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.selectUser }}</template>
|
||||
<div>
|
||||
|
|
|
@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkTooltip from './MkTooltip.vue';
|
||||
|
||||
defineProps<{
|
||||
showing: boolean;
|
||||
users: any[]; // TODO
|
||||
users: Misskey.entities.UserLite[];
|
||||
count: number;
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
||||
</MkSelect>
|
||||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
</header>
|
||||
<Sortable
|
||||
:modelValue="props.widgets"
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
|
||||
appear
|
||||
@afterLeave="$emit('closed')"
|
||||
@afterLeave="emit('closed')"
|
||||
>
|
||||
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
|
||||
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
|
@ -60,6 +60,13 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
type WindowButton = {
|
||||
title: string;
|
||||
icon: string;
|
||||
onClick: () => void;
|
||||
highlighted?: boolean;
|
||||
};
|
||||
|
||||
const minHeight = 50;
|
||||
const minWidth = 250;
|
||||
|
||||
|
@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{
|
|||
mini?: boolean;
|
||||
front?: boolean;
|
||||
contextmenu?: MenuItem[] | null;
|
||||
buttonsLeft?: any[];
|
||||
buttonsRight?: any[];
|
||||
buttonsLeft?: WindowButton[];
|
||||
buttonsRight?: WindowButton[];
|
||||
}>(), {
|
||||
initialWidth: 400,
|
||||
initialHeight: null,
|
||||
|
|
|
@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="T extends unknown">
|
||||
import { ref, watch } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
p: () => Promise<any>;
|
||||
p: () => Promise<T>;
|
||||
}>();
|
||||
|
||||
const pending = ref(true);
|
||||
const resolved = ref(false);
|
||||
const rejected = ref(false);
|
||||
const result = ref<any>(null);
|
||||
const result = ref<T | null>(null);
|
||||
|
||||
const process = () => {
|
||||
if (props.p == null) {
|
||||
|
|
|
@ -467,8 +467,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
}
|
||||
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
console.error('unrecognized ast type:', (token as any).type);
|
||||
// @ts-expect-error 存在しないASTタイプ
|
||||
console.error('unrecognized ast type:', token.type);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* A typesafe enum of keys for localStorage.
|
||||
*/
|
||||
export type Keys =
|
||||
'v' |
|
||||
'lastVersion' |
|
||||
|
@ -44,16 +47,45 @@ export type Keys =
|
|||
// セッション毎に廃棄されるLocalStorage代替(セーフモードなどで使用できそう)
|
||||
//const safeSessionStorage = new Map<Keys, string>();
|
||||
|
||||
|
||||
/**
|
||||
* A utility object for interacting with the browser's localStorage.
|
||||
*
|
||||
* It's mostly a small wrapper around window.localStorage, but it validates
|
||||
* keys with a typesafe enum, and provides a few convenience methods for JSON.
|
||||
*/
|
||||
export const miLocalStorage = {
|
||||
/**
|
||||
* Retrieves an item from localStorage.
|
||||
* @param {Keys} key - The key of the item to retrieve.
|
||||
* @returns {string | null} The value of the item, or null if the item does not exist.
|
||||
*/
|
||||
getItem: (key: Keys): string | null => {
|
||||
return window.localStorage.getItem(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores an item in localStorage.
|
||||
* @param {Keys} key - The key of the item to store.
|
||||
* @param {string} value - The value of the item to store.
|
||||
*/
|
||||
setItem: (key: Keys, value: string): void => {
|
||||
window.localStorage.setItem(key, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes an item from localStorage.
|
||||
* @param {Keys} key - The key of the item to remove.
|
||||
*/
|
||||
removeItem: (key: Keys): void => {
|
||||
window.localStorage.removeItem(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves an item from localStorage and parses it as JSON.
|
||||
* @param {Keys} key - The key of the item to retrieve.
|
||||
* @returns {any | undefined} The parsed value of the item, or undefined if the item does not exist.
|
||||
*/
|
||||
getItemAsJson: (key: Keys): any | undefined => {
|
||||
const item = miLocalStorage.getItem(key);
|
||||
if (item === null) {
|
||||
|
@ -61,6 +93,12 @@ export const miLocalStorage = {
|
|||
}
|
||||
return JSON.parse(item);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores an item in localStorage as a JSON string.
|
||||
* @param {Keys} key - The key of the item to store.
|
||||
* @param {any} value - The value of the item to store.
|
||||
*/
|
||||
setItemAsJson: (key: Keys, value: any): void => {
|
||||
miLocalStorage.setItem(key, JSON.stringify(value));
|
||||
},
|
||||
|
|
|
@ -36,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase {
|
|||
|
||||
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
|
||||
|
||||
export type RouterFlag = 'forcePage';
|
||||
|
||||
type ParsedPath = (string | {
|
||||
name: string;
|
||||
startsWith?: string;
|
||||
|
@ -107,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
|
|||
current: Resolved;
|
||||
currentRef: ShallowRef<Resolved>;
|
||||
currentRoute: ShallowRef<RouteDef>;
|
||||
navHook: ((path: string, flag?: any) => boolean) | null;
|
||||
navHook: ((path: string, flag?: RouterFlag) => boolean) | null;
|
||||
|
||||
/**
|
||||
* ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
|
||||
|
@ -116,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
|
|||
|
||||
resolve(path: string): Resolved | null;
|
||||
|
||||
getCurrentPath(): any;
|
||||
getCurrentPath(): string;
|
||||
|
||||
getCurrentKey(): string;
|
||||
|
||||
push(path: string, flag?: any): void;
|
||||
push(path: string, flag?: RouterFlag): void;
|
||||
|
||||
replace(path: string, key?: string | null): void;
|
||||
|
||||
|
@ -197,7 +199,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||
private currentKey = Date.now().toString();
|
||||
private redirectCount = 0;
|
||||
|
||||
public navHook: ((path: string, flag?: any) => boolean) | null = null;
|
||||
public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
|
||||
|
||||
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
|
||||
super();
|
||||
|
@ -404,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||
return this.currentKey;
|
||||
}
|
||||
|
||||
public push(path: string, flag?: any) {
|
||||
public push(path: string, flag?: RouterFlag) {
|
||||
const beforePath = this.currentPath;
|
||||
if (path === beforePath) {
|
||||
this.emit('same');
|
||||
|
|
|
@ -28,12 +28,13 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
|||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||
import { focusParent } from '@/scripts/focus.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
|
||||
export const openingWindowsCount = ref(0);
|
||||
|
||||
export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
||||
export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
data: P = {} as any,
|
||||
data: P,
|
||||
token?: string | null | undefined,
|
||||
customErrors?: Record<string, { title?: string; text: string; }>,
|
||||
) => {
|
||||
|
@ -94,7 +95,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
|
|||
|
||||
export function promiseDialog<T extends Promise<any>>(
|
||||
promise: T,
|
||||
onSuccess?: ((res: any) => void) | null,
|
||||
onSuccess?: ((res: Awaited<T>) => void) | null,
|
||||
onFailure?: ((err: Misskey.api.APIError) => void) | null,
|
||||
text?: string,
|
||||
): T {
|
||||
|
@ -135,20 +136,40 @@ export function promiseDialog<T extends Promise<any>>(
|
|||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counter for generating unique popup IDs.
|
||||
* @type {number}
|
||||
*/
|
||||
let popupIdCount = 0;
|
||||
export const popups = ref([]) as Ref<{
|
||||
|
||||
/**
|
||||
* A reactive list of the currently opened popups. This is used in a Vue component
|
||||
* in a v-for loop to render the popups.
|
||||
*/
|
||||
export const popups = ref<{
|
||||
id: number;
|
||||
component: Component;
|
||||
props: Record<string, any>;
|
||||
events: Record<string, any>;
|
||||
}[]>;
|
||||
}[]>([]);
|
||||
|
||||
/**
|
||||
* An object containing z-index values for different priority levels.
|
||||
*/
|
||||
const zIndexes = {
|
||||
veryLow: 500000,
|
||||
low: 1000000,
|
||||
middle: 2000000,
|
||||
high: 3000000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Claims a z-index value for a given priority level.
|
||||
* Increments the z-index value for the specified priority by 100 and returns the new value.
|
||||
*
|
||||
* @param {keyof typeof zIndexes} [priority='low'] - The priority level for which to claim a z-index.
|
||||
* @returns {number} The new z-index value for the specified priority.
|
||||
*/
|
||||
export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
|
||||
zIndexes[priority] += 100;
|
||||
return zIndexes[priority];
|
||||
|
@ -176,6 +197,15 @@ type EmitsExtractor<T> = {
|
|||
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens a popup with the specified component, props, and events.
|
||||
*
|
||||
* @template T - The type of the component.
|
||||
* @param {T} component - The Vue component to display in the popup.
|
||||
* @param {ComponentProps<T>} props - The props to pass to the component.
|
||||
* @param {ComponentEmit<T>} [events={}] - The events to bind to the component.
|
||||
* @returns {{ dispose: () => void }} An object containing a dispose function to close the popup.
|
||||
*/
|
||||
export function popup<T extends Component>(
|
||||
component: T,
|
||||
props: ComponentProps<T>,
|
||||
|
@ -183,13 +213,18 @@ export function popup<T extends Component>(
|
|||
): { dispose: () => void } {
|
||||
markRaw(component);
|
||||
|
||||
// Generate a unique ID for this popup.
|
||||
const id = ++popupIdCount;
|
||||
|
||||
// On disposal, remove this popup from the list of open popups.
|
||||
const dispose = () => {
|
||||
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
|
||||
window.setTimeout(() => {
|
||||
popups.value = popups.value.filter(p => p.id !== id);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Bundle the component, props, and events into a state object.
|
||||
const state = {
|
||||
component,
|
||||
props,
|
||||
|
@ -197,13 +232,19 @@ export function popup<T extends Component>(
|
|||
id,
|
||||
};
|
||||
|
||||
// Add the popup to the list of open popups.
|
||||
popups.value.push(state);
|
||||
|
||||
// Return a function that can be called to close the popup.
|
||||
return {
|
||||
dispose,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the page with the given path in a pop-up window.
|
||||
* @param path The path of the page to open.
|
||||
*/
|
||||
export function pageWindow(path: string) {
|
||||
const { dispose } = popup(MkPageWindow, {
|
||||
initialPath: path,
|
||||
|
@ -212,6 +253,11 @@ export function pageWindow(path: string) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a toast message to the user.
|
||||
*
|
||||
* @param {string} message - The message to display in the toast.
|
||||
*/
|
||||
export function toast(message: string) {
|
||||
const { dispose } = popup(MkToast, {
|
||||
message,
|
||||
|
@ -220,6 +266,15 @@ export function toast(message: string) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an alert dialog to the user.
|
||||
*
|
||||
* @param {Object} props - The properties for the alert dialog.
|
||||
* @param {'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'} [props.type] - The type of the alert.
|
||||
* @param {string} [props.title] - The title of the alert dialog.
|
||||
* @param {string} [props.text] - The text content of the alert dialog.
|
||||
* @returns {Promise<void>} A promise that resolves when the alert dialog is closed.
|
||||
*/
|
||||
export function alert(props: {
|
||||
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||
title?: string;
|
||||
|
@ -458,7 +513,7 @@ type SelectItem<C> = {
|
|||
};
|
||||
|
||||
// default が指定されていたら result は null になり得ないことを保証する overload function
|
||||
export function select<C = any>(props: {
|
||||
export function select<C = unknown>(props: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
default: string;
|
||||
|
@ -471,7 +526,7 @@ export function select<C = any>(props: {
|
|||
} | {
|
||||
canceled: false; result: C;
|
||||
}>;
|
||||
export function select<C = any>(props: {
|
||||
export function select<C = unknown>(props: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
default?: string | null;
|
||||
|
@ -484,7 +539,7 @@ export function select<C = any>(props: {
|
|||
} | {
|
||||
canceled: false; result: C | null;
|
||||
}>;
|
||||
export function select<C = any>(props: {
|
||||
export function select<C = unknown>(props: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
default?: string | null;
|
||||
|
@ -687,13 +742,13 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
|||
}));
|
||||
}
|
||||
|
||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||
export function post(props: PostFormProps = {}): Promise<void> {
|
||||
pleaseLogin({
|
||||
openOnRemote: (props.initialText || props.initialNote ? {
|
||||
type: 'share',
|
||||
params: {
|
||||
text: props.initialText ?? props.initialNote.text,
|
||||
visibility: props.initialVisibility ?? props.initialNote?.visibility,
|
||||
text: props.initialText ?? props.initialNote?.text ?? '',
|
||||
visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public',
|
||||
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
||||
},
|
||||
} : undefined),
|
||||
|
|
|
@ -272,6 +272,9 @@ const patronsWithIcon = [{
|
|||
}, {
|
||||
name: 'Yatoigawa',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
|
||||
}, {
|
||||
name: '秋瀬カヲル',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/0f22aeb866484f4fa51db6721e3f9847.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
|
@ -380,6 +383,7 @@ const patrons = [
|
|||
'ケモナーのケシン',
|
||||
'こまつぶり',
|
||||
'まゆつな空高',
|
||||
'asata',
|
||||
];
|
||||
|
||||
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
@ -99,19 +99,19 @@ async function addUser() {
|
|||
const { canceled: canceled1, result: username } = await os.inputText({
|
||||
title: i18n.ts.username,
|
||||
});
|
||||
if (canceled1) return;
|
||||
if (canceled1 || username == null) return;
|
||||
|
||||
const { canceled: canceled2, result: password } = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
});
|
||||
if (canceled2) return;
|
||||
if (canceled2 || password == null) return;
|
||||
|
||||
os.apiWithDialog('admin/accounts/create', {
|
||||
username: username,
|
||||
password: password,
|
||||
}).then(res => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ const headerActions = computed(() => []);
|
|||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
|
||||
title: announcement.value ? announcement.value.title : i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}));
|
||||
</script>
|
||||
|
|
|
@ -62,7 +62,7 @@ function accepted() {
|
|||
state.value = 'accepted';
|
||||
if (session.value && session.value.app.callbackUrl) {
|
||||
const url = new URL(session.value.app.callbackUrl);
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ const selectAll = () => {
|
|||
if (selectedEmojis.value.length > 0) {
|
||||
selectedEmojis.value = [];
|
||||
} else {
|
||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
|
||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -133,7 +133,7 @@ const add = async (ev: MouseEvent) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.created) {
|
||||
emojisPaginationComponent.value.prepend(result.created);
|
||||
emojisPaginationComponent.value?.prepend(result.created);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
|
@ -146,12 +146,12 @@ const edit = (emoji) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
|
||||
...oldEmoji,
|
||||
...result.updated,
|
||||
}));
|
||||
} else if (result.deleted) {
|
||||
emojisPaginationComponent.value.removeItem(emoji.id);
|
||||
emojisPaginationComponent.value?.removeItem(emoji.id);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
|
@ -226,7 +226,7 @@ const setCategoryBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
category: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const setLicenseBulk = async () => {
|
||||
|
@ -238,43 +238,43 @@ const setLicenseBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
license: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const addTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const removeTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const setTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const delBulk = async () => {
|
||||
|
@ -286,7 +286,7 @@ const delBulk = async () => {
|
|||
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
|
|
|
@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:initialWidth="400"
|
||||
:initialHeight="500"
|
||||
:canResize="true"
|
||||
@close="windowEl.close()"
|
||||
@closed="$emit('closed')"
|
||||
@close="windowEl?.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
|
||||
<template v-else #header>New emoji</template>
|
||||
|
@ -95,14 +95,19 @@ import { selectFile } from '@/scripts/select-file.js';
|
|||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji?: any,
|
||||
emoji?: Misskey.entities.EmojiDetailed,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
||||
const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
|
||||
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||
const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
|
||||
const license = ref<string>(props.emoji?.license ? props.emoji.license : '');
|
||||
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
|
||||
const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
|
||||
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||
|
@ -115,12 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
|
|||
|
||||
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
async function changeImage(ev) {
|
||||
async function changeImage(ev: Event) {
|
||||
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||
if (candidate.match(/^[a-z0-9_]+$/)) {
|
||||
|
@ -140,7 +140,7 @@ async function addRole() {
|
|||
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
||||
}
|
||||
|
||||
async function removeRole(role, ev) {
|
||||
async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ async function done() {
|
|||
},
|
||||
});
|
||||
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/emoji/add', params);
|
||||
|
||||
|
@ -180,11 +180,12 @@ async function done() {
|
|||
created: created,
|
||||
});
|
||||
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
if (!props.emoji) return;
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.removeAreYouSure({ x: name.value }),
|
||||
|
@ -197,7 +198,7 @@ async function del() {
|
|||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -55,13 +55,13 @@ const pagination = {
|
|||
|
||||
function accept(user) {
|
||||
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function reject(user) {
|
||||
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ function fetch() {
|
|||
return;
|
||||
}
|
||||
|
||||
let promise: Promise<any>;
|
||||
let promise: Promise<unknown>;
|
||||
|
||||
if (uri.startsWith('https://')) {
|
||||
promise = misskeyApi('ap/show', {
|
||||
|
|
|
@ -65,7 +65,7 @@ async function onAccept(token: string) {
|
|||
|
||||
if (props.callback && props.callback !== '') {
|
||||
const cbUrl = new URL(props.callback);
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||
cbUrl.searchParams.set('session', props.session);
|
||||
location.href = cbUrl.toString();
|
||||
} else {
|
||||
|
|
|
@ -77,15 +77,15 @@ async function create() {
|
|||
|
||||
clipsCache.delete();
|
||||
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
function onClipCreated() {
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
function onClipDeleted() {
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -110,7 +110,7 @@ function addUser() {
|
|||
listId: list.value.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
paginationEl.value.reload();
|
||||
paginationEl.value?.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ async function removeUser(item, ev) {
|
|||
listId: list.value.id,
|
||||
userId: item.userId,
|
||||
}).then(() => {
|
||||
paginationEl.value.removeItem(item.id);
|
||||
paginationEl.value?.removeItem(item.id);
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
|
||||
<template #func>
|
||||
<button @click="choose()">
|
||||
|
@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'image' };
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
|
||||
(ev: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
|
||||
|
||||
<section style="padding: 16px;" class="_gaps_s">
|
||||
|
@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'note' };
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
|
||||
}>();
|
||||
|
||||
const id = ref<any>(props.modelValue.note);
|
||||
const id = ref(props.modelValue.note);
|
||||
const note = ref<Misskey.entities.Note | null>(null);
|
||||
|
||||
watch(id, async () => {
|
||||
if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
|
||||
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop();
|
||||
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null;
|
||||
}
|
||||
|
||||
if (!id.value) {
|
||||
note.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update:modelValue', {
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
|
||||
<template #func>
|
||||
<button class="_button" @click="rename()">
|
||||
|
@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
|
||||
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -33,14 +34,13 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
|
|||
|
||||
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: any,
|
||||
}>(), {
|
||||
modelValue: {},
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'section'; },
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
|
||||
(ev: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const children = ref(deepClone(props.modelValue.children ?? []));
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
|
||||
|
||||
<section>
|
||||
|
@ -15,18 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
|
||||
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Autocomplete } from '@/scripts/autocomplete.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'text' }
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
|
||||
}>();
|
||||
|
||||
let autocomplete: Autocomplete;
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)">
|
||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{element}">
|
||||
<div :class="$style.item">
|
||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||
|
|
|
@ -52,7 +52,7 @@ const props = defineProps<{
|
|||
|
||||
const scope = computed(() => props.path ? props.path.split('/') : []);
|
||||
|
||||
const keys = ref<any>(null);
|
||||
const keys = ref<[string, string][]>([]);
|
||||
|
||||
function fetchKeys() {
|
||||
misskeyApi('i/registry/keys-with-type', {
|
||||
|
|
|
@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
|
|||
|
||||
const props = defineProps<{
|
||||
game: Misskey.entities.ReversiGameDetailed;
|
||||
connection: Misskey.ChannelConnection;
|
||||
connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
|
||||
}>();
|
||||
|
||||
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
|
||||
|
@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
|
|||
game.value.user2Ready = states.user2;
|
||||
}
|
||||
|
||||
function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
|
||||
function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
|
||||
props.connection.send('updateSettings', {
|
||||
key: key,
|
||||
value: game.value[key],
|
||||
});
|
||||
}
|
||||
|
||||
function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) {
|
||||
function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
|
||||
if (userId === $i.id) return;
|
||||
if (game.value[key] === value) return;
|
||||
game.value[key] = value;
|
||||
|
|
|
@ -76,7 +76,11 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
|||
const parser = new Parser();
|
||||
let aiscript: Interpreter;
|
||||
const code = ref('');
|
||||
const logs = ref<any[]>([]);
|
||||
const logs = ref<{
|
||||
id: number;
|
||||
text: string;
|
||||
print: boolean;
|
||||
}[]>([]);
|
||||
const root = ref<AsUiRoot>();
|
||||
const components = ref<Ref<AsUiComponent>[]>([]);
|
||||
const uiKey = ref(0);
|
||||
|
|
|
@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
|
|||
const backupCodes = ref<string[]>();
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function tokenDone() {
|
||||
if (token.value == null) return;
|
||||
const res = await os.apiWithDialog('i/2fa/done', {
|
||||
token: token.value.toString(),
|
||||
token: typeof token.value === 'string' ? token.value : token.value.toString(),
|
||||
});
|
||||
|
||||
backupCodes.value = res.backupCodes;
|
||||
|
@ -166,7 +167,7 @@ function downloadBackupCodes() {
|
|||
}
|
||||
|
||||
function allDone() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ function createAccount() {
|
|||
});
|
||||
}
|
||||
|
||||
async function switchAccount(account: any) {
|
||||
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
||||
const fetchedAccounts = await getAccounts();
|
||||
const token = fetchedAccounts.find(x => x.id === account.id)!.token;
|
||||
switchAccountWithToken(token);
|
||||
|
|
|
@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import FormPagination from '@/components/MkPagination.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -77,7 +78,7 @@ const pagination = {
|
|||
|
||||
function revoke(token) {
|
||||
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||
list.value.reload();
|
||||
list.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ const decorationsForPreview = computed(() => {
|
|||
});
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
|
@ -120,7 +120,7 @@ async function update() {
|
|||
offsetX: offsetX.value,
|
||||
offsetY: offsetY.value,
|
||||
});
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function attach() {
|
||||
|
@ -130,12 +130,12 @@ async function attach() {
|
|||
offsetX: offsetX.value,
|
||||
offsetY: offsetY.value,
|
||||
});
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function detach() {
|
||||
emit('detach');
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
|
||||
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.tl">
|
||||
<MkTimeline
|
||||
ref="tlComponent"
|
||||
:key="src + withRenotes + withReplies + onlyFiles"
|
||||
:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
|
||||
:src="src.split(':')[0]"
|
||||
:list="src.split(':')[1]"
|
||||
:withRenotes="withRenotes"
|
||||
|
|
|
@ -257,7 +257,7 @@ function parallaxLoop() {
|
|||
}
|
||||
|
||||
function parallax() {
|
||||
const banner = bannerEl.value as any;
|
||||
const banner = bannerEl.value;
|
||||
if (banner == null) return;
|
||||
|
||||
const top = getScrollPosition(rootEl.value);
|
||||
|
|
|
@ -241,9 +241,13 @@ export class Storage<T extends StateDef> {
|
|||
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||
* 主にvue上で設定コントロールのmodelとして使う用
|
||||
*/
|
||||
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
|
||||
get: () => T[K]['default'];
|
||||
set: (value: T[K]['default']) => void;
|
||||
public makeGetterSetter<K extends keyof T, R = T[K]['default']>(
|
||||
key: K,
|
||||
getter?: (v: T[K]['default']) => R,
|
||||
setter?: (v: R) => T[K]['default'],
|
||||
): {
|
||||
get: () => R;
|
||||
set: (value: R) => void;
|
||||
} {
|
||||
const valueRef = ref(this.state[key]);
|
||||
|
||||
|
@ -265,7 +269,7 @@ export class Storage<T extends StateDef> {
|
|||
return valueRef.value;
|
||||
}
|
||||
},
|
||||
set: (value: unknown) => {
|
||||
set: (value) => {
|
||||
const val = setter ? setter(value) : value;
|
||||
this.set(key, val);
|
||||
valueRef.value = val;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { $i, iAmModerator } from '@/account.js';
|
|||
import MkLoading from '@/pages/_loading_.vue';
|
||||
import MkError from '@/pages/_error_.vue';
|
||||
|
||||
export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
|
||||
export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
|
||||
loader: loader,
|
||||
loadingComponent: MkLoading,
|
||||
errorComponent: MkError,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
|
||||
import { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js';
|
||||
|
||||
import type { App, ShallowRef } from 'vue';
|
||||
|
||||
|
@ -79,7 +79,7 @@ class MainRouterProxy implements IRouter {
|
|||
return this.supplier().currentRoute;
|
||||
}
|
||||
|
||||
get navHook(): ((path: string, flag?: any) => boolean) | null {
|
||||
get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
|
||||
return this.supplier().navHook;
|
||||
}
|
||||
|
||||
|
@ -91,11 +91,11 @@ class MainRouterProxy implements IRouter {
|
|||
return this.supplier().getCurrentKey();
|
||||
}
|
||||
|
||||
getCurrentPath(): any {
|
||||
getCurrentPath(): string {
|
||||
return this.supplier().getCurrentPath();
|
||||
}
|
||||
|
||||
push(path: string, flag?: any): void {
|
||||
push(path: string, flag?: RouterFlag): void {
|
||||
this.supplier().push(path, flag);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
|
||||
|
|
|
@ -3,22 +3,22 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
await defaultStore.ready;
|
||||
export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
|
||||
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
|
||||
|
||||
const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
|
||||
// navigator.platform may be deprecated but this check is still required
|
||||
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
||||
const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
|
||||
export const DEFAULT_DEVICE_KIND: DeviceKind = (
|
||||
isSmartphone
|
||||
? 'smartphone'
|
||||
: isTablet
|
||||
? 'tablet'
|
||||
: 'desktop'
|
||||
);
|
||||
|
||||
export const isFullscreenNotSupported = isIPhone || isIos;
|
||||
export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
|
||||
|
||||
export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind
|
||||
: isSmartphone ? 'smartphone'
|
||||
: isTablet ? 'tablet'
|
||||
: 'desktop';
|
||||
export function updateDeviceKind(kind: DeviceKind | null) {
|
||||
deviceKind = kind ?? DEFAULT_DEVICE_KIND;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean);
|
|||
export type FormItem = {
|
||||
label?: string;
|
||||
type: 'string';
|
||||
default: string | null;
|
||||
default?: string | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
|
@ -24,7 +24,7 @@ export type FormItem = {
|
|||
} | {
|
||||
label?: string;
|
||||
type: 'number';
|
||||
default: number | null;
|
||||
default?: number | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
|
@ -32,20 +32,20 @@ export type FormItem = {
|
|||
} | {
|
||||
label?: string;
|
||||
type: 'boolean';
|
||||
default: boolean | null;
|
||||
default?: boolean | null;
|
||||
description?: string;
|
||||
hidden?: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'enum';
|
||||
default: string | null;
|
||||
default?: string | null;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
enum: EnumItem[];
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'radio';
|
||||
default: unknown | null;
|
||||
default?: unknown | null;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
options: {
|
||||
|
@ -55,7 +55,7 @@ export type FormItem = {
|
|||
} | {
|
||||
label?: string;
|
||||
type: 'range';
|
||||
default: number | null;
|
||||
default?: number | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
step?: number;
|
||||
|
@ -66,12 +66,12 @@ export type FormItem = {
|
|||
} | {
|
||||
label?: string;
|
||||
type: 'object';
|
||||
default: Record<string, unknown> | null;
|
||||
default?: Record<string, unknown> | null;
|
||||
hidden: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'array';
|
||||
default: unknown[] | null;
|
||||
default?: unknown[] | null;
|
||||
hidden: Hidden;
|
||||
} | {
|
||||
type: 'button';
|
||||
|
|
46
packages/frontend/src/scripts/fullscreen.ts
Normal file
46
packages/frontend/src/scripts/fullscreen.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
|
||||
webkitEnterFullscreen?(): void;
|
||||
webkitExitFullscreen?(): void;
|
||||
};
|
||||
|
||||
type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
|
||||
|
||||
type RequestFullscreenProps = {
|
||||
readonly videoEl: VideoEl;
|
||||
readonly playerEl: PlayerEl;
|
||||
readonly options?: FullscreenOptions | null;
|
||||
};
|
||||
|
||||
type ExitFullscreenProps = {
|
||||
readonly videoEl: VideoEl;
|
||||
};
|
||||
|
||||
export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
|
||||
if (playerEl.requestFullscreen != null) {
|
||||
playerEl.requestFullscreen(options ?? undefined);
|
||||
return;
|
||||
}
|
||||
if (videoEl.webkitEnterFullscreen != null) {
|
||||
videoEl.webkitEnterFullscreen();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (document.exitFullscreen != null) {
|
||||
document.exitFullscreen();
|
||||
return;
|
||||
}
|
||||
if (videoEl.webkitExitFullscreen != null) {
|
||||
videoEl.webkitExitFullscreen();
|
||||
return;
|
||||
}
|
||||
};
|
|
@ -26,6 +26,7 @@ if (window.Cypress) {
|
|||
console.log('Cypress detected. It will use localStorage.');
|
||||
}
|
||||
|
||||
// Check for the availability of indexedDB.
|
||||
if (idbAvailable) {
|
||||
await iset('idb-test', 'test')
|
||||
.catch(err => {
|
||||
|
@ -37,16 +38,36 @@ if (idbAvailable) {
|
|||
console.error('indexedDB is unavailable. It will use localStorage.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from indexedDB (or localStorage as a fallback).
|
||||
*
|
||||
* @param key The key of the item to retrieve.
|
||||
*
|
||||
* @returns The value of the item.
|
||||
*/
|
||||
export async function get(key: string) {
|
||||
if (idbAvailable) return iget(key);
|
||||
return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in indexedDB (or localStorage as a fallback).
|
||||
*
|
||||
* @param {string} key - The key of the item to set.
|
||||
* @param {any} val - The value of the item to set.
|
||||
* @returns {Promise<void>} - A promise that resolves when the value has been set.`
|
||||
*/
|
||||
export async function set(key: string, val: any) {
|
||||
if (idbAvailable) return iset(key, val);
|
||||
return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a value from indexedDB (or localStorage as a fallback).
|
||||
*
|
||||
* @param {string} key - The key of the item to delete.
|
||||
* @returns {Promise<void>} - A promise that resolves when the value has been deleted.
|
||||
*/
|
||||
export async function del(key: string) {
|
||||
if (idbAvailable) return idel(key);
|
||||
return miLocalStorage.removeItem(`${PREFIX}${key}`);
|
||||
|
|
|
@ -17,7 +17,7 @@ export function misskeyApi<
|
|||
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||
>(
|
||||
endpoint: E,
|
||||
data: P = {} as any,
|
||||
data: P & { i?: string | null; } = {} as any,
|
||||
token?: string | null | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<_ResT> {
|
||||
|
@ -30,8 +30,8 @@ export function misskeyApi<
|
|||
|
||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||
// Append a credential
|
||||
if ($i) (data as any).i = $i.token;
|
||||
if (token !== undefined) (data as any).i = token;
|
||||
if ($i) data.i = $i.token;
|
||||
if (token !== undefined) data.i = token;
|
||||
|
||||
// Send request
|
||||
window.fetch(`${apiUrl}/${endpoint}`, {
|
||||
|
|
|
@ -80,7 +80,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
|
|||
});
|
||||
}
|
||||
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||
function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
||||
|
||||
|
@ -107,10 +107,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss
|
|||
});
|
||||
}
|
||||
|
||||
export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> {
|
||||
export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
|
||||
return select(src, label, false).then(files => files[0]);
|
||||
}
|
||||
|
||||
export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
|
||||
export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
|
||||
return select(src, label, true);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue