Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2024-02-07 16:16:06 +09:00
commit afb4380ba3
68 changed files with 1038 additions and 149 deletions

View file

@ -49,6 +49,12 @@
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735 - Enhance: MFMの属性でオートコンプリートが使用できるように #12735
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように - Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように - Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
- Enhance: リモートへの引用リノートと同一のリンクにはリンクプレビューを表示しないように
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなくリアクションピッカーなどから打てないように
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
- ロールが必要な絵文字をリアクションしようとした場合
- Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
@ -62,6 +68,11 @@
- Enhance: ページ遷移時にPlayerを閉じるように - Enhance: ページ遷移時にPlayerを閉じるように
- Fix: iOSで大きな画像を変換してアップロードできない問題を修正 - Fix: iOSで大きな画像を変換してアップロードできない問題を修正
- Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正 - Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正
- Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正
- Fix: 画像をクロップ時、正常に完了できない問題の修正
- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正
- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正
- Fix: エラー画像URLを設定した後解除するとデフォルトの画像が表示されない問題の修正
### Server ### Server
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました

View file

@ -130,6 +130,7 @@ overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats"
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir." reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes" rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
attachCancel: "Eliminar el fitxer adjunt" attachCancel: "Eliminar el fitxer adjunt"
deleteFile: "Esborrar l'arxiu "
markAsSensitive: "Marcar com a NSFW" markAsSensitive: "Marcar com a NSFW"
unmarkAsSensitive: "Deixar de marcar com a sensible" unmarkAsSensitive: "Deixar de marcar com a sensible"
enterFileName: "Defineix nom del fitxer" enterFileName: "Defineix nom del fitxer"
@ -1039,6 +1040,12 @@ rolesAssignedToMe: "Rols assignats "
resetPasswordConfirm: "Vols canviar la teva contrasenya?" resetPasswordConfirm: "Vols canviar la teva contrasenya?"
sensitiveWords: "Paraules sensibles" sensitiveWords: "Paraules sensibles"
sensitiveWordsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves." sensitiveWordsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves."
sensitiveWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular."
hiddenTags: "Etiquetes ocultes"
hiddenTagsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves."
notesSearchNotAvailable: "La cerca de notes no es troba disponible."
license: "Llicència"
unfavoriteConfirm: "Esborrar dels favorits?"
myClips: "Els meus retalls" myClips: "Els meus retalls"
drivecleaner: "Netejador de Disc" drivecleaner: "Netejador de Disc"
retryAllQueuesNow: "Prova de nou d'executar totes les cues" retryAllQueuesNow: "Prova de nou d'executar totes les cues"
@ -1048,6 +1055,14 @@ enableChartsForRemoteUser: "Generar gràfiques d'usuaris remots"
enableChartsForFederatedInstances: "Generar gràfiques d'instàncies remotes" enableChartsForFederatedInstances: "Generar gràfiques d'instàncies remotes"
showClipButtonInNoteFooter: "Afegir \"Retall\" al menú d'acció de la nota" showClipButtonInNoteFooter: "Afegir \"Retall\" al menú d'acció de la nota"
reactionsDisplaySize: "Mida de les reaccions" reactionsDisplaySize: "Mida de les reaccions"
limitWidthOfReaction: "Limitar l'amplada màxima de la reacció i mostrar-les en una mida reduïda "
noteIdOrUrl: "ID o URL de la nota"
video: "Vídeo"
videos: "Vídeos "
audio: "So"
audioFiles: "So"
dataSaver: "Economitzador de dades"
accountMigration: "Migració del compte"
accountMoved: "Aquest usuari té un compte nou:" accountMoved: "Aquest usuari té un compte nou:"
accountMovedShort: "Aquest compte ha sigut migrat" accountMovedShort: "Aquest compte ha sigut migrat"
operationForbidden: "Operació no permesa " operationForbidden: "Operació no permesa "
@ -1098,9 +1113,56 @@ branding: "Marca"
enableServerMachineStats: "Publicar estadístiques del maquinari del servidor" enableServerMachineStats: "Publicar estadístiques del maquinari del servidor"
enableIdenticonGeneration: "Activar la generació d'icones d'identificació " enableIdenticonGeneration: "Activar la generació d'icones d'identificació "
turnOffToImprovePerformance: "Desactivant aquesta opció es pot millorar el rendiment." turnOffToImprovePerformance: "Desactivant aquesta opció es pot millorar el rendiment."
createInviteCode: "Crear codi d'invitació "
createWithOptions: "Crear invitació amb opcions"
createCount: "Comptador d'invitacions "
inviteCodeCreated: "Invitació creada"
inviteLimitExceeded: "Has sobrepassat el límit d'invitacions que pots crear."
createLimitRemaining: "Et queden {limit} invitacions restants"
inviteLimitResetCycle: "Cada {time} {limit} invitacions."
expirationDate: "Data de venciment"
noExpirationDate: "Sense data de venciment"
inviteCodeUsedAt: "Codi d'invitació fet servir el"
registeredUserUsingInviteCode: "Codi d'invitació fet servir per l'usuari "
waitingForMailAuth: "Esperant la verificació per correu electrònic "
inviteCodeCreator: "Invitació creada per"
usedAt: "Utilitzada el"
unused: "Sense utilitzar"
used: "Utilitzada"
expired: "Caducat"
doYouAgree: "Estàs d'acord?"
beSureToReadThisAsItIsImportant: "Llegeix això perquè és molt important."
iHaveReadXCarefullyAndAgree: "He llegit {x} i estic d'acord."
dialog: "Diàleg "
icon: "Icona" icon: "Icona"
forYou: "Per a tu"
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: "Respondre" replies: "Respondre"
renotes: "Impulsa" renotes: "Impulsa"
loadReplies: "Mostrar les respostes"
loadConversation: "Mostrar la conversació "
pinnedList: "Llista fixada"
keepScreenOn: "Mantenir la pantalla encesa"
verifiedLink: "La propietat de l'enllaç ha sigut verificada"
notifyNotes: "Notificar quan hi hagi notes noves"
unnotifyNotes: "Deixar de notificar quan hi hagi notes noves"
authentication: "Autenticació "
authenticationRequiredToContinue: "Si us plau autentificat per continuar"
dateAndTime: "Data i hora"
showRenotes: "Mostrar impulsos"
edited: "Editat"
notificationRecieveConfig: "Paràmetres de notificacions"
mutualFollow: "Seguidor mutu"
fileAttachedOnly: "Només notes amb adjunts"
showRepliesToOthersInTimeline: "Mostrar les respostes a altres a la línia de temps"
hideRepliesToOthersInTimeline: "Amagar les respostes a altres a la línia de temps"
showRepliesToOthersInTimelineAll: "Mostrar les respostes a altres a usuaris que segueixes a la línia de temps"
hideRepliesToOthersInTimelineAll: "Ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps"
confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les teves respostes a tots els que segueixes a la teva línia de temps?"
confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?"
externalServices: "Serveis externs" externalServices: "Serveis externs"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Adreça URL impressum" impressumUrl: "Adreça URL impressum"
@ -1131,7 +1193,25 @@ seasonalScreenEffect: "Efectes de pantalla segons les estacions"
decorate: "Decorar" decorate: "Decorar"
addMfmFunction: "Afegeix funcions MFM" addMfmFunction: "Afegeix funcions MFM"
enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions MFM" enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions MFM"
bubbleGame: "Bubble Game"
sfx: "Efectes de so"
soundWillBePlayed: "Es reproduiran efectes de so"
showReplay: "Veure reproducció"
replay: "Reproduir"
replaying: "Reproduint"
ranking: "Classificació"
lastNDays: "Últims {n} dies" lastNDays: "Últims {n} dies"
backToTitle: "Torna al títol"
hemisphere: "Geolocalització"
withSensitive: "Incloure notes amb fitxers sensibles"
userSaysSomethingSensitive: "La publicació de {name} conte material sensible"
enableHorizontalSwipe: "Lliscar per canviar de pestanya"
_bubbleGame:
howToPlay: "Com es juga"
_howToPlay:
section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa."
section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts."
section3: "El joc s'acabarà quan els objectes sobresurtin de la caixa. Intenta aconseguir la puntuació més gran possible fusionant objectes mentre impedeixes que sobresurtin de la caixa!"
_announcement: _announcement:
forExistingUsers: "Anunci per usuaris registrats" forExistingUsers: "Anunci per usuaris registrats"
forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació." forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació."
@ -1153,8 +1233,318 @@ _initialAccountSetting:
privacySetting: "Configuració de seguretat" privacySetting: "Configuració de seguretat"
theseSettingsCanEditLater: "Aquests ajustos es poden canviar més tard." theseSettingsCanEditLater: "Aquests ajustos es poden canviar més tard."
youCanEditMoreSettingsInSettingsPageLater: "A més d'això, es poden fer diferents configuracions a través de la pàgina de configuració. Assegureu-vos de comprovar-ho més tard." youCanEditMoreSettingsInSettingsPageLater: "A més d'això, es poden fer diferents configuracions a través de la pàgina de configuració. Assegureu-vos de comprovar-ho més tard."
followUsers: "Prova de seguir usuaris que t'interessin per construir la teva línia de temps."
pushNotificationDescription: "Activant les notificacions emergents et permetrà rebre notificacions de {name} directament al teu dispositiu."
initialAccountSettingCompleted: "Configuració del perfil completada!"
haveFun: "Disfruta {name}!"
youCanContinueTutorial: "Pots continuar amb un tutorial per aprendre a Fer servir {name} (CherryPick) o tu pots estalviar i començar a fer-lo servir ja."
startTutorial: "Començar el tutorial"
skipAreYouSure: "Et vols saltar la configuració del perfil?"
laterAreYouSure: "Vols continuar la configuració del perfil més tard?"
_initialTutorial:
launchTutorial: "Començar tutorial"
title: "Tutorial"
wellDone: "Ben fet!"
skipAreYouSure: "Sortir del tutorial?"
_landing:
title: "Benvingut al tutorial"
description: "Aquí aprendràs el bàsic per poder fer servir CherryPick i les seves característiques."
_note:
title: "Què és una Nota?"
description: "Les publicacions a CherryPick es diuen 'Notes'. Les Notes s'ordenen cronològicament a la línia de temps i s'actualitzen de forma automàtica."
reply: "Fes clic en aquest botó per contestar a un missatge. També és possible contestar a una contestació, continuant la conversació en forma de fil."
renote: "Pots compartir una Nota a la teva pròpia línia de temps. Inclús pots citar-les amb els teus comentaris."
reaction: "Pots afegir reaccions a les Notes. Entrarem més en detall a la pròxima pàgina."
menu: "Pots veure els detalls de les Notes, copiar enllaços i fer diferents accions."
_reaction:
title: "Què són les Reaccions?"
description: "Es poden reaccionar a les Notes amb diferents emoticones. Les reaccions et permeten expressar matisos que hi són més enllà d'un simple m'agrada."
letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!"
reactToContinue: "Afegeix una reacció per continuar."
reactNotification: "Rebràs notificacions en temps real quan un usuari reaccioni a les teves notes."
reactDone: "Pots desfer una reacció fent clic al botó '-'."
_timeline:
title: "El concepte de les línies de temps"
description1: "CherryPick mostra diferents línies de temps basades en l'ús (algunes poden no estar disponibles depenent de la política del servidor)"
home: "Pots veure notes dels comptes que segueixes"
local: "Pots veure les notes dels usuaris del servidor."
social: "Es mostren les notes de les línies de temps d'Inici i Local."
global: "Pots veure les notes de tots els servidors connectats."
description2: "Pots canviar la línia de temps en qualsevol moment fent servir la barra de la pantalla superior."
description3: "A més hi ha línies de temps per llistes i per canals. Si vols saber més {link}."
_postNote:
title: "Configuració de la publicació de les notes"
description1: "Quan públiques una nota a CherryPick hi ha diferents opcions disponibles. El formulari de publicació es veu així"
_visibility:
description: "Pots limitar qui pot veure les teves notes."
public: "La teva nota serà visible per a tots els usuaris."
home: "Publicar només a línia de temps d'Inici. La gent que visiti el teu perfil o mitjançant les remotes també la podran veure."
followers: "Només visible per a seguidors. Només els teus seguidors la podran veure i ningú més. Ningú més podrà fer renotes."
direct: "Només visible per a alguns seguidors, el destinatari rebre una notificació. Es pot fer servir com una alternativa als missatges directes."
doNotSendConfidencialOnDirect1: "Tingues cura quan enviïs informació sensible."
doNotSendConfidencialOnDirect2: "Els administradors del servidor poden veure tot el que escrius. Ves compte quan enviïs informació sensible en enviar notes directes a altres usuaris en servidors de poca confiança."
localOnly: "Publicar amb aquesta opció activada farà que la nota no federi amb altres servidors. Els usuaris d'altres servidors no podran veure la nota directament, sense importar les opcions de visualització."
_cw:
title: "Avís de Contingut (CW)"
description: "En comptes del cos de la nota es mostrarà el que s'escrigui al camp de 'comentaris'. Fent clic a 'Llegir més' es mostrarà el cos."
_exampleNote:
cw: "Això et farà venir gana!"
note: "Acabo de menjar-me un donut de xocolata 🍩😋"
useCases: "Això es fa servir per seguir normes del servidor sobre certes notes o per ocultar contingut sensible O revelador."
_howToMakeAttachmentsSensitive:
title: "Com marcar adjunts com a contingut sensible?"
description: "Per adjunts que sigui requerit per les normes del servidor o que puguin contenir material sensible, s'ha d'afegir l'opció 'sensible'."
tryThisFile: "Prova de marcar la imatge adjunta en aquest formulari com a sensible!"
_exampleNote:
note: "Oops! L'he fet bona en obrir la tapa de Nocilla..."
method: "Per marcar un adjunt com a sensible, fes clic a la miniatura de l'adjunt, obre el menú i fes clic a 'Marcar com a sensible'."
sensitiveSucceeded: "Quan adjuntis fitxers si us plau marca la sensibilitat seguint les normes del servidor."
doItToContinue: "Marca el fitxer adjunt com a sensible per poder continuar."
_done:
title: "Has completat el tutorial 🎉"
description: "Les funcions explicades aquí és una petita mostra. Per una explicació més detallada de com fer servir CherryPick consulta {link}."
_timelineDescription:
home: "A la línia de temps d'Inici pots veure les notes dels usuaris que segueixes."
local: "A la línia de temps Local pots veure les notes de tots els usuaris d'aquest servidor."
social: "La línia de temps Social mostren les notes de les línies de temps d'Inici i Local."
global: "A la línia de temps Global pots veure les notes de tots els servidors connectats."
_serverRules:
description: "Un conjunt de regles que seran mostrades abans de registrar-se. Es recomanable configurar un resum dels termes d'ús."
_serverSettings:
iconUrl: "URL de la icona"
appIconDescription: "Especifica la icona que es mostrarà quan el {host} es mostri en una aplicació."
appIconUsageExample: "Per exemple com a PWA, o quan es mostri com un favorit a la pàgina d'inici del telèfon mòbil"
appIconStyleRecommendation: "Com la icona pot ser retallada com un cercle o un quadrat, es recomana fer servir una icona amb un marge acolorit que l'envolti."
appIconResolutionMustBe: "La resolució mínima és {resolution}."
manifestJsonOverride: "Sobreescriure manifest.json"
shortName: "Nom curt"
shortNameDescription: "Una abreviatura del nom de la instància que es poguí mostrar en cas que el nom oficial sigui massa llarg"
fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les línies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat."
fanoutTimelineDbFallback: "Carregar de la base de dades"
fanoutTimelineDbFallbackDescription: "Quan s'activa, la línia de temps fa servir la base de dades per consultes adicionals si la línia de temps no es troba a la memòria cau. Si és desactiva la càrrega del servidor és veure reduïda, però també és reduirà el nombre de línies de temps que és poden obtenir."
_accountMigration:
moveFrom: "Migrar un altre compte a aquest"
moveFromSub: "Crear un àlies per un altre compte"
moveFromLabel: "Compte original #{n}"
moveFromDescription: "Has de crear un àlies del compte que vols migrar en aquest compte.\nFes servir aquest format per posar el compte que vols migrar: @nomusuari@servidor.exemple.com\nPer esborrar l'àlies deixa el camp en blanc (no és recomanable de fer)"
moveTo: "Migrar aquest compte a un altre"
moveToLabel: "Compte al qual es vol migrar:"
moveCannotBeUndone: "Les migracions dels comptes no es poden desfer."
moveAccountDescription: "Això migrarà la teva compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a CherryPick v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)"
moveAccountHowTo: "Per fer la migració, primer has de crear un àlies per aquest compte al compte al qual vols migrar.\nDesprés de crear l'àlies, introdueix el compte al qual vols migrar amb el format següent: @nomusuari@servidor.exemple.com"
startMigration: "Migrar"
migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podràs tornar a fer servir aquest compte mai més."
movedAndCannotBeUndone: "Aquest compte ha migrat.\nLes migracions no es poden desfer."
postMigrationNote: "Aquest compte deixarà de seguir tots els comptes que segueix 24 hores després de germinar la migració.\nEl nombre de seguidors i seguits passarà a ser de zero. Per evitar que els teus seguidors no puguin veure les publicacions marcades com a només seguidors continuaren seguint aquest compte."
movedTo: "Nou compte:"
_achievements:
earnedAt: "Desbloquejat el"
_types:
_notes1:
title: "Aquí, configurant el meu msky"
description: "Publica la teva primera Nota"
flavor: "Passa-t'ho bé fent servir Miskey!"
_notes10:
title: "Algunes notes"
description: "Publica 10 notes"
_notes100:
title: "Un piló de notes"
description: "Publica 100 notes"
_notes500:
title: "Cobert de notes"
description: "Publica 500 notes"
_notes1000:
title: "Un piló de notes"
description: "1 000 notes publicades"
_notes5000:
title: "Desbordament de notes"
description: "5 000 notes publicades"
_notes10000:
title: "Supernota"
description: "10 000 notes publicades"
_notes20000:
title: "Necessito... Més... Notes!"
description: "20 000 notes publicades"
_notes30000:
title: "Notes notes notes!"
description: "30 000 notes publicades"
_notes40000:
title: "Fàbrica de notes"
description: "40 000 notes publicades"
_notes50000:
title: "Planeta de notes"
description: "50 000 notes publicades"
_notes60000:
title: "Quàsar de notes"
description: "60 000 notes publicades"
_notes70000:
title: "Forat negre de notes"
description: "70 000 notes publicades"
_notes80000:
title: "Galàxia de notes"
description: "80 000 notes publicades"
_notes90000:
title: "Univers de notes"
description: "90 000 notes publicades"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "100 000 notes publicades"
flavor: "Segur que tens moltes coses a dir?"
_login3:
title: "Principiant I"
description: "Vas iniciar sessió fa tres dies"
flavor: "Des d'avui diguem Misskist"
_login7:
title: "Principiant II"
description: "Vas iniciar sessió fa set dies"
flavor: "Ja saps com va funcionant tot?"
_login15:
title: "Principiant III"
description: "Vas iniciar sessió fa quinze dies"
_login30:
title: "Misskist I"
description: "Vas iniciar sessió fa trenta dies"
_login60:
title: "Misskist II"
description: "Vas iniciar sessió fa seixanta dies"
_login100:
title: "Misskist III"
description: "Vas iniciar sessió fa cent dies"
flavor: "Misskist violent"
_login200:
title: "Regular I"
description: "Vas iniciar sessió fa dos-cents dies"
_login300:
title: "Regular II"
description: "Vas iniciar sessió fa tres-cents dies"
_login400:
title: "Regular III"
description: "Vas iniciar sessió fa quatre-cents dies"
_login500:
title: "Expert I"
description: "Vas iniciar sessió fa cinc-cents dies"
flavor: "Amics, he dit massa vegades que soc un amant de les notes"
_login600:
title: "Expert II"
description: "Vas iniciar sessió fa sis-cents dies"
_login700:
title: "Expert III"
description: "Vas iniciar sessió fa set-cents dies"
_login800:
title: "Mestre de les Notes I"
description: "Vas iniciar sessió fa vuit-cents dies "
_login900:
title: "Mestre de les Notes II"
description: "Vas iniciar sessió fa nou-cents dies"
_login1000:
title: "Mestre de les Notes III"
description: "Vas iniciar sessió fa mil dies"
flavor: "Gràcies per fer servir CherryPick!"
_noteClipped1:
title: "He de retallar-te!"
description: "Retalla la teva primera nota"
_noteFavorited1:
title: "Quan miro les estrelles"
description: "La primera vegada que vaig registrar el meu favorit"
_myNoteFavorited1:
title: "Vull una estrella"
description: "La meva nota va ser registrada com favorita per una de les altres persones"
_profileFilled:
title: "Estic a punt"
description: "Vaig fer la configuració de perfil"
_markedAsCat:
title: "Soc un gat"
description: "He establert el meu compte com si fos un Gat"
flavor: "Encara no tinc nom"
_following1:
title: "És el meu primer seguiment"
description: "És la primera vegada que et segueixo"
_following10:
title: "Segueix-me... Segueix-me..."
description: "Seguir 10 usuaris"
_following50:
title: "Molts amics"
description: "Seguir 50 comptes"
_following100:
title: "100 amics"
description: "Segueixes 100 comptes"
_following300:
title: "Sobrecàrrega d'amics"
description: "Segueixes 300 comptes"
_followers1:
title: "Primer seguidor"
description: "1 seguidor guanyat"
_followers10:
title: "Segueix-me!"
description: "10 seguidors guanyats"
_followers50:
title: "Venen en manada"
description: "50 seguidors guanyats"
_followers100:
title: "Popular"
description: "100 seguidors guanyats"
_followers300:
title: "Si us plau, d'un en un!"
description: "300 seguidors guanyats"
_followers500:
title: "Torre de ràdio"
description: "500 seguidors guanyats"
_followers1000:
title: "Influenciador"
description: "1 000 seguidors guanyats"
_collectAchievements30:
title: "Col·leccionista d'èxits "
description: "Desbloqueja 30 assoliments"
_viewAchievements3min:
title: "M'agraden els èxits "
description: "Mira la teva llista d'assoliments durant més de 3 minuts"
_iLoveMisskey:
title: "Estimo CherryPick"
description: "Publica \"I ❤ #CherryPick\""
flavor: "L'equip de desenvolupament de CherryPick agraeix el vostre suport!"
_foundTreasure:
title: "A la Recerca del Tresor"
description: "Has trobat el tresor amagat"
_client30min:
title: "Parem una estona"
description: "Mantingues obert CherryPick per 30 minuts"
_client60min:
title: "A totes amb CherryPick"
description: "Mantingues CherryPick obert per 60 minuts"
_noteDeletedWithin1min:
title: "No et preocupis"
description: "Esborra una nota al minut de publicar-la"
_postedAtLateNight:
title: "Nocturn"
description: "Publica una nota a altes hores de la nit "
flavor: "És hora d'anar a dormir."
_open3windows:
title: "Multi finestres"
description: "I va obrir més de tres finestres"
_driveFolderCircularReference:
title: "Consulteu la secció de bucle"
_role: _role:
permission: "Permisos de rol"
descriptionOfPermission: "Els <b>Moderadors</b> poden fer operacions bàsiques de moderació.\nEls <b>Administradors</b> poden canviar tots els ajustos del servidor."
assignTarget: "Assignar " assignTarget: "Assignar "
descriptionOfAssignTarget: "<b>Manual</b> per canviar manualment qui és part d'aquest rol i qui no.\n<b>Condicional</b> per afegir o eliminar de manera automàtica els usuaris d'aquest rol basat en una determinada condició."
manual: "Manual"
manualRoles: "Rols manuals"
conditional: "Condicional"
conditionalRoles: "Rols condicionals"
condition: "Condició"
isConditionalRole: "Aquest és un rol condicional"
isPublic: "Rol públic"
descriptionOfIsPublic: "Aquest rol es mostrarà al perfil dels usuaris al que se'ls assigni."
options: "Opcions"
policies: "Polítiques"
baseRole: "Plantilla de rols"
useBaseValue: "Fer servir els valors de la plantilla de rols"
chooseRoleToAssign: "Selecciona els rols a assignar"
iconUrl: "URL de la icona "
asBadge: "Mostrar com a insígnia "
descriptionOfAsBadge: "La icona d'aquest rol es mostrarà al costat dels noms d'usuaris que tinguin assignats aquest rol."
isExplorable: "Fer el rol explorable"
priority: "Prioritat" priority: "Prioritat"
_priority: _priority:
low: "Baixa" low: "Baixa"
@ -1275,6 +1665,7 @@ _webhookSettings:
_moderationLogTypes: _moderationLogTypes:
suspend: "Suspèn" suspend: "Suspèn"
resetPassword: "Restableix la contrasenya" resetPassword: "Restableix la contrasenya"
createInvitation: "Crear codi d'invitació "
_reversi: _reversi:
total: "Total" total: "Total"

View file

@ -122,7 +122,10 @@ add: "Hinzufügen"
reaction: "Reaktionen" reaction: "Reaktionen"
reactions: "Reaktionen" reactions: "Reaktionen"
emojiPicker: "Emoji auswählen" emojiPicker: "Emoji auswählen"
pinnedEmojisForReactionSettingDescription: "Wähle die Emojis aus, um sie an zu pinnen" pinnedEmojisForReactionSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie beim Reagieren als Erstes anzuzeigen."
pinnedEmojisSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie in der Emoji-Auswahl als Erstes anzuzeigen"
overwriteFromPinnedEmojisForReaction: "Überschreiben mit den Reaktions-Einstellungen"
overwriteFromPinnedEmojis: "Überschreiben mit den allgemeinen Einstellungen"
reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen" reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen"
rememberNoteVisibility: "Notizsichtbarkeit merken" rememberNoteVisibility: "Notizsichtbarkeit merken"
attachCancel: "Anhang entfernen" attachCancel: "Anhang entfernen"
@ -263,6 +266,7 @@ removed: "Erfolgreich gelöscht"
removeAreYouSure: "Möchtest du „{x}“ wirklich entfernen?" removeAreYouSure: "Möchtest du „{x}“ wirklich entfernen?"
deleteAreYouSure: "Möchtest du „{x}“ wirklich löschen?" deleteAreYouSure: "Möchtest du „{x}“ wirklich löschen?"
resetAreYouSure: "Wirklich zurücksetzen?" resetAreYouSure: "Wirklich zurücksetzen?"
areYouSure: "Bist du sicher?"
saved: "Erfolgreich gespeichert" saved: "Erfolgreich gespeichert"
messaging: "Chat" messaging: "Chat"
upload: "Hochladen" upload: "Hochladen"
@ -357,7 +361,7 @@ enableLocalTimeline: "Lokale Chronik aktivieren"
enableGlobalTimeline: "Globale Chronik aktivieren" enableGlobalTimeline: "Globale Chronik aktivieren"
disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind." disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind."
registration: "Registrieren" registration: "Registrieren"
enableRegistration: "Registration neuer Benutzer erlauben" enableRegistration: "Registrierung neuer Benutzer erlauben"
invite: "Einladen" invite: "Einladen"
driveCapacityPerLocalAccount: "Drive-Kapazität pro lokalem Benutzerkonto" driveCapacityPerLocalAccount: "Drive-Kapazität pro lokalem Benutzerkonto"
driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer fremder Instanzen" driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer fremder Instanzen"
@ -375,8 +379,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptcha aktivieren" enableHcaptcha: "hCaptcha aktivieren"
hcaptchaSiteKey: "Site key" hcaptchaSiteKey: "Site key"
hcaptchaSecretKey: "Secret key" hcaptchaSecretKey: "Secret key"
mcaptcha: "mCaptcha"
enableMcaptcha: "mCaptcha aktivieren"
mcaptchaSiteKey: "Site key" mcaptchaSiteKey: "Site key"
mcaptchaSecretKey: "Secret key" mcaptchaSecretKey: "Secret key"
mcaptchaInstanceUrl: "mCaptcha Instanz-URL"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHA aktivieren" enableRecaptcha: "reCAPTCHA aktivieren"
recaptchaSiteKey: "Site key" recaptchaSiteKey: "Site key"
@ -434,7 +441,7 @@ lastUsed: "Zuletzt benutzt"
lastUsedAt: "Zuletzt verwendet: {t}" lastUsedAt: "Zuletzt verwendet: {t}"
unregister: "Deaktivieren" unregister: "Deaktivieren"
passwordLessLogin: "Passwortloses Anmelden" passwordLessLogin: "Passwortloses Anmelden"
passwordLessLoginDescription: "Ermöglicht passwortfreies Einloggen, nur via Security-Token oder Passkey" passwordLessLoginDescription: "Ermöglicht passwortloses Einloggen mit einem Security-Token oder Passkey"
resetPassword: "Passwort zurücksetzen" resetPassword: "Passwort zurücksetzen"
newPasswordIs: "Das neue Passwort ist „{password}“" newPasswordIs: "Das neue Passwort ist „{password}“"
reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren" reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren"
@ -1185,6 +1192,9 @@ signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen.
cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden." cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
doReaction: "Reagieren" doReaction: "Reagieren"
code: "Code" code: "Code"
decorate: "Dekorieren"
addMfmFunction: "MFM hinzufügen"
sfx: "Soundeffekte"
lastNDays: "Letzten {n} Tage" lastNDays: "Letzten {n} Tage"
_announcement: _announcement:
forExistingUsers: "Nur für existierende Nutzer" forExistingUsers: "Nur für existierende Nutzer"
@ -1225,6 +1235,24 @@ _initialTutorial:
description: "Hier kannst du sehen, wie CherryPick funktioniert" description: "Hier kannst du sehen, wie CherryPick funktioniert"
_note: _note:
title: "Was sind Notizen?" title: "Was sind Notizen?"
description: "Beiträge auf CherryPick heißen \"Notizen\". Notizen werden chronologisch in der Chronik angeordnet und in Echtzeit aktualisiert."
reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen."
_reaction:
title: "Was sind Reaktionen?"
reactToContinue: "Füge eine Reaktion hinzu, um fortzufahren."
reactNotification: "Du erhältst Echtzeit-Benachrichtigungen, wenn jemand auf deine Notiz reagiert."
_postNote:
_visibility:
description: "Du kannst einschränken, wer deine Notiz sehen kann."
public: "Deine Notiz wird für alle Nutzer sichtbar sein."
doNotSendConfidencialOnDirect1: "Sei vorsichtig, wenn du sensible Informationen verschickst!"
_cw:
title: "Inhaltswarnung"
_done:
title: "Du hast das Tutorial abgeschlossen! 🎉"
_timelineDescription:
local: "In der lokalen Chronik siehst du Notizen von allen Benutzern auf diesem Server."
global: "In der globalen Chronik siehst du Notizen von allen föderierten Servern."
_serverRules: _serverRules:
description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen." description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen."
_serverSettings: _serverSettings:
@ -1238,6 +1266,7 @@ _serverSettings:
shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist." shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist."
fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden." fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden."
fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen" fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen"
fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. "
_accountMigration: _accountMigration:
moveFrom: "Von einem anderen Konto zu diesem migrieren" moveFrom: "Von einem anderen Konto zu diesem migrieren"
moveFromSub: "Alias für ein anderes Konto erstellen" moveFromSub: "Alias für ein anderes Konto erstellen"
@ -1498,6 +1527,8 @@ _achievements:
_smashTestNotificationButton: _smashTestNotificationButton:
title: "Testüberfluss" title: "Testüberfluss"
description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne" description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne"
_tutorialCompleted:
description: "Tutorial abgeschlossen"
_role: _role:
new: "Rolle erstellen" new: "Rolle erstellen"
edit: "Rolle bearbeiten" edit: "Rolle bearbeiten"
@ -1553,8 +1584,8 @@ _role:
webhookMax: "Maximale Anzahl an Webhooks" webhookMax: "Maximale Anzahl an Webhooks"
clipMax: "Maximale Anzahl an Clips" clipMax: "Maximale Anzahl an Clips"
noteEachClipsMax: "Maximale Anzahl an Notizen innerhalb eines Clips" noteEachClipsMax: "Maximale Anzahl an Notizen innerhalb eines Clips"
userListMax: "Maximale Anzahl an Benutzern in einer Benutzerliste" userListMax: "Maximale Anzahl an Benutzerlisten"
userEachUserListsMax: "Maximale Anzahl an Benutzerlisten" userEachUserListsMax: "Maximale Anzahl an Benutzern in einer Benutzerliste"
rateLimitFactor: "Versuchsanzahl" rateLimitFactor: "Versuchsanzahl"
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
canHideAds: "Kann Werbung ausblenden" canHideAds: "Kann Werbung ausblenden"
@ -2340,5 +2371,9 @@ _externalResourceInstaller:
title: "Das Farbschema konnte nicht installiert werden" title: "Das Farbschema konnte nicht installiert werden"
description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden." description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
_reversi: _reversi:
blackOrWhite: "Schwarz/Weiß"
rules: "Regeln"
black: "Schwarz"
white: "Weiß"
total: "Gesamt" total: "Gesamt"

View file

@ -749,7 +749,7 @@ smtpHost: "Host"
smtpPort: "Port" smtpPort: "Port"
smtpUser: "Username" smtpUser: "Username"
smtpPass: "Password" smtpPass: "Password"
emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP verification" emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP authentication"
smtpSecure: "Use implicit SSL/TLS for SMTP connections" smtpSecure: "Use implicit SSL/TLS for SMTP connections"
smtpSecureInfo: "Turn this off when using STARTTLS" smtpSecureInfo: "Turn this off when using STARTTLS"
testEmail: "Test email delivery" testEmail: "Test email delivery"

View file

@ -399,7 +399,7 @@ antennaKeywords: "Mots clés à recevoir"
antennaExcludeKeywords: "Mots clés à exclure" antennaExcludeKeywords: "Mots clés à exclure"
antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR."
notifyAntenna: "Me notifier pour les nouvelles notes" notifyAntenna: "Me notifier pour les nouvelles notes"
withFileAntenna: "Notes ayant des attachements uniquement" withFileAntenna: "Notes ayant des fichiers joints uniquement"
enableServiceworker: "Activer ServiceWorker" enableServiceworker: "Activer ServiceWorker"
antennaUsersDescription: "Saisissez un seul nom dutilisateur·rice par ligne" antennaUsersDescription: "Saisissez un seul nom dutilisateur·rice par ligne"
caseSensitive: "Sensible à la casse" caseSensitive: "Sensible à la casse"

View file

@ -102,7 +102,7 @@ defaultNoteVisibility: "Privacy predefinita delle note"
follow: "Segui" follow: "Segui"
followRequest: "Richiesta di follow" followRequest: "Richiesta di follow"
followRequests: "Richieste di follow" followRequests: "Richieste di follow"
unfollow: "Interrompi following" unfollow: "Smetti di seguire"
followRequestPending: "Richiesta in approvazione" followRequestPending: "Richiesta in approvazione"
enterEmoji: "Inserisci emoji" enterEmoji: "Inserisci emoji"
renote: "Rinota" renote: "Rinota"
@ -380,9 +380,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "Abilita hCaptcha" enableHcaptcha: "Abilita hCaptcha"
hcaptchaSiteKey: "Chiave del sito" hcaptchaSiteKey: "Chiave del sito"
hcaptchaSecretKey: "Chiave segreta" hcaptchaSecretKey: "Chiave segreta"
mcaptcha: "mCaptcha"
enableMcaptcha: "Abilita hCaptcha" enableMcaptcha: "Abilita hCaptcha"
mcaptchaSiteKey: "Chiave del sito" mcaptchaSiteKey: "Chiave del sito"
mcaptchaSecretKey: "Chiave segreta" mcaptchaSecretKey: "Chiave segreta"
mcaptchaInstanceUrl: "URL della istanza mCaptcha"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "Abilita reCAPTCHA" enableRecaptcha: "Abilita reCAPTCHA"
recaptchaSiteKey: "Chiave del sito" recaptchaSiteKey: "Chiave del sito"
@ -642,6 +644,7 @@ medium: "Medio"
small: "Piccolo" small: "Piccolo"
generateAccessToken: "Genera token di accesso" generateAccessToken: "Genera token di accesso"
permission: "Autorizzazioni " permission: "Autorizzazioni "
adminPermission: "Privilegi amministrativi"
enableAll: "Abilita tutto" enableAll: "Abilita tutto"
disableAll: "Disabilita tutto" disableAll: "Disabilita tutto"
tokenRequested: "Autorizza accesso al profilo" tokenRequested: "Autorizza accesso al profilo"
@ -685,6 +688,7 @@ useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifi
other: "Ulteriori" other: "Ulteriori"
regenerateLoginToken: "Genera di nuovo un token di connessione" regenerateLoginToken: "Genera di nuovo un token di connessione"
regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi." regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi."
theKeywordWhenSearchingForCustomEmoji: "Questa sarà la parola chiave durante la ricerca di emoji personalizzate"
setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi." setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi."
fileIdOrUrl: "ID o URL del file" fileIdOrUrl: "ID o URL del file"
behavior: "Comportamento" behavior: "Comportamento"
@ -881,7 +885,7 @@ pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente" lastCommunication: "La comunicazione più recente"
resolved: "Risolto" resolved: "Risolto"
unresolved: "Non risolto" unresolved: "Non risolto"
breakFollow: "Interrompi follow" breakFollow: "Impedire di seguirmi"
breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?" breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?"
itsOn: "Abilitato" itsOn: "Abilitato"
itsOff: "Disabilitato" itsOff: "Disabilitato"
@ -897,6 +901,8 @@ makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a di
classic: "Classico" classic: "Classico"
muteThread: "Silenzia conversazione" muteThread: "Silenzia conversazione"
unmuteThread: "Riattiva la conversazione" unmuteThread: "Riattiva la conversazione"
followingVisibility: "Visibilità dei profili seguiti"
followersVisibility: "Visibilità dei profili che ti seguono"
continueThread: "Altre conversazioni" continueThread: "Altre conversazioni"
deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
incorrectPassword: "La password è errata." incorrectPassword: "La password è errata."
@ -1067,6 +1073,8 @@ limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale"
noteIdOrUrl: "ID della Nota o URL" noteIdOrUrl: "ID della Nota o URL"
video: "Video" video: "Video"
videos: "Video" videos: "Video"
audio: "Audio"
audioFiles: "Audio"
dataSaver: "Risparmia dati" dataSaver: "Risparmia dati"
accountMigration: "Migrazione del profilo" accountMigration: "Migrazione del profilo"
accountMoved: "Questo profilo ha migrato altrove:" accountMoved: "Questo profilo ha migrato altrove:"
@ -1197,7 +1205,27 @@ remainingN: "Rimangono: {n}"
overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?" overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?"
seasonalScreenEffect: "Schermate in base alla stagione" seasonalScreenEffect: "Schermate in base alla stagione"
decorate: "Decora" decorate: "Decora"
addMfmFunction: "Aggiungi decorazioni"
enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM"
bubbleGame: "Bubble Game"
sfx: "Effetti sonori"
soundWillBePlayed: "Verrà riprodotto il suono"
showReplay: "Vedi i replay"
replay: "Replay"
replaying: "Replay in corso"
ranking: "Classifica"
lastNDays: "Ultimi {n} giorni" lastNDays: "Ultimi {n} giorni"
backToTitle: "Torna al titolo"
hemisphere: "Geolocalizzazione"
withSensitive: "Mostra le Note con allegati espliciti"
userSaysSomethingSensitive: "Note da {name} con allegati espliciti"
enableHorizontalSwipe: "Trascina per invertire i tab"
_bubbleGame:
howToPlay: "Come giocare"
_howToPlay:
section1: "Regola la posizione e rilascia l'oggetto nella casella."
section2: "Ottieni un punteggio, quando due oggetti dello stesso tipo si toccano e si trasformano in un oggetto diverso."
section3: "Se gli oggetti traboccano dalla scatola, il gioco finisce. Cerca di ottenere un punteggio elevato fondendo gli oggetti, evitando che escano dalla scatola!"
_announcement: _announcement:
forExistingUsers: "Solo ai profili attuali" forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
@ -1571,6 +1599,13 @@ _achievements:
_tutorialCompleted: _tutorialCompleted:
title: "Attestato di partecipazione al corso per principianti di CherryPick" title: "Attestato di partecipazione al corso per principianti di CherryPick"
description: "Ha completato il tutorial" description: "Ha completato il tutorial"
_bubbleGameExplodingHead:
title: "🤯"
description: "Estrai l'oggetto più grande dal Bubble Game"
_bubbleGameDoubleExplodingHead:
title: "Doppio 🤯"
description: "Due oggetti più grossi contemporaneamente nel Bubble Game"
flavor: "Ha le dimensioni di una bento-box 🤯 🤯"
_role: _role:
new: "Nuovo ruolo" new: "Nuovo ruolo"
edit: "Modifica ruolo" edit: "Modifica ruolo"
@ -1661,6 +1696,7 @@ _emailUnavailable:
disposable: "Indirizzo email non utilizzabile" disposable: "Indirizzo email non utilizzabile"
mx: "Server email non corretto" mx: "Server email non corretto"
smtp: "Il server email non risponde" smtp: "Il server email non risponde"
banned: "Non puoi registrarti con questo indirizzo email"
_ffVisibility: _ffVisibility:
public: "Pubblica" public: "Pubblica"
followers: "Mostra solo ai follower" followers: "Mostra solo ai follower"
@ -2020,6 +2056,26 @@ _permissions:
"write:flash": "Modifica Play" "write:flash": "Modifica Play"
"read:flash-likes": "Visualizza lista di Play piaciuti" "read:flash-likes": "Visualizza lista di Play piaciuti"
"write:flash-likes": "Modifica lista di Play piaciuti" "write:flash-likes": "Modifica lista di Play piaciuti"
"read:admin:abuse-user-reports": "Mostra i report dai profili utente"
"write:admin:delete-account": "Elimina l'account utente"
"write:admin:delete-all-files-of-a-user": "Elimina i file dell'account utente"
"read:admin:index-stats": "Visualizza informazioni sugli indici del database"
"read:admin:table-stats": "Visualizza informazioni sulle tabelle del database"
"read:admin:user-ips": "Visualizza indirizzi IP degli account"
"read:admin:meta": "Visualizza i metadati dell'istanza"
"write:admin:reset-password": "Ripristina la password dell'account utente"
"write:admin:resolve-abuse-user-report": "Risolvere le segnalazioni dagli account utente"
"write:admin:send-email": "Spedire email"
"read:admin:server-info": "Vedere le informazioni sul server"
"read:admin:show-moderation-log": "Vedere lo storico di moderazione"
"read:admin:show-user": "Vedere le informazioni private degli account utente"
"read:admin:show-users": "Vedere le informazioni private degli account utente"
"write:admin:suspend-user": "Sospendere i profili"
"write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili"
"write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili"
"write:admin:unsuspend-user": "Togliere la sospensione ai profili"
"write:admin:meta": "Modificare i metadati dell'istanza"
"write:admin:user-note": "Scrivere annotazioni di moderazione"
_auth: _auth:
shareAccessTitle: "Permessi dell'applicazione" shareAccessTitle: "Permessi dell'applicazione"
shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?" shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?"

View file

@ -40,7 +40,7 @@ favorites: "질겨찾기"
unfavorite: "질겨찾기서 어ᇝ애기" unfavorite: "질겨찾기서 어ᇝ애기"
favorited: "질겨찾기에 담앗십니다." favorited: "질겨찾기에 담앗십니다."
alreadyFavorited: "벌시로 질겨찾기에 담기 잇십니다." alreadyFavorited: "벌시로 질겨찾기에 담기 잇십니다."
cantFavorite: "질겨찾기에 몬 담십니다." cantFavorite: "질겨찾기에 몬 담십니다."
pin: "프로필에 붙이기" pin: "프로필에 붙이기"
unpin: "프로필서 띠기" unpin: "프로필서 띠기"
copyContent: "내용 복사하기" copyContent: "내용 복사하기"
@ -124,6 +124,7 @@ reactions: "반엉"
reactionSettingDescription2: "꺼시서 두고, 누질라서 뭉캐고, +’럴 누질라서 옇십니다." reactionSettingDescription2: "꺼시서 두고, 누질라서 뭉캐고, +’럴 누질라서 옇십니다."
rememberNoteVisibility: "공개 범위럴 기억하기" rememberNoteVisibility: "공개 범위럴 기억하기"
attachCancel: "붙임 빼기" attachCancel: "붙임 빼기"
deleteFile: "파일 뭉캐기"
markAsSensitive: "수ᇚ힘 설정" markAsSensitive: "수ᇚ힘 설정"
unmarkAsSensitive: "수ᇚ힘 무루기" unmarkAsSensitive: "수ᇚ힘 무루기"
enterFileName: "파일 이럼 서기" enterFileName: "파일 이럼 서기"
@ -463,6 +464,8 @@ onlyOneFileCanBeAttached: "메시지엔 파일 하나까제밖에 몬 넣십니
invitations: "초대하기" invitations: "초대하기"
invitationCode: "초대장" invitationCode: "초대장"
checking: "학인하고 잇십니다" checking: "학인하고 잇십니다"
tooShort: "억수로 짜립니다"
tooLong: "억수로 집니다"
passwordMatched: "맞십니다" passwordMatched: "맞십니다"
passwordNotMatched: "안 맞십니다" passwordNotMatched: "안 맞십니다"
signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소." signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소."
@ -515,7 +518,7 @@ objectStoragePrefixDesc: "요 Prefix 디렉토리 안에다가 파일이 들어
objectStorageEndpoint: "Endpoint" objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "AWS S3을 쓸라멘 요는 비워두고, 아이멘은 그 서비스 가이드에 맞게 endpoint를 넣어 주이소. '<host>' 내지 '<host>:<port>'처럼 넣십니다." objectStorageEndpointDesc: "AWS S3을 쓸라멘 요는 비워두고, 아이멘은 그 서비스 가이드에 맞게 endpoint를 넣어 주이소. '<host>' 내지 '<host>:<port>'처럼 넣십니다."
objectStorageRegion: "Region" objectStorageRegion: "Region"
objectStorageRegionDesc: "'xx-east-1' 같은 region 이름을 옇어 주이소. 써먹을 서비스에 region 개념 같은 게 읎다! 카면은 대신에 'us-east-1'을 옇어 놓으이소. AWS 설정 파일이나 환경 변수를 갖다 끌어다 쓸 거면은 요는 비워 두이소." objectStorageRegionDesc: "'xx-east-1' 같은 region 이름을 옇어 주이소. 만약에 내 서비스엔 region 같은 개념이 읎다, 카면은 대신에 'us-east-1'라고 해 두이소. AWS 설정 파일이나 환경 변수를 끌어다 쓰겠다믄 요는 비워 두이소."
objectStorageUseSSL: "SSL 쓰기" objectStorageUseSSL: "SSL 쓰기"
objectStorageUseSSLDesc: "API 호출할 때 HTTPS 안 쓸거면은 꺼 두이소" objectStorageUseSSLDesc: "API 호출할 때 HTTPS 안 쓸거면은 꺼 두이소"
objectStorageUseProxy: "연결에 프락시 사용" objectStorageUseProxy: "연결에 프락시 사용"
@ -537,8 +540,8 @@ popout: "새 창 열기"
volume: "음량" volume: "음량"
masterVolume: "대빵 음량" masterVolume: "대빵 음량"
notUseSound: "음소거하기" notUseSound: "음소거하기"
useSoundOnlyWhenActive: "CherryPick 활성화되어 있을 때만 소리 내기" useSoundOnlyWhenActive: "CherryPick 활성화되어 있을 때만 소리 내기"
details: "좀 더" details: "자세히"
chooseEmoji: "이모지 선택" chooseEmoji: "이모지 선택"
unableToProcess: "작업 다 몬 했십니다" unableToProcess: "작업 다 몬 했십니다"
recentUsed: "최근 쓴 놈" recentUsed: "최근 쓴 놈"
@ -571,7 +574,11 @@ userSilenced: "요 게정은... 수ᇚ혀 있십니다."
relays: "릴레이" relays: "릴레이"
addRelay: "릴레이 옇기" addRelay: "릴레이 옇기"
addedRelays: "옇은 릴레이" addedRelays: "옇은 릴레이"
deletedNote: "뭉캔 걸"
enableInfiniteScroll: "알아서 더 보기" enableInfiniteScroll: "알아서 더 보기"
useCw: "내용 수ᇚ후기"
description: "설멩"
describeFile: "캡션 옇기"
author: "맨던 사람" author: "맨던 사람"
manage: "간리" manage: "간리"
emailServer: "전자우펜 서버" emailServer: "전자우펜 서버"
@ -600,6 +607,7 @@ renotesCount: "리노트한 수"
renotedCount: "리노트덴 수" renotedCount: "리노트덴 수"
followingCount: "팔로우 수" followingCount: "팔로우 수"
followersCount: "팔로워 수" followersCount: "팔로워 수"
noteFavoritesCount: "질겨찾기한 노트 수"
clips: "클립 맨걸기" clips: "클립 맨걸기"
clearCache: "캐시 비우기" clearCache: "캐시 비우기"
unlikeConfirm: "좋네예럴 무룹니꺼?" unlikeConfirm: "좋네예럴 무룹니꺼?"
@ -608,6 +616,7 @@ user: "사용자"
administration: "간리" administration: "간리"
on: "킴" on: "킴"
off: "껌" off: "껌"
hide: "수ᇚ후기"
clickToFinishEmailVerification: "[{ok}]럴 누질라서 전자우펜 정멩얼 껕내이소." clickToFinishEmailVerification: "[{ok}]럴 누질라서 전자우펜 정멩얼 껕내이소."
searchByGoogle: "찾기" searchByGoogle: "찾기"
tenMinutes: "십 분" tenMinutes: "십 분"
@ -626,9 +635,11 @@ role: "옉할"
noRole: "옉할이 없십니다" noRole: "옉할이 없십니다"
thisPostMayBeAnnoyingCancel: "아이예" thisPostMayBeAnnoyingCancel: "아이예"
likeOnly: "좋네예마" likeOnly: "좋네예마"
myClips: "내 클립"
icon: "아바타" icon: "아바타"
replies: "답하기" replies: "답하기"
renotes: "리노트" renotes: "리노트"
attach: "옇기"
_initialAccountSetting: _initialAccountSetting:
startTutorial: "길라잡이 하기" startTutorial: "길라잡이 하기"
_initialTutorial: _initialTutorial:
@ -641,9 +652,52 @@ _initialTutorial:
title: "길라잡이가 껕낫십니다!🎉" title: "길라잡이가 껕낫십니다!🎉"
_achievements: _achievements:
_types: _types:
_notes1:
description: "첫 노트럴 섯어예"
_notes10:
description: "노트럴 10번 섰어예"
_notes100:
description: "노트럴 100번 섰어예"
_notes500:
description: "노트럴 500번 섰어예"
_notes1000:
description: "노트럴 1,000번 섰어예"
_notes5000:
description: "노트럴 5,000번 섰어예"
_notes10000:
description: "노트럴 10,000번 섰어예"
_notes20000:
description: "노트럴 20,000번 섰어예"
_notes30000:
description: "노트럴 30,000번 섰어예"
_notes40000:
description: "노트럴 40,000번 섰어예"
_notes50000:
description: "노트럴 50,000번 섰어예"
_notes60000:
description: "노트럴 60,000번 섰어예"
_notes70000:
description: "노트럴 70,000번 섰어예"
_notes80000:
description: "노트럴 80,000번 섰어예"
_notes90000:
description: "노트럴 90,000번 섰어예"
_notes100000:
description: "노트럴 100,000번 섰어예"
_noteClipped1:
description: "첫 노트럴 클립햇어예"
_noteFavorited1:
description: "첫 노트럴 질겨찾기에 담앗어예"
_myNoteFavorited1:
description: "다런 사람이 내 노트럴 질겨찾기에 담앗십니다"
_iLoveMisskey:
description: "“I ❤ #CherryPick”럴 섰어예"
_postedAt0min0sec:
description: "0분 0초에 노트를 섰어예"
_tutorialCompleted: _tutorialCompleted:
description: "길라잡이럴 껕냇십니다" description: "길라잡이럴 껕냇십니다"
_gallery: _gallery:
my: "내 걸"
liked: "좋네예한 걸" liked: "좋네예한 걸"
like: "좋네예!" like: "좋네예!"
unlike: "좋네예 무루기" unlike: "좋네예 무루기"
@ -654,7 +708,12 @@ _serverDisconnectedBehavior:
reload: "알아서 새로곤침" reload: "알아서 새로곤침"
_channel: _channel:
removeBanner: "배너 뭉캐기" removeBanner: "배너 뭉캐기"
usersCount: "{n}명 참여"
notesCount: "노트 {n}개"
_menuDisplay:
hide: "수ᇚ후기"
_theme: _theme:
description: "설멩"
keys: keys:
mention: "멘션" mention: "멘션"
_sfx: _sfx:
@ -663,6 +722,9 @@ _sfx:
_2fa: _2fa:
step3Title: "학인 기호럴 서기" step3Title: "학인 기호럴 서기"
renewTOTPCancel: "뎃어예" renewTOTPCancel: "뎃어예"
_permissions:
"read:favorites": "질겨찾기 보기"
"write:favorites": "질겨찾기 곤치기"
_widgets: _widgets:
profile: "프로필" profile: "프로필"
instanceInfo: "서버 정보" instanceInfo: "서버 정보"
@ -674,7 +736,10 @@ _widgets:
_userList: _userList:
chooseList: "리스트 개리기" chooseList: "리스트 개리기"
_cw: _cw:
hide: "수ᇚ후기"
show: "더 볼래예" show: "더 볼래예"
chars: "걸자 {count}개"
files: "파일 {count}개"
_visibility: _visibility:
home: "덜머리" home: "덜머리"
followers: "팔로워" followers: "팔로워"
@ -682,6 +747,7 @@ _profile:
name: "이럼" name: "이럼"
username: "사용자 이럼" username: "사용자 이럼"
_exportOrImport: _exportOrImport:
favoritedNotes: "질겨찾기한 노트"
clips: "클립 맨걸기" clips: "클립 맨걸기"
followingList: "팔로잉" followingList: "팔로잉"
muteList: "수ᇚ후기" muteList: "수ᇚ후기"
@ -692,16 +758,20 @@ _charts:
_timelines: _timelines:
home: "덜머리" home: "덜머리"
_play: _play:
my: "내 플레이"
script: "스크립트" script: "스크립트"
summary: "설멩"
_pages: _pages:
like: "좋네예" like: "좋네예"
unlike: "좋네예 무루기" unlike: "좋네예 무루기"
my: "내 페이지"
blocks: blocks:
image: "이미지" image: "이미지"
_note: _note:
id: "노트 아이디" id: "노트 아이디"
_notification: _notification:
youWereFollowed: "새 팔로워가 잇십니다" youWereFollowed: "새 팔로워가 잇십니다"
newNote: "새 걸"
_types: _types:
follow: "팔로잉" follow: "팔로잉"
mention: "멘션" mention: "멘션"

View file

@ -359,7 +359,7 @@ uploadFromUrl: "URL 업로드"
uploadFromUrlDescription: "업로드하려는 파일의 URL" uploadFromUrlDescription: "업로드하려는 파일의 URL"
uploadFromUrlRequested: "업로드를 요청했어요" uploadFromUrlRequested: "업로드를 요청했어요"
uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 약간의 시간이 필요할 수 있어요." uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 약간의 시간이 필요할 수 있어요."
explore: "발견하기" explore: "둘러보기"
messageRead: "읽음" messageRead: "읽음"
messageSend: "보냄" messageSend: "보냄"
noMoreHistory: "타임머신이 더 이상은 돌아갈 수 없대요!" noMoreHistory: "타임머신이 더 이상은 돌아갈 수 없대요!"
@ -2107,7 +2107,7 @@ _instanceMute:
title: "지정한 서버의 노트가 숨겨져요." title: "지정한 서버의 노트가 숨겨져요."
heading: "뮤트할 서버" heading: "뮤트할 서버"
_theme: _theme:
explore: "테마 찾아보기" explore: "테마 둘러보기"
install: "테마 설치" install: "테마 설치"
manage: "테마 관리" manage: "테마 관리"
code: "테마 코드" code: "테마 코드"
@ -2335,9 +2335,9 @@ _permissions:
"write:report-abuse": "위반 내용 신고하기" "write:report-abuse": "위반 내용 신고하기"
_auth: _auth:
shareAccessTitle: "애플리케이션 접근 허가" shareAccessTitle: "애플리케이션 접근 허가"
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용할까요?" shareAccess: "{name}’에서 계정에 접근하는 것을 허용할까요?"
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용할까요?" shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용할까요?"
permission: "{name}에서 다음 권한을 요청했어요" permission: "{name}에서 다음 권한을 요청했어요"
permissionAsk: "이 앱은 다음 권한을 요청하고 있습니다" permissionAsk: "이 앱은 다음 권한을 요청하고 있습니다"
pleaseGoBack: "앱으로 돌아가서 계속 진행해 주세요" pleaseGoBack: "앱으로 돌아가서 계속 진행해 주세요"
callback: "앱으로 돌아갈게요!" callback: "앱으로 돌아갈게요!"
@ -2801,7 +2801,7 @@ _reversi:
freeMatch: "프리 매치" freeMatch: "프리 매치"
lookingForPlayer: "대전 상대를 찾고 있어요" lookingForPlayer: "대전 상대를 찾고 있어요"
gameCanceled: "대국이 취소되었어요" gameCanceled: "대국이 취소되었어요"
shareToTlTheGameWhenStart: "시작 시 대국을 타임라인에 게시" shareToTlTheGameWhenStart: "대국 시작 시 타임라인에 대국 게시하기"
iStartedAGame: "대국이 시작되었어요! #MisskeyReversi" iStartedAGame: "대국이 시작되었어요! #MisskeyReversi"
opponentHasSettingsChanged: "상대방이 게임 설정을 변경했어요" opponentHasSettingsChanged: "상대방이 게임 설정을 변경했어요"
allowIrregularRules: "변칙 허용(완전 자유)" allowIrregularRules: "변칙 허용(완전 자유)"

View file

@ -1221,6 +1221,10 @@ userSaysSomethingSensitive: "含 {name} 敏感文件的帖子"
enableHorizontalSwipe: "滑动切换标签页" enableHorizontalSwipe: "滑动切换标签页"
_bubbleGame: _bubbleGame:
howToPlay: "游戏说明" howToPlay: "游戏说明"
_howToPlay:
section1: "对准位置将Emoji投入盒子。"
section2: "相同的Emoji相互接触合成后会得到新的Emoji以此获得分数。"
section3: "如果Emoji从箱子中溢出游戏将会结束。在防止Emoji溢出的同时不断合成新的Emoji来获取更高的分数吧"
_announcement: _announcement:
forExistingUsers: "仅限现有用户" forExistingUsers: "仅限现有用户"
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
@ -1596,6 +1600,10 @@ _achievements:
description: "完成了教学" description: "完成了教学"
_bubbleGameExplodingHead: _bubbleGameExplodingHead:
title: "🤯" title: "🤯"
description: "你合成出了游戏里最大的Emoji"
_bubbleGameDoubleExplodingHead:
title: "两个🤯"
description: "你合成出了2个游戏里最大的Emoji"
_role: _role:
new: "创建角色" new: "创建角色"
edit: "编辑角色" edit: "编辑角色"
@ -2527,9 +2535,41 @@ _hemisphere:
caption: "在某些客户端设置中用来确定季节" caption: "在某些客户端设置中用来确定季节"
_reversi: _reversi:
reversi: "黑白棋" reversi: "黑白棋"
gameSettings: "对局设置"
blackOrWhite: "先手/后手"
blackIs: "{name}执黑(先手)"
rules: "规则" rules: "规则"
thisGameIsStartedSoon: "对局即将开始"
waitingForOther: "等待对手准备"
waitingForMe: "等待你的准备"
waitingBoth: "请准备"
ready: "准备就绪" ready: "准备就绪"
cancelReady: "重新准备"
opponentTurn: "对手的回合"
myTurn: "你的回合"
turnOf: "{name}的回合"
pastTurnOf: "{name}的回合"
timeout: "超时"
drawn: "平局"
won: "{name}获胜"
black: "黑"
white: "白"
total: "总计" total: "总计"
turnCount: "第{count}回合"
myGames: "我的对局"
allGames: "所有对局"
ended: "结束"
playing: "对局中"
canPutEverywhere: "无限制放置模式"
timeLimitForEachTurn: "1回合的时间限制"
freeMatch: "自由匹配"
lookingForPlayer: "正在寻找对手"
gameCanceled: "对局被取消了"
shareToTlTheGameWhenStart: "开始时在时间线发布对局"
iStartedAGame: "对局开始!#MisskeyReversi"
opponentHasSettingsChanged: "对手更改了设定"
allowIrregularRules: "允许非常规规则(完全自由)"
disallowIrregularRules: "禁止非常规规则"
_offlineScreen: _offlineScreen:
title: "离线——无法连接到服务器" title: "离线——无法连接到服务器"
header: "无法连接到服务器" header: "无法连接到服务器"

View file

@ -1235,7 +1235,7 @@ _announcement:
tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。" tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。"
readConfirmTitle: "標記為已讀嗎?" readConfirmTitle: "標記為已讀嗎?"
readConfirmText: "閱讀「{title}」的內容並標記為已讀。" readConfirmText: "閱讀「{title}」的內容並標記為已讀。"
shouldNotBeUsedToPresentPermanentInfo: "由於可能會破壞使用者體驗,尤其是對於新使用者而言,我們建議使用公告來發布有時效性的資訊而不是固定不變的資訊。" shouldNotBeUsedToPresentPermanentInfo: "為了避免損害新用戶的使用體驗,建議使用公告來發布即時性的訊息,而不是用於固定不變的資訊。"
dialogAnnouncementUxWarn: "如果同時有 2 個以上對話方塊形式的公告存在,對於使用者體驗很可能會有不良的影響,因此建議謹慎使用。" dialogAnnouncementUxWarn: "如果同時有 2 個以上對話方塊形式的公告存在,對於使用者體驗很可能會有不良的影響,因此建議謹慎使用。"
silence: "不發送通知" silence: "不發送通知"
silenceDescription: "啟用此選項後,將不會發送此公告的通知,並且無需將其標記為已讀。" silenceDescription: "啟用此選項後,將不會發送此公告的通知,並且無需將其標記為已讀。"

View file

@ -1,7 +1,7 @@
{ {
"name": "cherrypick", "name": "cherrypick",
"version": "4.7.0-beta.1", "version": "4.7.0-beta.1",
"basedMisskeyVersion": "2024.2.0-beta.8", "basedMisskeyVersion": "2024.2.0-beta.10",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -39,6 +39,8 @@ export class EmailService {
public async sendEmail(to: string, subject: string, html: string, text: string) { public async sendEmail(to: string, subject: string, html: string, text: string) {
const meta = await this.metaService.fetch(true); const meta = await this.metaService.fetch(true);
if (!meta.enableEmail) return;
const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
const emailSettingUrl = `${this.config.url}/settings/email`; const emailSettingUrl = `${this.config.url}/settings/email`;

View file

@ -4,7 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm'; import { IsNull, Not } from 'typeorm';
import type { MiLocalUser } from '@/models/User.js'; import type { MiLocalUser } from '@/models/User.js';
import type { UsersRepository } from '@/models/_.js'; import type { UsersRepository } from '@/models/_.js';
import { MemorySingleCache } from '@/misc/cache.js'; import { MemorySingleCache } from '@/misc/cache.js';
@ -27,6 +27,14 @@ export class InstanceActorService {
this.cache = new MemorySingleCache<MiLocalUser>(Infinity); this.cache = new MemorySingleCache<MiLocalUser>(Infinity);
} }
@bindThis
public async realLocalUsersPresent(): Promise<boolean> {
return await this.usersRepository.existsBy({
host: IsNull(),
username: Not(ACTOR_USERNAME),
});
}
@bindThis @bindThis
public async getInstanceActor(): Promise<MiLocalUser> { public async getInstanceActor(): Promise<MiLocalUser> {
const cached = this.cache.get(); const cached = this.cache.get();

View file

@ -16,6 +16,7 @@ import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js'; import { MiUsedUsername } from '@/models/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.js'; import generateUserToken from '@/misc/generate-native-user-token.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js'; import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@ -37,6 +38,7 @@ export class SignupService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private metaService: MetaService, private metaService: MetaService,
private instanceActorService: InstanceActorService,
private usersChart: UsersChart, private usersChart: UsersChart,
) { ) {
} }
@ -81,7 +83,7 @@ export class SignupService {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
} }
const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0; const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
if (!opts.ignorePreservedUsernames && !isTheFirstUser) { if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
const instance = await this.metaService.fetch(true); const instance = await this.metaService.fetch(true);

View file

@ -658,6 +658,7 @@ export class ApRendererService {
'https://www.w3.org/ns/activitystreams', 'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1', 'https://w3id.org/security/v1',
{ {
Key: 'sec:Key',
// as non-standards // as non-standards
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
sensitive: 'as:sensitive', sensitive: 'as:sensitive',

View file

@ -243,20 +243,37 @@ export class ApPersonService implements OnModuleInit {
return null; return null;
} }
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> { private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>>> {
if (user == null) throw new Error('failed to create user: user is null');
const [avatar, banner] = await Promise.all([icon, image].map(img => { const [avatar, banner] = await Promise.all([icon, image].map(img => {
if (img == null) return null; // if we have an explicitly missing image, return an
if (user == null) throw new Error('failed to create user: user is null'); // explicitly-null set of values
if ((img == null) || (typeof img === 'object' && img.url == null)) {
return { id: null, url: null, blurhash: null };
}
return this.apImageService.resolveImage(user, img).catch(() => null); return this.apImageService.resolveImage(user, img).catch(() => null);
})); }));
/*
we don't want to return nulls on errors! if the database fields
are already null, nothing changes; if the database has old
values, we should keep those. The exception is if the remote has
actually removed the images: in that case, the block above
returns the special {id:null}&c value, and we return those
*/
return { return {
avatarId: avatar?.id ?? null, ...( avatar ? {
bannerId: banner?.id ?? null, avatarId: avatar.id,
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar', false) : null, avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, undefined, false) : null, avatarBlurhash: avatar.blurhash,
avatarBlurhash: avatar?.blurhash ?? null, } : {}),
bannerBlurhash: banner?.blurhash ?? null, ...( banner ? {
bannerId: banner.id,
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner) : null,
bannerBlurhash: banner.blurhash,
} : {}),
}; };
} }

View file

@ -31,6 +31,7 @@ export class EmojiEntityService {
category: emoji.category, category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl, url: emoji.publicUrl || emoji.originalUrl,
localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
}; };

View file

@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
localOnly: {
type: 'boolean',
optional: true, nullable: false,
},
isSensitive: { isSensitive: {
type: 'boolean', type: 'boolean',
optional: true, nullable: false, optional: true, nullable: false,

View file

@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js'; import type { UsersRepository } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js'; import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
@ -46,13 +47,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private signupService: SignupService, private signupService: SignupService,
private instanceActorService: InstanceActorService,
) { ) {
super(meta, paramDef, async (ps, _me, token) => { super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
const noUsers = (await this.usersRepository.countBy({ const realUsers = await this.instanceActorService.realLocalUsersPresent();
host: IsNull(), if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
})) === 0;
if ((!noUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
const { account, secret } = await this.signupService.signup({ const { account, secret } = await this.signupService.signup({
username: ps.username, username: ps.username,

View file

@ -6,11 +6,12 @@
import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm'; import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5'; import JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/_.js'; import type { AdsRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
@ -330,14 +331,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.adsRepository) @Inject(DI.adsRepository)
private adsRepository: AdsRepository, private adsRepository: AdsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private metaService: MetaService, private metaService: MetaService,
private instanceActorService: InstanceActorService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true); const instance = await this.metaService.fetch(true);
@ -418,9 +417,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
...(ps.detail ? { ...(ps.detail ? {
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: (await this.usersRepository.countBy({ requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
host: IsNull(),
})) === 0,
} : {}), } : {}),
}; };

View file

@ -1,7 +1,7 @@
/* /*
* version: 4.7.0-beta.1 * version: 4.7.0-beta.1
* basedMisskeyVersion: 2024.2.0-beta.8 * basedMisskeyVersion: 2024.2.0-beta.10
* generatedAt: 2024-02-02T07:19:50.775Z * generatedAt: 2024-02-07T07:14:28.049Z
*/ */
import type { SwitchCaseResponseType } from '../api.js'; import type { SwitchCaseResponseType } from '../api.js';

View file

@ -1,7 +1,7 @@
/* /*
* version: 4.7.0-beta.1 * version: 4.7.0-beta.1
* basedMisskeyVersion: 2024.2.0-beta.8 * basedMisskeyVersion: 2024.2.0-beta.10
* generatedAt: 2024-02-02T07:19:50.773Z * generatedAt: 2024-02-07T07:14:28.047Z
*/ */
import type { import type {

View file

@ -1,7 +1,7 @@
/* /*
* version: 4.7.0-beta.1 * version: 4.7.0-beta.1
* basedMisskeyVersion: 2024.2.0-beta.8 * basedMisskeyVersion: 2024.2.0-beta.10
* generatedAt: 2024-02-02T07:19:50.772Z * generatedAt: 2024-02-07T07:14:28.045Z
*/ */
import { operations } from './types.js'; import { operations } from './types.js';

View file

@ -1,7 +1,7 @@
/* /*
* version: 4.7.0-beta.1 * version: 4.7.0-beta.1
* basedMisskeyVersion: 2024.2.0-beta.8 * basedMisskeyVersion: 2024.2.0-beta.10
* generatedAt: 2024-02-02T07:19:50.771Z * generatedAt: 2024-02-07T07:14:28.044Z
*/ */
import { components } from './types.js'; import { components } from './types.js';

View file

@ -3,8 +3,8 @@
/* /*
* version: 4.7.0-beta.1 * version: 4.7.0-beta.1
* basedMisskeyVersion: 2024.2.0-beta.8 * basedMisskeyVersion: 2024.2.0-beta.10
* generatedAt: 2024-02-02T07:19:50.682Z * generatedAt: 2024-02-07T07:14:27.957Z
*/ */
/** /**
@ -4756,6 +4756,7 @@ export type components = {
name: string; name: string;
category: string | null; category: string | null;
url: string; url: string;
localOnly?: boolean;
isSensitive?: boolean; isSensitive?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
}; };

View file

@ -445,7 +445,7 @@ function applySelect() {
function chooseUser() { function chooseUser() {
props.close(); props.close();
os.selectUser().then(user => { os.selectUser({ includeSelf: true }).then(user => {
complete('user', user); complete('user', user);
props.textarea.focus(); props.textarea.focus();
}); });

View file

@ -30,9 +30,9 @@ function onClick(item: LegendItem) {
if (chart.value == null) return; if (chart.value == null) return;
if (type.value === 'pie' || type.value === 'doughnut') { if (type.value === 'pie' || type.value === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item // Pie and doughnut charts only have a single dataset and visibility is per item
if (item.index) chart.value.toggleDataVisibility(item.index); if (item.index != null) chart.value.toggleDataVisibility(item.index);
} else { } else {
if (item.datasetIndex) chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex)); if (item.datasetIndex != null) chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
} }
chart.value.update(); chart.value.update();
} }

View file

@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<template> <template>
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div> <div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
import { bundledLanguagesInfo } from 'shiki'; import { bundledLanguagesInfo } from 'shiki';
import type { BuiltinLanguage } from 'shiki'; import type { BuiltinLanguage } from 'shiki';
import { getHighlighter } from '@/scripts/code-highlighter.js'; import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{ const props = defineProps<{
code: string; code: string;
@ -21,11 +22,23 @@ const props = defineProps<{
}>(); }>();
const highlighter = await getHighlighter(); const highlighter = await getHighlighter();
const darkMode = defaultStore.reactiveState.darkMode;
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js'); const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
const [lightThemeName, darkThemeName] = await Promise.all([
getTheme('light', true),
getTheme('dark', true),
]);
const html = computed(() => highlighter.codeToHtml(props.code, { const html = computed(() => highlighter.codeToHtml(props.code, {
lang: codeLang.value, lang: codeLang.value,
theme: 'dark-plus', themes: {
fallback: 'dark-plus',
light: lightThemeName,
dark: darkThemeName,
},
defaultColor: false,
cssVariablePrefix: '--shiki-',
})); }));
async function fetchLanguage(to: string): Promise<void> { async function fetchLanguage(to: string): Promise<void> {
@ -64,6 +77,15 @@ watch(() => props.lang, (to) => {
margin: .5em 0; margin: .5em 0;
overflow: auto; overflow: auto;
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--divider);
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
& span {
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
}
& pre, & pre,
& code { & code {
@ -71,6 +93,26 @@ watch(() => props.lang, (to) => {
} }
} }
.light.codeBlockRoot :global(.shiki) {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
& span {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
}
}
.dark.codeBlockRoot :global(.shiki) {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
& span {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
}
}
.codeBlockRoot.codeEditor { .codeBlockRoot.codeEditor {
min-width: 100%; min-width: 100%;
height: 100%; height: 100%;
@ -79,6 +121,7 @@ watch(() => props.lang, (to) => {
padding: 12px; padding: 12px;
margin: 0; margin: 0;
border-radius: 6px; border-radius: 6px;
border: none;
min-height: 130px; min-height: 130px;
pointer-events: none; pointer-events: none;
min-width: calc(100% - 24px); min-width: calc(100% - 24px);
@ -90,6 +133,11 @@ watch(() => props.lang, (to) => {
text-rendering: inherit; text-rendering: inherit;
text-transform: inherit; text-transform: inherit;
white-space: pre; white-space: pre;
& span {
display: inline-block;
min-height: 1em;
}
} }
} }
</style> </style>

View file

@ -53,7 +53,6 @@ function copy() {
} }
.codeBlockCopyButton { .codeBlockCopyButton {
color: #D4D4D4;
position: absolute; position: absolute;
top: 8px; top: 8px;
right: 8px; right: 8px;
@ -67,8 +66,7 @@ function copy() {
.codeBlockFallbackRoot { .codeBlockFallbackRoot {
display: block; display: block;
overflow-wrap: anywhere; overflow-wrap: anywhere;
color: #D4D4D4; background: var(--bg);
background: #1E1E1E;
padding: 1em; padding: 1em;
margin: .5em 0; margin: .5em 0;
overflow: auto; overflow: auto;
@ -91,8 +89,8 @@ function copy() {
border-radius: 8px; border-radius: 8px;
padding: 24px; padding: 24px;
margin-top: 4px; margin-top: 4px;
color: #D4D4D4; color: var(--fg);
background: #1E1E1E; background: var(--bg);
} }
.codePlaceholderContainer { .codePlaceholderContainer {

View file

@ -196,10 +196,11 @@ watch(v, newValue => {
resize: none; resize: none;
text-align: left; text-align: left;
color: transparent; color: transparent;
caret-color: rgb(225, 228, 232); caret-color: var(--fg);
background-color: transparent; background-color: transparent;
border: 0; border: 0;
border-radius: 6px; border-radius: 6px;
box-sizing: border-box;
outline: 0; outline: 0;
min-width: calc(100% - 24px); min-width: calc(100% - 24px);
height: 100%; height: 100%;
@ -210,6 +211,6 @@ watch(v, newValue => {
} }
.textarea::selection { .textarea::selection {
color: #fff; color: var(--bg);
} }
</style> </style>

View file

@ -18,8 +18,7 @@ const props = defineProps<{
display: inline-block; display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere; overflow-wrap: anywhere;
color: #D4D4D4; background: var(--bg);
background: #1E1E1E;
padding: .1em; padding: .1em;
border-radius: .3em; border-radius: .3em;
} }

View file

@ -63,18 +63,25 @@ const loading = ref(true);
const ok = async () => { const ok = async () => {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => { const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas(); const croppedImage = await cropper?.getCropperImage();
const croppedSection = await cropper?.getCropperSelection();
// ()
const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth;
const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate;
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
croppedCanvas?.toBlob(blob => { croppedCanvas?.toBlob(blob => {
if (!blob) return; if (!blob) return;
const formData = new FormData(); const formData = new FormData();
formData.append('file', blob); formData.append('file', blob);
formData.append('name', `cropped_${props.file.name}`); formData.append('name', `cropped_${props.file.name}`);
formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false'); formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
formData.append('comment', props.file.comment ?? 'null'); if (props.file.comment) { formData.append('comment', props.file.comment);}
formData.append('i', $i!.token); formData.append('i', $i!.token);
if (props.uploadFolder || props.uploadFolder === null) { if (props.uploadFolder) {
formData.append('folderId', props.uploadFolder ?? 'null'); formData.append('folderId', props.uploadFolder);
} else if (defaultStore.state.uploadFolder) { } else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder); formData.append('folderId', defaultStore.state.uploadFolder);
} }

View file

@ -119,6 +119,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
showPinned?: boolean; showPinned?: boolean;
@ -127,6 +128,7 @@ const props = withDefaults(defineProps<{
asDrawer?: boolean; asDrawer?: boolean;
asWindow?: boolean; asWindow?: boolean;
asReactionPicker?: boolean; // 使使 asReactionPicker?: boolean; // 使使
targetNote?: Misskey.entities.Note;
}>(), { }>(), {
showPinned: true, showPinned: true,
}); });
@ -341,7 +343,7 @@ watch(q, () => {
}); });
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false; return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
} }
function focus() { function focus() {

View file

@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:showPinned="showPinned" :showPinned="showPinned"
:pinnedEmojis="pinnedEmojis" :pinnedEmojis="pinnedEmojis"
:asReactionPicker="asReactionPicker" :asReactionPicker="asReactionPicker"
:targetNote="targetNote"
:asDrawer="type === 'drawer'" :asDrawer="type === 'drawer'"
:max-height="maxHeight" :max-height="maxHeight"
@chosen="chosen" @chosen="chosen"
@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from 'cherrypick-js';
import { shallowRef } from 'vue'; import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
showPinned?: boolean; showPinned?: boolean;
pinnedEmojis?: string[], pinnedEmojis?: string[],
asReactionPicker?: boolean; asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note;
choseAndClose?: boolean; choseAndClose?: boolean;
}>(), { }>(), {
manualShowing: null, manualShowing: null,

View file

@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
:front="true" :front="true"
@closed="emit('closed')" @closed="emit('closed')"
> >
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/> <MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
</MkWindow> </MkWindow>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import * as Misskey from 'cherrypick-js';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@ -26,6 +27,7 @@ withDefaults(defineProps<{
src?: HTMLElement; src?: HTMLElement;
showPinned?: boolean; showPinned?: boolean;
asReactionPicker?: boolean; asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note
}>(), { }>(), {
showPinned: true, showPinned: true,
}); });

View file

@ -412,6 +412,7 @@ onDeactivated(() => {
.hidden { .hidden {
width: 100%; width: 100%;
height: 100%;
background: #000; background: #000;
border: none; border: none;
outline: none; outline: none;

View file

@ -314,7 +314,7 @@ const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entiti
const isMyRenote = $i && ($i.id === note.value.userId); const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false); const showContent = ref(false);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null); const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const isMFM = shouldMfmCollapsed(appearNote.value); const isMFM = shouldMfmCollapsed(appearNote.value);
const collapsed = ref(appearNote.value.cw == null && (isLong || (isMFM && defaultStore.state.collapseDefault) || (appearNote.value.files.length > 0 && defaultStore.state.allMediaNoteCollapse))); const collapsed = ref(appearNote.value.cw == null && (isLong || (isMFM && defaultStore.state.collapseDefault) || (appearNote.value.files.length > 0 && defaultStore.state.allMediaNoteCollapse)));
@ -508,7 +508,7 @@ function react(viaKeyboard = false): void {
} }
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value ?? null, reaction => { reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
if (props.mock) { if (props.mock) {
emit('reaction', reaction); emit('reaction', reaction);
return; return;

View file

@ -374,7 +374,7 @@ const translating = ref(false);
const viewTextSource = ref(false); const viewTextSource = ref(false);
const noNyaize = ref(false); const noNyaize = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed) : null; const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]); const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]);
@ -507,7 +507,7 @@ function react(viaKeyboard = false): void {
} }
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value ?? null, reaction => { reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
toggleReaction(reaction); toggleReaction(reaction);
}, () => { }, () => {
focus(); focus();

View file

@ -926,7 +926,7 @@ function cancel() {
} }
function insertMention() { function insertMention() {
os.selectUser().then(user => { os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => {
insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' '); insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
}); });
} }

View file

@ -920,7 +920,7 @@ function cancel() {
} }
function insertMention() { function insertMention() {
os.selectUser().then(user => { os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => {
insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' '); insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
}); });
} }

View file

@ -32,8 +32,9 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { customEmojis } from '@/custom-emojis.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
import { customEmojis } from '@/custom-emojis.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{ const props = defineProps<{
@ -51,6 +52,16 @@ const emit = defineEmits<{
const buttonEl = shallowRef<HTMLElement>(); const buttonEl = shallowRef<HTMLElement>();
const isCustomEmoji = computed(() => props.reaction.includes(':'));
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
const canToggle = computed(() => {
return !props.reaction.match(/@\w/) && $i
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|| !isCustomEmoji.value;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
const reactionName = computed(() => { const reactionName = computed(() => {
const r = props.reaction.replace(':', ''); const r = props.reaction.replace(':', '');
return r.slice(0, r.indexOf('@')); return r.slice(0, r.indexOf('@'));
@ -58,16 +69,12 @@ const reactionName = computed(() => {
const alternative: ComputedRef<string | null> = computed(() => defaultStore.state.reactableRemoteReactionEnabled ? (customEmojis.value.find(it => it.name === reactionName.value)?.name ?? null) : null); const alternative: ComputedRef<string | null> = computed(() => defaultStore.state.reactableRemoteReactionEnabled ? (customEmojis.value.find(it => it.name === reactionName.value)?.name ?? null) : null);
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
async function toggleReaction(ev: MouseEvent) { async function toggleReaction(ev: MouseEvent) {
if (!canToggle.value) { if (!canToggle.value) {
chooseAlternative(ev); chooseAlternative(ev);
return; return;
} }
// TODO: 使
const oldReaction = props.note.myReaction; const oldReaction = props.note.myReaction;
if (oldReaction) { if (oldReaction) {
const confirm = await os.confirm({ const confirm = await os.confirm({
@ -146,8 +153,8 @@ function stealReaction(ev: MouseEvent) {
} }
async function menu(ev) { async function menu(ev) {
if (!canToggle.value) return; if (!canGetInfo.value) return;
if (!props.reaction.includes(':')) return;
os.popupMenu([{ os.popupMenu([{
type: 'label', type: 'label',
text: `:${reactionName.value}:`, text: `:${reactionName.value}:`,

View file

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div :class="$style.caption"><slot name="caption"></slot></div> <div :class="$style.caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div> </div>
</template> </template>
@ -141,6 +141,7 @@ function show() {
active: computed(() => v.value === option.props?.value), active: computed(() => v.value === option.props?.value),
action: () => { action: () => {
v.value = option.props?.value; v.value = option.props?.value;
changed.value = true;
emit('changeByUser', v.value); emit('changeByUser', v.value);
}, },
}); });
@ -291,6 +292,10 @@ function show() {
padding-left: 6px; padding-left: 6px;
} }
.save {
margin: 8px 0 0 0;
}
.chevron { .chevron {
transition: transform 0.1s ease-out; transition: transform 0.1s ease-out;
} }

View file

@ -309,7 +309,7 @@ function react(viaKeyboard = false): void {
} }
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value ?? null, reaction => { reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
if (props.mock) { if (props.mock) {
emit('reaction', reaction); emit('reaction', reaction);
return; return;

View file

@ -78,10 +78,13 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const props = defineProps<{ const props = withDefaults(defineProps<{
includeSelf?: boolean; includeSelf?: boolean;
localOnly?: boolean; localOnly?: boolean;
}>(); }>(), {
includeSelf: false,
localOnly: false,
});
const username = ref(''); const username = ref('');
const host = ref(''); const host = ref('');
@ -102,10 +105,10 @@ function search() {
detail: false, detail: false,
}).then(_users => { }).then(_users => {
users.value = _users.filter((u) => { users.value = _users.filter((u) => {
if (props.includeSelf === false) { if (props.includeSelf) {
return u.id !== $i?.id;
} else {
return true; return true;
} else {
return u.id !== $i?.id;
} }
}); });
}); });
@ -146,10 +149,10 @@ onMounted(() => {
} }
}); });
_users = _users.filter((u) => { _users = _users.filter((u) => {
if (props.includeSelf === false) { if (props.includeSelf) {
return u.id !== $i?.id;
} else {
return true; return true;
} else {
return u.id !== $i?.id;
} }
}); });
recentUsers.value = _users; recentUsers.value = _users;

View file

@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<KeepAlive :max="defaultStore.state.numberOfPageCache"> <KeepAlive
:max="defaultStore.state.numberOfPageCache"
:exclude="pageCacheController"
>
<Suspense :timeout="0"> <Suspense :timeout="0">
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
@ -16,9 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue'; import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
import { IRouter, Resolved } from '@/nirax.js'; import { IRouter, Resolved, RouteDef } from '@/nirax.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { globalEvents } from '@/events.js';
import MkLoadingPage from '@/pages/_loading_.vue';
const props = defineProps<{ const props = defineProps<{
router?: IRouter; router?: IRouter;
@ -46,20 +51,47 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
} }
const current = resolveNested(router.current)!; const current = resolveNested(router.current)!;
const currentPageComponent = shallowRef(current.route.component); const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
const currentPageProps = ref(current.props); const currentPageProps = ref(current.props);
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
function onChange({ resolved, key: newKey }) { function onChange({ resolved, key: newKey }) {
const current = resolveNested(resolved); const current = resolveNested(resolved);
if (current == null) return; if (current == null || 'redirect' in current.route) return;
currentPageComponent.value = current.route.component; currentPageComponent.value = current.route.component;
currentPageProps.value = current.props; currentPageProps.value = current.props;
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props)); key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
nextTick(() => {
//
if (clearCacheRequested.value) {
clearCacheRequested.value = false;
}
});
} }
router.addListener('change', onChange); router.addListener('change', onChange);
// #region
/**
* キャッシュクリアが有効になったら全キャッシュをクリアする
*
* keepAlive側にwatcherがあるのですぐ消えるとはおもうけど念のためページ遷移完了まではキャッシュを無効化しておく
* キャッシュ有効時向けにexcludeを使いたい場合はpageCacheControllerに並列に突っ込むのではなく下に追記すること
*/
const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined);
const clearCacheRequested = ref(false);
globalEvents.on('requestClearPageCache', () => {
if (_DEV_) console.log('clear page cache requested');
if (!clearCacheRequested.value) {
clearCacheRequested.value = true;
}
});
// #endregion
onBeforeUnmount(() => { onBeforeUnmount(() => {
router.removeListener('change', onChange); router.removeListener('change', onChange);
}); });

View file

@ -4,6 +4,10 @@
*/ */
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'cherrypick-js';
// TODO: 型付け export const globalEvents = new EventEmitter<{
export const globalEvents = new EventEmitter(); themeChanged: () => void;
clientNotification: (notification: Misskey.entities.Notification) => void;
requestClearPageCache: () => void;
}>();

View file

@ -129,9 +129,10 @@ export function promiseDialog<T extends Promise<any>>(
let popupIdCount = 0; let popupIdCount = 0;
export const popups = ref([]) as Ref<{ export const popups = ref([]) as Ref<{
id: any; id: number;
component: any; component: Component;
props: Record<string, any>; props: Record<string, any>;
events: Record<string, any>;
}[]>; }[]>;
const zIndexes = { const zIndexes = {
@ -145,7 +146,18 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
return zIndexes[priority]; return zIndexes[priority];
} }
export async function popup<T extends Component>(component: T, props: ComponentProps<T>, events = {}, disposeEvent?: string) { // InstanceType<typeof Component>['$emit'] だとインターセクション型が返ってきて
// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
type ComponentEmit<T> = T extends new () => { $props: infer Props }
? EmitsExtractor<Props>
: never;
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];
};
export async function popup<T extends Component>(component: T, props: ComponentProps<T>, events: ComponentEmit<T> = {} as ComponentEmit<T>, disposeEvent?: keyof ComponentEmit<T>) {
markRaw(component); markRaw(component);
const id = ++popupIdCount; const id = ++popupIdCount;

View file

@ -324,6 +324,12 @@ const patronsWithIconWithMisskey = [{
}, { }, {
name: 'taichan', name: 'taichan',
icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png', icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png',
}, {
name: '猫吉よりお',
icon: 'https://assets.misskey-hub.net/patrons/a11518b3b34b4536a4bdd7178ba76a7b.png',
}, {
name: '有栖かずみ',
icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.png',
}]; }];
const patronsWithCherryPick = [ const patronsWithCherryPick = [

View file

@ -148,9 +148,9 @@ function save() {
themeColor: themeColor.value === '' ? null : themeColor.value, themeColor: themeColor.value === '' ? null : themeColor.value,
defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value, defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value,
defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value, defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value,
infoImageUrl: infoImageUrl.value, infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value,
notFoundImageUrl: notFoundImageUrl.value, notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value,
serverErrorImageUrl: serverErrorImageUrl.value, serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value,
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)), manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();

View file

@ -45,7 +45,7 @@ async function init() {
} }
function chooseProxyAccount() { function chooseProxyAccount() {
os.selectUser().then(user => { os.selectUser({ localOnly: true }).then(user => {
proxyAccount.value = user; proxyAccount.value = user;
proxyAccountId.value = user.id; proxyAccountId.value = user.id;
save(); save();

View file

@ -112,9 +112,7 @@ async function del() {
} }
async function assign() { async function assign() {
const user = await os.selectUser({ const user = await os.selectUser({ includeSelf: true });
includeSelf: true,
});
const { canceled: canceled2, result: period } = await os.select({ const { canceled: canceled2, result: period } = await os.select({
title: i18n.ts.period, title: i18n.ts.period,

View file

@ -92,7 +92,7 @@ const pagination = {
}; };
function searchUser() { function searchUser() {
os.selectUser().then(user => { os.selectUser({ includeSelf: true }).then(user => {
show(user); show(user);
}); });
} }

View file

@ -151,7 +151,7 @@ async function deleteAntenna() {
} }
function addUser() { function addUser() {
os.selectUser().then(user => { os.selectUser({ includeSelf: true }).then(user => {
users.value = users.value.trim(); users.value = users.value.trim();
users.value += '\n@' + Misskey.acct.toString(user as any); users.value += '\n@' + Misskey.acct.toString(user as any);
users.value = users.value.trim(); users.value = users.value.trim();

View file

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m"> <div class="_gaps_m">
<!-- <MkSwitch v-model="isLocalOnly">{{ i18n.ts.localOnly }}</MkSwitch> --> <!-- <MkSwitch v-model="isLocalOnly">{{ i18n.ts.localOnly }}</MkSwitch> -->
<MkFolder> <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.specifyUser }}</template> <template #label>{{ i18n.ts.specifyUser }}</template>
<template v-if="user" #suffix>@{{ user.username }}</template> <template v-if="user" #suffix>@{{ user.username }}</template>
@ -69,7 +69,7 @@ const user = ref<any>(null);
const isLocalOnly = ref(false); const isLocalOnly = ref(false);
function selectUser() { function selectUser() {
os.selectUser().then(_user => { os.selectUser({ includeSelf: true }).then(_user => {
user.value = _user; user.value = _user;
}); });
} }

View file

@ -157,7 +157,7 @@ const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
const setDefaultEmoji = () => setDefault(pinnedEmojis); const setDefaultEmoji = () => setDefault(pinnedEmojis);
function previewReaction(ev: MouseEvent) { function previewReaction(ev: MouseEvent) {
reactionPicker.show(getHTMLElement(ev)); reactionPicker.show(getHTMLElement(ev), null);
} }
function previewEmoji(ev: MouseEvent) { function previewEmoji(ev: MouseEvent) {

View file

@ -125,6 +125,7 @@ import { langmap } from '@/scripts/langmap.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { globalEvents } from '@/events.js';
import { unisonReload } from '@/scripts/unison-reload.js'; import { unisonReload } from '@/scripts/unison-reload.js';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
@ -174,6 +175,7 @@ function saveFields() {
os.apiWithDialog('i/update', { os.apiWithDialog('i/update', {
fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })), fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })),
}); });
globalEvents.emit('requestClearPageCache');
} }
function save() { function save() {
@ -192,6 +194,7 @@ function save() {
isBot: !!profile.isBot, isBot: !!profile.isBot,
isCat: !!profile.isCat, isCat: !!profile.isCat,
}); });
globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled'); claimAchievement('profileFilled');
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') { if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
claimAchievement('setNameToSyuilo'); claimAchievement('setNameToSyuilo');
@ -234,6 +237,7 @@ function changeAvatar(ev) {
}); });
$i.avatarId = i.avatarId; $i.avatarId = i.avatarId;
$i.avatarUrl = i.avatarUrl; $i.avatarUrl = i.avatarUrl;
globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled'); claimAchievement('profileFilled');
}); });
} }
@ -260,6 +264,7 @@ function changeBanner(ev) {
}); });
$i.bannerId = i.bannerId; $i.bannerId = i.bannerId;
$i.bannerUrl = i.bannerUrl; $i.bannerUrl = i.bannerUrl;
globalEvents.emit('requestClearPageCache');
}); });
} }

View file

@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js';
import { fetchThemes, getThemes } from '@/theme-store.js'; import { fetchThemes, getThemes } from '@/theme-store.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import * as os from '@/os.js';
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
const installedThemes = ref(getThemes()); const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef(); const builtinThemes = getBuiltinThemesRef();
@ -124,6 +136,7 @@ const lightThemeId = computed({
} }
}, },
}); });
const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
const wallpaper = ref(miLocalStorage.getItem('wallpaper')); const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
@ -141,7 +154,7 @@ watch(wallpaper, () => {
} else { } else {
miLocalStorage.setItem('wallpaper', wallpaper.value); miLocalStorage.setItem('wallpaper', wallpaper.value);
} }
location.reload(); reloadAsk();
}); });
onActivated(() => { onActivated(() => {

View file

@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
import { deepMerge } from '@/scripts/merge.js';
type StateDef = Record<string, { type StateDef = Record<string, {
where: 'account' | 'device' | 'deviceAccount'; where: 'account' | 'device' | 'deviceAccount';
@ -84,29 +85,9 @@ export class Storage<T extends StateDef> {
return typeof value === 'object' && value !== null && !Array.isArray(value); return typeof value === 'object' && value !== null && !Array.isArray(value);
} }
/**
* valueにないキーをdefからもらう\
* nullはそのままundefinedはdefの値
**/
private mergeObject<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
const result = structuredClone(value) as X;
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
result[k] = v;
} else if (this.isPureObject(v) && this.isPureObject(result[k])) {
const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
result[k] = this.mergeObject<typeof v>(child, v);
}
}
return result;
}
return value;
}
private mergeState<X>(value: X, def: X): X { private mergeState<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) { if (this.isPureObject(value) && this.isPureObject(def)) {
const merged = this.mergeObject(value, def); const merged = deepMerge(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged); if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);

View file

@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter {
return this.supplier().resolve(path); return this.supplier().resolve(path);
} }
init(): void {
this.supplier().init();
}
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> { eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
return this.supplier().eventNames(); return this.supplier().eventNames();
} }

View file

@ -0,0 +1,8 @@
import * as Misskey from 'cherrypick-js';
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean {
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
return !(emoji.localOnly && note.user.host !== me.host)
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
&& (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
}

View file

@ -8,13 +8,13 @@
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった // あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045 // https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[]; export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
export function deepClone<T extends Cloneable>(x: T): T { export function deepClone<T extends Cloneable>(x: T): T {
if (typeof x === 'object') { if (typeof x === 'object') {
if (x === null) return x; if (x === null) return x;
if (Array.isArray(x)) return x.map(deepClone) as T; if (Array.isArray(x)) return x.map(deepClone) as T;
const obj = {} as Record<string, Cloneable>; const obj = {} as Record<string | number | symbol, Cloneable>;
for (const [k, v] of Object.entries(x)) { for (const [k, v] of Object.entries(x)) {
obj[k] = v === undefined ? undefined : deepClone(v); obj[k] = v === undefined ? undefined : deepClone(v);
} }

View file

@ -1,9 +1,51 @@
import { bundledThemesInfo } from 'shiki';
import { getHighlighterCore, loadWasm } from 'shiki/core'; import { getHighlighterCore, loadWasm } from 'shiki/core';
import darkPlus from 'shiki/themes/dark-plus.mjs'; import darkPlus from 'shiki/themes/dark-plus.mjs';
import type { Highlighter, LanguageRegistration } from 'shiki'; import { unique } from './array.js';
import { deepClone } from './clone.js';
import { deepMerge } from './merge.js';
import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki';
import { ColdDeviceStorage } from '@/store.js';
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
let _highlighter: Highlighter | null = null; let _highlighter: Highlighter | null = null;
export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme'));
if (theme.base) {
const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
}
if (theme.codeHighlighter) {
let _res: ThemeRegistration = {};
if (theme.codeHighlighter.base === '_none_') {
_res = deepClone(theme.codeHighlighter.overrides);
} else {
const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
_res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
}
if (_res.name == null) {
_res.name = theme.id;
}
_res.type = mode;
if (getName) {
return _res.name;
}
return _res;
}
if (getName) {
return 'dark-plus';
}
return darkPlus;
}
export async function getHighlighter(): Promise<Highlighter> { export async function getHighlighter(): Promise<Highlighter> {
if (!_highlighter) { if (!_highlighter) {
return await initHighlighter(); return await initHighlighter();
@ -13,11 +55,17 @@ export async function getHighlighter(): Promise<Highlighter> {
export async function initHighlighter() { export async function initHighlighter() {
const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json'); const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
await loadWasm(import('shiki/onig.wasm?init')); await loadWasm(import('shiki/onig.wasm?init'));
// テーマの重複を消す
const themes = unique([
darkPlus,
...(await Promise.all([getTheme('light'), getTheme('dark')])),
]);
const highlighter = await getHighlighterCore({ const highlighter = await getHighlighterCore({
themes: [darkPlus], themes,
langs: [ langs: [
import('shiki/langs/javascript.mjs'), import('shiki/langs/javascript.mjs'),
{ {
@ -27,6 +75,20 @@ export async function initHighlighter() {
], ],
}); });
ColdDeviceStorage.watch('lightTheme', async () => {
const newTheme = await getTheme('light');
if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
highlighter.loadTheme(newTheme);
}
});
ColdDeviceStorage.watch('darkTheme', async () => {
const newTheme = await getTheme('dark');
if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
highlighter.loadTheme(newTheme);
}
});
_highlighter = highlighter; _highlighter = highlighter;
return highlighter; return highlighter;

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { deepClone } from './clone.js';
import type { Cloneable } from './clone.js';
function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* valueにないキーをdefからもらう\
* nullはそのままundefinedはdefの値
**/
export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: X, def: X): X {
if (isPureObject(value) && isPureObject(def)) {
const result = deepClone(value as Cloneable) as X;
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
result[k] = v;
} else if (isPureObject(v) && isPureObject(result[k])) {
const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record<string | number | symbol, unknown>;
result[k] = deepMerge<typeof v>(child, v);
}
}
return result;
}
return value;
}

View file

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import * as Misskey from 'cherrypick-js';
import { defineAsyncComponent, Ref, ref } from 'vue'; import { defineAsyncComponent, Ref, ref } from 'vue';
import { popup } from '@/os.js'; import { popup } from '@/os.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@ -10,6 +11,7 @@ import { defaultStore } from '@/store.js';
class ReactionPicker { class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null); private src: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false); private manualShowing = ref(false);
private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
private onChosen?: (reaction: string) => void; private onChosen?: (reaction: string) => void;
private onClosed?: () => void; private onClosed?: () => void;
@ -23,6 +25,7 @@ class ReactionPicker {
src: this.src, src: this.src,
pinnedEmojis: reactionsRef, pinnedEmojis: reactionsRef,
asReactionPicker: true, asReactionPicker: true,
targetNote: this.targetNote,
manualShowing: this.manualShowing, manualShowing: this.manualShowing,
}, { }, {
done: reaction => { done: reaction => {
@ -38,8 +41,9 @@ class ReactionPicker {
}); });
} }
public show(src: HTMLElement | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
this.src.value = src; this.src.value = src;
this.targetNote.value = targetNote;
this.manualShowing.value = true; this.manualShowing.value = true;
this.onChosen = onChosen; this.onChosen = onChosen;
this.onClosed = onClosed; this.onClosed = onClosed;

View file

@ -6,6 +6,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { deepClone } from './clone.js'; import { deepClone } from './clone.js';
import type { BuiltinTheme } from 'shiki';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
import lightTheme from '@/themes/_light.json5'; import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5'; import darkTheme from '@/themes/_dark.json5';
@ -18,6 +19,13 @@ export type Theme = {
desc?: string; desc?: string;
base?: 'dark' | 'light'; base?: 'dark' | 'light';
props: Record<string, string>; props: Record<string, string>;
codeHighlighter?: {
base: BuiltinTheme;
overrides?: Record<string, any>;
} | {
base: '_none_';
overrides: Record<string, any>;
};
}; };
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
@ -69,7 +77,7 @@ export const getBuiltinThemesRef = () => {
return builtinThemes; return builtinThemes;
}; };
let timeout = null; let timeout: number | null = null;
export function applyTheme(theme: Theme, persist = true) { export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout); if (timeout) window.clearTimeout(timeout);

View file

@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import { miLocalStorage } from './local-storage.js'; import { miLocalStorage } from './local-storage.js';
import type { SoundType } from '@/scripts/sound.js'; import type { SoundType } from '@/scripts/sound.js';
import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki';
import { Storage } from '@/pizzax.js'; import { Storage } from '@/pizzax.js';
import { hemisphere } from '@/scripts/intl-const.js'; import { hemisphere } from '@/scripts/intl-const.js';

View file

@ -103,4 +103,8 @@
X16: ':alpha<0.7<@panel', X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg', X17: ':alpha<0.8<@bg',
}, },
codeHighlighter: {
base: 'one-dark-pro',
},
} }

View file

@ -103,4 +103,8 @@
X16: ':alpha<0.7<@panel', X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg', X17: ':alpha<0.8<@bg',
}, },
codeHighlighter: {
base: 'catppuccin-latte',
},
} }