Merge remote-tracking branch 'misskey-original/develop' into develop

# Conflicts:
#	locales/en-US.yml
#	locales/index.d.ts
#	locales/ja-JP.yml
#	package.json
#	packages/backend/src/server/api/endpoints/notes/create.ts
#	packages/frontend/src/components/MkDrive.file.vue
#	packages/frontend/src/components/MkNote.vue
#	packages/frontend/src/components/MkNoteHeader.vue
#	packages/frontend/src/components/MkPostForm.vue
#	packages/frontend/src/components/MkReactionsViewer.reaction.vue
#	packages/frontend/src/components/MkUserSetupDialog.vue
#	packages/frontend/src/pages/timeline.tutorial.vue
#	packages/frontend/src/pages/timeline.vue
#	packages/misskey-js/etc/misskey-js.api.md
This commit is contained in:
mattyatea 2023-11-04 01:24:44 +09:00
commit cb1005811d
106 changed files with 2036 additions and 963 deletions

View File

@ -20,16 +20,22 @@
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
- 画像は512x512pxを推奨します。
- Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加
- Enhance: アカウント登録時のメールアドレス認証に30分の有効期限を設定
- 有効期限が切れた後であれば、登録時に使用した招待コードを再度利用できるように変更しました。
- ユーザーが誤ったメールアドレスを入力した場合に招待コードが失効してしまう問題が解消されます。
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
- Enhance: 未読の通知数を表示できるように
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
- Change: CWを使用する場合、注釈を空にすることは許可されなくなりました
### Client
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
- Feat: 通知をグルーピングして表示するオプション(オプトアウト)
- Feat: Misskeyの基本的なチュートリアルを実装
- Feat: スワイプしてタイムラインを再読込できるように
- PCの場合は右上のボタンからでも再読込できます
- Enhance: タイムラインの自動更新を無効にできるように
@ -49,8 +55,13 @@
- Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983
- Fix: 個人カードのemojiがバッテリーになっている問題を修正
- Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正
- Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197
- Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174
- Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224
- Fix: In deck layout, replies option is not saved after refresh
### Server
- Feat: Registry APIがサードパーティから利用可能になりました
- Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように
- Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善
- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました

View File

@ -15,7 +15,7 @@ Before creating an issue, please check the following:
- To avoid duplication, please search for similar issues before creating a new issue.
- Do not use Issues to ask questions or troubleshooting.
- Issues should only be used to feature requests, suggestions, and bug tracking.
- Please ask questions or troubleshooting in ~~the [Misskey Forum](https://forum.misskey.io/)~~ [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3).
- Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3).
> **Warning**
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.

View File

@ -1262,9 +1262,6 @@ _time:
minute: "د"
hour: "سا"
day: "ي"
_timelineTutorial:
title: "كيف تستخدم Misskey"
step3_1: "هل نشرت ملاحظتك الأولى؟"
_2fa:
alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين."
step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})."

View File

@ -1109,7 +1109,6 @@ _initialAccountSetting:
pushNotificationDescription: "Povolení push oznámení vám umožní přijímat oznámení od {name} přímo ve vašem zařízení."
initialAccountSettingCompleted: "Nastavení profilu dokončeno!"
haveFun: "Užívejte {name}!"
ifYouNeedLearnMore: "Pokud se chcete dozvědět více o tom, jak používat {name} (Misskey), navštivte {link}."
skipAreYouSure: "Opravdu chcete přeskočit nastavení profilu?"
laterAreYouSure: "Opravdu chcete provést nastavení profilu později?"
_serverRules:
@ -1658,16 +1657,6 @@ _time:
minute: "Minut"
hour: "Hodin"
day: "Dnů"
_timelineTutorial:
title: "Jak používat Misskey"
step1_1: "Toto je \"časová osa\". Zde se chronologicky zobrazují všechny \"poznámky\" odeslané na {name}."
step1_2: "Existuje několik různých časových plánů. Například \"Domácí časová osa\" bude obsahovat poznámky uživatelů, které sledujete, a \"Místní časová osa\" bude obsahovat poznámky všech uživatelů {name}."
step2_1: "Zkusme zveřejnit poznámku. Můžete tak učinit stisknutím tlačítka s ikonou tužky."
step2_2: "Co takhle napsat sebepředstavení, nebo jen \"Ahoj {name}!\", pokud se vám nechce?"
step3_1: "Dokončil jsi svou první poznámku?"
step3_2: "Na časové ose by se nyní měla zobrazit vaše první poznámka."
step4_1: "K poznámkám můžete také připojit \"Reakce\"."
step4_2: "Chcete-li připojit reakci, stiskněte na poznámce znaménko \"+\" a vyberte emoji, kterým chcete reagovat."
_2fa:
alreadyRegistered: "Již jste zaregistrovali dvoufaktorové ověřovací zařízení."
registerTOTP: "Registrovat aplikaci autentizátoru"

View File

@ -979,6 +979,7 @@ assign: "Zuweisen"
unassign: "Entfernen"
color: "Farbe"
manageCustomEmojis: "Kann benutzerdefinierte Emojis verwalten"
manageAvatarDecorations: "Profilbilddekorationen verwalten"
youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht."
cannotPerformTemporary: "Vorübergehend nicht verfügbar"
cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut."
@ -1149,6 +1150,12 @@ detach: "Entfernen"
angle: "Winkel"
flip: "Umdrehen"
showAvatarDecorations: "Profilbilddekoration anzeigen"
releaseToRefresh: "Zum Aktualisieren loslassen"
refreshing: "Wird aktualisiert..."
pullDownToRefresh: "Zum Aktualisieren ziehen"
disableStreamingTimeline: "Echtzeitaktualisierung der Chronik deaktivieren"
useGroupedNotifications: "Benachrichtigungen gruppieren"
cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
_announcement:
forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
@ -1170,7 +1177,6 @@ _initialAccountSetting:
pushNotificationDescription: "Durch die Aktivierung von Push-Benachrichtigungen kannst du von {name} Benachrichtigungen direkt auf dein Gerät erhalten."
initialAccountSettingCompleted: "Kontoeinrichtung abgeschlossen!"
haveFun: "Viel Spaß mit {name}!"
ifYouNeedLearnMore: "Besuche {link}, falls du mehr über {name} (Misskey) lernen möchtest."
skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?"
laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?"
_serverRules:
@ -1485,6 +1491,7 @@ _role:
inviteLimitCycle: "Zyklus des Einladungslimits"
inviteExpirationTime: "Gültigkeitsdauer von Einladungen"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
canManageAvatarDecorations: "Profilbilddekorationen verwalten"
driveCapacity: "Drive-Kapazität"
alwaysMarkNsfw: "Dateien immer als NSFW markieren"
pinMax: "Maximale Anzahl an angehefteten Notizen"
@ -1604,6 +1611,7 @@ _aboutMisskey:
donate: "An Misskey spenden"
morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰"
patrons: "UnterstützerInnen"
projectMembers: "Projektmitglieder"
_displayOfSensitiveMedia:
respect: "Sensible Medien verbergen"
ignore: "Sensible Medien anzeigen"
@ -1735,16 +1743,6 @@ _time:
minute: "Minute(n)"
hour: "Stunde(n)"
day: "Tag(en)"
_timelineTutorial:
title: "Wie du Misskey verwendest"
step1_1: "Dieser Bildschirm ist die \"Chronik\". Hier werden alle \"Notizen\" von {name} angezeigt."
step1_2: "Es gibt einige verschiedene Chroniken. Beispielsweise werden in der \"Startseite\" alle Notizen von Nutzern, denen du folgst, angezeigt, und in der \"Lokalen Chronik\" werden Notizen aller Nutzer auf {name} angezeigt."
step2_1: "Lass uns als nächstes versuchen, eine Notiz zu schreiben. Dies kannst du tun, indem du auf den Knopf mit dem Stift-Icon drückst."
step2_2: "Stell dich den anderen vor oder schreibe einfach \"Hallo {name}!\", wenn du darauf keine Lust hast oder dir nichts einfällt."
step3_1: "Fertig mit dem Senden deiner ersten Notiz?"
step3_2: "Falls deine Notiz nun in deiner Chronik auftaucht, hast du alles richtig gemacht."
step4_1: "Notizen können zusätzlich mit \"Reaktionen\" ausgestattet werden."
step4_2: "Um eine Reaktion anzufügen, klicke auf das „+“-Symbol einer Notiz und wähle ein Emoji aus, mit dem du reagieren möchtest."
_2fa:
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert."
registerTOTP: "Authentifizierungs-App registrieren"
@ -2054,6 +2052,9 @@ _notification:
checkNotificationBehavior: "Aussehen von Benachrichtigungen überprüfen"
sendTestNotification: "Testbenachrichtigung senden"
notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus"
reactedBySomeUsers: "{n} Benutzer haben eine Reaktion geschickt"
renotedBySomeUsers: "Renote von {n} Benutzern"
followedBySomeUsers: "Von {n} Benutzern gefolgt"
_types:
all: "Alle"
note: "Neue Notizen"

View File

@ -983,6 +983,7 @@ assign: "Assign"
unassign: "Unassign"
color: "Color"
manageCustomEmojis: "Manage Custom Emojis"
manageAvatarDecorations: "Manage avatar decorations"
youCannotCreateAnymore: "You've hit the creation limit."
cannotPerformTemporary: "Temporarily unavailable"
cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again."
@ -1156,6 +1157,12 @@ detach: "Remove"
angle: "Angle"
flip: "Flip"
showAvatarDecorations: "Show avatar decorations"
releaseToRefresh: "Release to refresh"
refreshing: "Refreshing..."
pullDownToRefresh: "Pull down to refresh"
disableStreamingTimeline: "Disable real-time timeline updates"
useGroupedNotifications: "Display grouped notifications"
cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided."
_announcement:
forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1177,7 +1184,6 @@ _initialAccountSetting:
pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device."
initialAccountSettingCompleted: "Profile setup complete!"
haveFun: "Enjoy {name}!"
ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}."
skipAreYouSure: "Really skip profile setup?"
laterAreYouSure: "Really do profile setup later?"
_serverRules:
@ -1492,6 +1498,7 @@ _role:
inviteLimitCycle: "Invite limit cooldown"
inviteExpirationTime: "Invite expiration interval"
canManageCustomEmojis: "Can manage custom emojis"
canManageAvatarDecorations: "Manage avatar decorations"
driveCapacity: "Drive capacity"
alwaysMarkNsfw: "Always mark files as NSFW"
pinMax: "Maximum number of pinned notes"
@ -1612,6 +1619,7 @@ _aboutMisskey:
donate: "Donate to Misskey"
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
patrons: "Patrons"
projectMembers: "Project members"
_displayOfSensitiveMedia:
respect: "Hide media marked as sensitive"
ignore: "Display media marked as sensitive"
@ -2065,6 +2073,9 @@ _notification:
checkNotificationBehavior: "Check notification appearance"
sendTestNotification: "Send test notification"
notificationWillBeDisplayedLikeThis: "Notifications look like this"
reactedBySomeUsers: "{n} users reacted"
renotedBySomeUsers: "Renote from {n} users"
followedBySomeUsers: "Followed by {n} users"
_types:
all: "All"
note: "New notes"

View File

@ -1161,7 +1161,6 @@ _initialAccountSetting:
pushNotificationDescription: "Habilitar las notificaciones push te permitirá recibir notificaciones de {name} directamente en tu dispositivo."
initialAccountSettingCompleted: "¡Configuración del perfil completada!"
haveFun: "¡Disfruta de {name}!"
ifYouNeedLearnMore: "Si quieres aprender cómo usar {name} (Misskey), por favor, visita {link}."
skipAreYouSure: "¿Realmente quieres saltarte la configuración del perfil?"
laterAreYouSure: "¿Realmente quieres configurar tu perfil después?"
_serverRules:
@ -1725,16 +1724,6 @@ _time:
minute: "Minutos"
hour: "Horas"
day: "Días"
_timelineTutorial:
title: "Cómo usar Misskey"
step1_1: "Ésta es la \"línea de tiempo\". Todas las \"notas\" que sean publicadas en {name} serán mostradas cronológicamente aquí."
step1_2: "Hay varias líneas de tiempo. Por ejemplo, la línea temporal \"Inicio\" contiene las notas de otros usuarios que sigues, y la línea \"Local\" contandrá las notas de todos los usuarios de {name}."
step2_1: "Ahora probemos publicar una nota. Puedes hacerlo presionando el botón que tiene un ícono de lápiz."
step2_2: "¿Qué tal si escribimos una introducción? o sólo un \"¡Hola {name}!\" ¿No te apetece?"
step3_1: "¿Terminaste de publicar tu primera nota?"
step3_2: "Tu primera nota ahora se mostrará en tu línea de tiempo."
step4_1: "También puedes añadir \"Reacciones\" a notas."
step4_2: "Para añadir una reacción selecciona el botón \"+\" en la nota y escoge el emoji que quieras para reaccionar."
_2fa:
alreadyRegistered: "Ya has completado la configuración."
registerTOTP: "Registrar aplicación autenticadora"

View File

@ -693,7 +693,7 @@ repliesCount: "Nombre de réponses envoyées"
renotesCount: "Nombre de notes que vous avez renotées"
repliedCount: "Nombre de réponses reçues"
renotedCount: "Nombre de vos notes renotées"
followingCount: "Nombre de comptes suivis"
followingCount: "Nombre d'abonnements"
followersCount: "Nombre d'abonnés"
sentReactionsCount: "Nombre de réactions envoyées"
receivedReactionsCount: "Nombre de réactions reçues"
@ -781,7 +781,7 @@ addDescription: "Ajouter une description"
userPagePinTip: "Vous pouvez afficher des notes ici en sélectionnant l'option « Épingler au profil » dans le menu de chaque note."
notSpecifiedMentionWarning: "Vous avez mentionné des utilisateur·rice·s qui ne font pas partie de la liste des destinataires"
info: "Informations"
userInfo: "Informations sur l'utilisateur"
userInfo: "Informations sur l'utilisateur·rice"
unknown: "Inconnu"
onlineStatus: "Statut"
hideOnlineStatus: "Se rendre invisible"
@ -970,6 +970,7 @@ assign: "Attribuer"
unassign: "Retirer"
color: "Couleur"
manageCustomEmojis: "Gestion des émojis personnalisés"
manageAvatarDecorations: "Gérer les décorations d'avatar"
youCannotCreateAnymore: "Vous avez atteint la limite de création."
cannotPerformTemporary: "Temporairement indisponible"
invalidParamError: "Paramètres invalides"
@ -1022,12 +1023,12 @@ continue: "Continuer"
preservedUsernames: "Noms d'utilisateur·rice réservés"
archive: "Archive"
displayOfNote: "Affichage de la note"
initialAccountSetting: "Réglage initial du profil"
initialAccountSetting: "Configuration initiale du profil"
youFollowing: "Abonné·e"
preventAiLearning: "Refuser l'usage dans l'apprentissage automatique d'IA générative"
preventAiLearningDescription: "Demander aux robots d'indexation de ne pas utiliser le contenu publié, tel que les notes et les images, dans l'apprentissage automatique d'IA générative. Cela est réalisé en incluant le drapeau « noai » dans la réponse HTML. Une prévention complète n'est toutefois pas possible, car il est au robot d'indexation de respecter cette demande."
options: "Options"
specifyUser: "Spécifier l'utilisateur"
specifyUser: "Spécifier l'utilisateur·rice"
failedToPreviewUrl: "Aperçu d'URL échoué"
update: "Mettre à jour"
later: "Plus tard"
@ -1052,7 +1053,11 @@ pinnedList: "Liste épinglée"
notifyNotes: "Notifier à propos des nouvelles notes"
authentication: "Authentification"
authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer"
dateAndTime: "Date et heure"
showRenotes: "Afficher les renotes"
edited: "Modifié"
notificationRecieveConfig: "Paramètres des notifications"
mutualFollow: "Abonnement mutuel"
showRepliesToOthersInTimeline: "Afficher les réponses aux autres dans le fil"
hideRepliesToOthersInTimeline: "Masquer les réponses aux autres dans le fil"
showRepliesToOthersInTimelineAll: "Afficher les réponses de toutes les personnes que vous suivez dans le fil"
@ -1072,16 +1077,21 @@ detach: "Enlever"
angle: "Angle"
flip: "Inverser"
showAvatarDecorations: "Afficher les décorations d'avatar"
releaseToRefresh: "Relâcher pour rafraîchir"
refreshing: "Rafraîchissement..."
pullDownToRefresh: "Tirer vers le bas pour rafraîchir"
disableStreamingTimeline: "Désactiver les mises à jour en temps réel de la ligne du temps"
useGroupedNotifications: "Grouper les notifications"
_announcement:
readConfirmTitle: "Marquer comme lu ?"
_initialAccountSetting:
profileSetting: "Paramètres du profil"
privacySetting: "Paramètres de confidentialité"
initialAccountSettingCompleted: "Configuration du profil terminée avec succès !"
ifYouNeedLearnMore: "Si vous voulez en savoir plus comment utiliser {name}(Misskey), veuillez visiter {link}."
skipAreYouSure: "Désirez-vous ignorer la configuration du profile ?"
skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?"
_serverSettings:
iconUrl: "URL de licône"
fanoutTimelineDescription: "Si activée, la performance de la récupération de la chronologie augmentera considérablement et la charge sur la base de données sera réduite. En revanche, l'utilisation de la mémoire de Redis augmentera. Considérez désactiver cette option si le serveur est bas en mémoire ou instable."
_accountMigration:
moveFrom: "Migrer un autre compte vers le présent compte"
moveFromSub: "Créer un alias vers un autre compte"
@ -1148,9 +1158,16 @@ _achievements:
description: "Rendre votre compte comme un chat"
flavor: "Je n'ai pas encore de nom"
_following1:
title: "Vous suivez votre premier utilisateur·rice"
title: "Vous suivez votre premier·ère utilisateur·rice"
_following10:
description: "S'abonner à plus de 10 utilisateur·rice·s"
_following50:
title: "Beaucoup d'amis"
description: "S'abonner à plus de 50 utilisateur·rice·s"
_following100:
description: "S'abonner à plus de 100 utilisateur·rice·s"
_following300:
description: "S'abonner à plus de 300 utilisateur·rice·s"
_followers10:
title: "Abonnez-moi !"
description: "Obtenir plus de 10 abonné·e·s"
@ -1230,6 +1247,7 @@ _role:
high: "Haute"
_options:
canManageCustomEmojis: "Gestion des émojis personnalisés"
canManageAvatarDecorations: "Gestion des décorations d'avatar"
wordMuteMax: "Nombre maximal de caractères dans le filtre de mots"
_sensitiveMediaDetection:
description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement."
@ -1264,8 +1282,10 @@ _ad:
back: "Retour"
reduceFrequencyOfThisAd: "Voir cette publicité moins souvent"
hide: "Cacher "
adsSettings: "Réglages des publicités"
adsSettings: "Paramètres des publicités"
notesPerOneAd: "Intervalle de diffusion de publicités lors de la mise à jour en temps réel (nombre de notes par publicité)"
setZeroToDisable: "Mettre cette valeur à 0 pour désactiver la diffusion de publicités lors de la mise à jour en temps réel"
adsTooClose: "L'expérience de l'utilisateur peut être gravement compromise par un intervalle de diffusion de publicités extrêmement court."
_forgotPassword:
enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte. Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette adresse."
ifNoEmail: "Si vous n'avez pas enregistré d'adresse e-mail, merci de contacter l'administrateur·rice de votre instance."
@ -1318,6 +1338,7 @@ _aboutMisskey:
donate: "Soutenir Misskey"
morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰"
patrons: "Contributeurs"
projectMembers: "Membres du projet"
_displayOfSensitiveMedia:
force: "Masquer tous les médias"
_instanceTicker:
@ -1447,9 +1468,6 @@ _time:
minute: "min"
hour: "h"
day: "j"
_timelineTutorial:
title: "Comment utiliser Misskey"
step3_1: "Avez-vous publié votre première note ?"
_2fa:
alreadyRegistered: "Configuration déjà achevée."
step1: "Tout d'abord, installez une application d'authentification, telle que {a} ou {b}, sur votre appareil."
@ -1613,6 +1631,7 @@ _exportOrImport:
userLists: "Listes"
excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine"
excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs"
withReplies: "Inclure les réponses des utilisateur·rice·s importé·e·s dans le fil"
_charts:
federation: "Fédération"
apRequest: "Requêtes"
@ -1709,13 +1728,16 @@ _notification:
youGotReply: "Réponse de {name}"
youGotQuote: "Cité·e par {name}"
youRenoted: "{name} vous a Renoté"
youWereFollowed: "Vous suit"
youWereFollowed: "s'est abonné·e à vous"
youReceivedFollowRequest: "Vous avez reçu une demande dabonnement"
yourFollowRequestAccepted: "Votre demande dabonnement a été accepté"
pollEnded: "Les résultats du sondage sont disponibles"
unreadAntennaNote: "Antenne {name}"
emptyPushNotificationMessage: "Les notifications push ont été mises à jour"
achievementEarned: "Accomplissement"
reactedBySomeUsers: "{n} utilisateur·rice·s ont réagi"
renotedBySomeUsers: "{n} utilisateur·rice·s ont renoté"
followedBySomeUsers: "{n} utilisateur·rice·s se sont abonné·e·s à vous"
_types:
all: "Toutes"
follow: "Nouvel·le abonné·e"
@ -1774,7 +1796,7 @@ _moderationLogTypes:
addCustomEmoji: "Émoji personnalisé ajouté"
updateCustomEmoji: "Émoji personnalisé mis à jour"
deleteCustomEmoji: "Émoji personnalisé supprimé"
updateServerSettings: "Réglages du serveur mis à jour"
updateServerSettings: "Paramètres du serveur mis à jour"
updateUserNote: "Note de modération mise à jour"
deleteDriveFile: "Fichier supprimé"
deleteNote: "Note supprimée"

View File

@ -1161,7 +1161,6 @@ _initialAccountSetting:
pushNotificationDescription: "Menyalakan notifikasi dorong akan membuatmu menerima notifikasi dari {name} secara langsung ke perangkatmu."
initialAccountSettingCompleted: "Pengaturan profil selesai!"
haveFun: "Selamat menikmati, {name}!"
ifYouNeedLearnMore: "Kalau kamu ingin mempelajari lebih lanjut bagaimana cara menggunakan {name} (Misskey), silahkan kunjungi {link}."
skipAreYouSure: "Yakin melewati atur profil?"
laterAreYouSure: "Yakin banget untuk atur profil nanti?"
_serverRules:
@ -1725,16 +1724,6 @@ _time:
minute: "menit"
hour: "jam"
day: "hari"
_timelineTutorial:
title: "Bagaimana cara menggunakan Misskey"
step1_1: "Ini adalah \"lini masa\". Semua \"catatan\" yang dikirimkan oleh {name} akan dimunculkan secara kronologis di sini."
step1_2: "Ada beberapa lini masa yang berbeda. Seperti contoh, \"Lini masa Beranda\" berisi catatan dari pengguna yang kamu ikuti, dan \"Lini masa lokal\" berisi catatan dari semua pengguna dari {name}."
step2_1: "Selanjutnya, mari kita coba memposting sebuah catatan. Kamu dapat melakukanya dengan menekan tombol dengan ikon pensil."
step2_2: "Bagaimana dengan menuliskan sedikit perkenalan diri, atau hanya \"Hello {name}\" kalau kamu lagi ngga feeling?"
step3_1: "Udah selesai memposting catatan pertamamu?"
step3_2: "Catatan pertamamu seharusnya sekarang sudah tampil di lini masa kamu."
step4_1: "Kamu dapat menyisipkan \"Reaksi\" ke dalam catatan."
step4_2: "Untuk menyisipkan reaksi, tekan tanda \"+\" dalam catatan dan pilih emoji yang kamu suka untuk mereaksi catatan tersebut."
_2fa:
alreadyRegistered: "Kamu telah mendaftarkan perangkat autentikasi 2-faktor."
registerTOTP: "Daftarkan aplikasi autentikator"

92
locales/index.d.ts vendored
View File

@ -1195,6 +1195,8 @@ export interface Locale {
"pullDownToRefresh": string;
"disableStreamingTimeline": string;
"useGroupedNotifications": string;
"signupPendingError": string;
"cwNotationRequired": string;
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;
@ -1204,6 +1206,8 @@ export interface Locale {
"tooManyActiveAnnouncementDescription": string;
"readConfirmTitle": string;
"readConfirmText": string;
"shouldNotBeUsedToPresentPermanentInfo": string;
"dialogAnnouncementUxWarn": string;
};
"_initialAccountSetting": {
"accountCreated": string;
@ -1217,10 +1221,91 @@ export interface Locale {
"pushNotificationDescription": string;
"initialAccountSettingCompleted": string;
"haveFun": string;
"ifYouNeedLearnMore": string;
"youCanContinueTutorial": string;
"startTutorial": string;
"skipAreYouSure": string;
"laterAreYouSure": string;
};
"_initialTutorial": {
"launchTutorial": string;
"title": string;
"wellDone": string;
"skipAreYouSure": string;
"_landing": {
"title": string;
"description": string;
};
"_note": {
"title": string;
"description": string;
"reply": string;
"renote": string;
"reaction": string;
"menu": string;
};
"_reaction": {
"title": string;
"description": string;
"letsTryReacting": string;
"reactToContinue": string;
"reactNotification": string;
"reactDone": string;
};
"_timeline": {
"title": string;
"description1": string;
"home": string;
"local": string;
"social": string;
"global": string;
"description2": string;
"description3": string;
};
"_postNote": {
"title": string;
"description1": string;
"_visibility": {
"description": string;
"public": string;
"home": string;
"followers": string;
"direct": string;
"doNotSendConfidencialOnDirect1": string;
"doNotSendConfidencialOnDirect2": string;
"localOnly": string;
};
"_cw": {
"title": string;
"description": string;
"_exampleNote": {
"cw": string;
"note": string;
};
"useCases": string;
};
};
"_howToMakeAttachmentsSensitive": {
"title": string;
"description": string;
"tryThisFile": string;
"_exampleNote": {
"note": string;
};
"method": string;
"sensitiveSucceeded": string;
"doItToContinue": string;
};
"_done": {
"title": string;
"description": string;
};
};
"_timelineDescription": {
"home": string;
"local": string;
"social": string;
"global": string;
};
"_serverRules": {
"description": string;
};
@ -1568,6 +1653,10 @@ export interface Locale {
"title": string;
"description": string;
};
"_tutorialCompleted": {
"title": string;
"description": string;
};
};
};
"_role": {
@ -1783,6 +1872,7 @@ export interface Locale {
"notesCount": string;
"nameAndDescription": string;
"nameOnly": string;
"allowRenoteToExternal": string;
};
"_menuDisplay": {
"sideFull": string;

View File

@ -162,8 +162,8 @@ cacheRemoteSensitiveFiles: "Copia nella cache locale i file espliciti remoti"
cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file espliciti verranno richiesti direttamente all'istanza remota senza essere salvati nel server locale."
flagAsBot: "Io sono un robot"
flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot."
flagAsCat: "Sono un gatto"
flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo"
flagAsCat: "MIIaaaoo!!! (Io sono un gatto è un romanzo del 1905, il primo dello scrittore giapponese Natsume Sōseki)"
flagAsCatDescription: "Miaoo mia miao mi miao?"
flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline."
flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note"
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da profili che già segui"
@ -326,9 +326,9 @@ avatar: "Foto del profilo"
banner: "Intestazione"
displayOfSensitiveMedia: "Visibilità dei media espliciti"
whenServerDisconnected: "Quando la connessione col server è persa"
disconnectedFromServer: "Il server si è disconnesso"
disconnectedFromServer: "Connessione persa"
reload: "Ricarica"
doNothing: "Nessun'azione"
doNothing: "Ignora"
reloadConfirm: "Vuoi ricaricare?"
watch: "Osserva"
unwatch: "Smetti di Osserva"
@ -653,7 +653,7 @@ notificationSetting: "Impostazioni notifiche"
notificationSettingDesc: "Seleziona il tipo di notifiche da visualizzare."
useGlobalSetting: "Usa impostazioni generali"
useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifiche del profilo. Altrimenti si possono segliere impostazioni personalizzate."
other: "Avanzate"
other: "Ulteriori"
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."
setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi."
@ -979,6 +979,7 @@ assign: "Assegna"
unassign: "Disassegna"
color: "Colore"
manageCustomEmojis: "Gestisci le emoji personalizzate"
manageAvatarDecorations: "Gestire le decorazioni di foto del profilo"
youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite."
cannotPerformTemporary: "Indisponibilità temporanea"
cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi."
@ -1149,6 +1150,11 @@ detach: "Rimuovi"
angle: "Angolo"
flip: "Inverti"
showAvatarDecorations: "Mostra decorazione della foto profilo"
releaseToRefresh: "Rilascia per aggiornare"
refreshing: "Aggiornamento..."
pullDownToRefresh: "Trascina per aggiornare"
disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo reale"
useGroupedNotifications: "Mostra le notifiche raggruppate"
_announcement:
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."
@ -1170,7 +1176,6 @@ _initialAccountSetting:
pushNotificationDescription: "Attivare le notifiche push ti permettera di ricevere informazioni sulla attività di {name} direttamente sul tuo dispositivo."
initialAccountSettingCompleted: "Hai completato la configurazione iniziale!"
haveFun: "Divertiti con {name}!"
ifYouNeedLearnMore: "Per saperne di più su come usare {name} (Misskey), visita la pagina {link}"
skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?"
laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?"
_serverRules:
@ -1485,6 +1490,7 @@ _role:
inviteLimitCycle: "Intervallo di emissione del codice di invito"
inviteExpirationTime: "Scadenza del codice di invito"
canManageCustomEmojis: "Gestire le emoji personalizzate"
canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo"
driveCapacity: "Capienza del Drive"
alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
pinMax: "Quantità massima di Note in primo piano"
@ -1604,6 +1610,7 @@ _aboutMisskey:
donate: "Sostieni Misskey"
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
patrons: "Sostenitori"
projectMembers: "Partecipanti al progetto"
_displayOfSensitiveMedia:
respect: "Nascondere i media espliciti"
ignore: "Non nascondere i media espliciti"
@ -1735,16 +1742,6 @@ _time:
minute: "min"
hour: "ore"
day: "giorni"
_timelineTutorial:
title: "Come usare Misskey"
step1_1: "Questa è la \"Timeline\". tutte le \"Note\" pubblicate su {name} vengono elencate in ordine cronologico."
step1_2: "Le Timeline sono diverse, ad esempio, la \"Home\" elenca le Note dei profili che segui. Quella \"Locale\" elenca quelle di tutti i profili attivi su {name}."
step2_1: "Prova a pubblicare una Nota. Semplicemente premendo il bottone con l'icona di una matita."
step2_2: "Potresti scrivere la tua presentazione, oppure semplicemente \"Ciao da {name}!\""
step3_1: "Hai pubblicato qualcosa?"
step3_2: "In tal caso, dovrebbe comparire subito nella tua \"Home\""
step4_1: "Puoi reagire con un emoji alle Note."
step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with.\nPer reagire con una emoji, premi il bottone \"+\" (più) visibile vicino ad ogni Nota e scegli dall'elenco la emoji che rappresenta la tua reazione."
_2fa:
alreadyRegistered: "La configurazione è stata già completata."
registerTOTP: "Registra un'app di autenticazione"
@ -1844,14 +1841,14 @@ _widgets:
calendar: "Calendario"
trends: "Di tendenza"
clock: "Orologio"
rss: "Aggregatore rss"
rssTicker: "Ticker RSS"
rss: "Lettura RSS"
rssTicker: "Nastro RSS"
activity: "Attività"
photos: "Foto"
digitalClock: "Orologio digitale"
unixClock: "Orologio UNIX"
federation: "Federazione"
instanceCloud: "Istanza Cloud"
instanceCloud: "Nuvola di federazione"
postForm: "Finestra di pubblicazione"
slideshow: "Diapositive"
button: "Pulsante"
@ -1867,7 +1864,7 @@ _widgets:
clicker: "Cliccaggio"
_cw:
hide: "Nascondere"
show: "Apri..."
show: "Attenzione: continua la lettura"
chars: "{count} caratteri"
files: "{count} file"
_poll:
@ -2054,6 +2051,9 @@ _notification:
checkNotificationBehavior: "Prova il comportamento della notifica"
sendTestNotification: "Spedisci una notifica di prova"
notificationWillBeDisplayedLikeThis: "La notifica apparirà così"
reactedBySomeUsers: "{n} reazioni"
renotedBySomeUsers: "{n} Rinota"
followedBySomeUsers: "{n} nuovi follower"
_types:
all: "Tutto"
note: "Nuove Note"

View File

@ -1192,6 +1192,8 @@ refreshing: "リロード中"
pullDownToRefresh: "引っ張ってリロード"
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
useGroupedNotifications: "通知をグルーピングして表示する"
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
_announcement:
forExistingUsers: "既存ユーザーのみ"
@ -1202,10 +1204,12 @@ _announcement:
tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。"
readConfirmTitle: "既読にしますか?"
readConfirmText: "「{title}」の内容を読み、既読にします。"
shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。"
dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。"
_initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!"
letsStartAccountSetup: "アカウントの初期設定を行いましょう。"
letsStartAccountSetup: "さっそくアカウントの初期設定を行いましょう。"
letsFillYourProfile: "まずはあなたのプロフィールを設定しましょう。"
profileSetting: "プロフィール設定"
privacySetting: "プライバシー設定"
@ -1215,10 +1219,80 @@ _initialAccountSetting:
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。"
initialAccountSettingCompleted: "初期設定が完了しました!"
haveFun: "{name}をお楽しみください!"
ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。"
youCanContinueTutorial: "このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。"
startTutorial: "チュートリアルを開始"
skipAreYouSure: "初期設定をスキップしますか?"
laterAreYouSure: "初期設定をあとでやり直しますか?"
_initialTutorial:
launchTutorial: "チュートリアルを見る"
title: "チュートリアル"
wellDone: "よくできました"
skipAreYouSure: "チュートリアルを終了しますか?"
_landing:
title: "チュートリアルへようこそ"
description: "ここでは、Misskeyの基本的な使い方や機能を確認できます。"
_note:
title: "ノートって何?"
description: "Misskeyでの投稿は「ート」と呼びます。ートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。"
reply: "返信することができます。返信に対しての返信も可能で、スレッドのように会話を続けることもできます。"
renote: "そのノートを自分のタイムラインに流して共有することができます。テキストを追加して引用することも可能です。"
reaction: "リアクションをつけることができます。詳しくは次のページで解説します。"
menu: "ノートの詳細を表示したり、リンクをコピーしたりなどの様々な操作が行えます。"
_reaction:
title: "リアクションって何?"
description: "ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。"
letsTryReacting: "リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!"
reactToContinue: "リアクションをつけると先に進めるようになります。"
reactNotification: "あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。"
reactDone: "「ー」ボタンを押すとリアクションを取り消すことができます。"
_timeline:
title: "タイムラインのしくみ"
description1: "Misskeyには、使い方に応じて複数のタイムラインが用意されていますサーバーによってはいずれかが無効になっていることがあります。"
home: "あなたがフォローしているアカウントの投稿を見られます。"
local: "このサーバーにいるユーザー全員の投稿を見られます。"
social: "ホームタイムラインとローカルタイムラインの投稿が両方表示されます。"
global: "接続している他のすべてのサーバーからの投稿を見られます。"
description2: "それぞれのタイムラインは、画面上部でいつでも切り替えられます。"
description3: "その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。"
_postNote:
title: "ノートの投稿設定"
description1: "Misskeyにートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。"
_visibility:
description: "ノートを表示できる相手を制限できます。"
public: "すべてのユーザーに公開。"
home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。"
followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。"
direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。"
doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。"
doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。"
localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。"
_cw:
title: "内容を隠すCW"
description: "本文のかわりに「注釈」に書いた内容が表示されます。「もっと見る」を押すと本文が表示されます。"
_exampleNote:
cw: "飯テロ注意"
note: "チョコのかかったドーナツを食べました🍩😋"
useCases: "サーバーのガイドラインにより必要とされるノートに指定したり、ネタバレ投稿やセンシティブな文章を自主規制したりするときに使います。"
_howToMakeAttachmentsSensitive:
title: "添付ファイルをセンシティブにするには?"
description: "サーバーのガイドラインにより必要とされる際や、そのまま見れる状態にしておくべきではない添付ファイルには、「センシティブ」設定を付けます。"
tryThisFile: "試しに、このフォームに添付された画像をセンシティブにしてみてください!"
_exampleNote:
note: "納豆のフタ開けるのミスったわね…"
method: "添付ファイルをセンシティブにする際は、そのファイルをクリックしてメニューを開き、「センシティブとして設定」をクリックします。"
sensitiveSucceeded: "ファイルを添付する際は、サーバーのガイドラインに従ってセンシティブを適切に設定してください。"
doItToContinue: "画像をセンシティブに設定すると先に進めるようになります。"
_done:
title: "チュートリアルは終了です🎉"
description: "ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。"
_timelineDescription:
home: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。"
local: "ローカルタイムラインでは、このサーバーにいるユーザー全員の投稿を見られます。"
social: "ソーシャルタイムラインには、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。"
global: "グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。"
_serverRules:
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
@ -1491,6 +1565,9 @@ _achievements:
_smashTestNotificationButton:
title: "テスト過剰"
description: "通知のテストをごく短時間のうちに連続して行った"
_tutorialCompleted:
title: "Misskey初心者講座 修了証"
description: "チュートリアルを完了した"
_role:
new: "ロールの作成"
@ -1592,7 +1669,7 @@ _ffVisibility:
_signup:
almostThere: "ほとんど完了です"
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。"
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。"
_accountDelete:
accountDelete: "アカウントの削除"
@ -1700,6 +1777,7 @@ _channel:
notesCount: "{n}投稿があります"
nameAndDescription: "名前と説明"
nameOnly: "名前のみ"
allowRenoteToExternal: "チャンネル外へのリノートと引用リノートを許可する"
_menuDisplay:
sideFull: "横"

View File

@ -1133,6 +1133,9 @@ fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "タイムラインに他の人への返信とかも含めんで"
hideRepliesToOthersInTimeline: "タイムラインに他の人への返信とかは見ーへんで"
showRepliesToOthersInTimelineAll: ""
hideRepliesToOthersInTimelineAll: ""
confirmShowRepliesAll: ""
confirmHideRepliesAll: ""
externalServices: "他のサイトのサービス"
impressum: "運営者の情報"
impressumUrl: "運営者の情報URL"
@ -1141,7 +1144,11 @@ privacyPolicy: "プライバシーポリシー"
privacyPolicyUrl: "プライバシーポリシーURL"
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
avatarDecorations: "アイコンデコレーション"
attach: ""
detach: ""
angle: ""
flip: "反転"
showAvatarDecorations: ""
_announcement:
forExistingUsers: "もうおるユーザーのみ"
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
@ -1163,7 +1170,6 @@ _initialAccountSetting:
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をあんたのデバイスで受け取れるで。"
initialAccountSettingCompleted: "初期設定が終わったで。"
haveFun: "{name}、楽しんでな~"
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
skipAreYouSure: "初期設定飛ばすか?"
laterAreYouSure: "初期設定あとでやり直すん?"
_serverRules:
@ -1177,6 +1183,7 @@ _serverSettings:
manifestJsonOverride: "manifest.jsonのオーバーライド"
shortName: "略称"
shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。"
fanoutTimelineDescription: ""
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromSub: "別のアカウントへエイリアスを作る"
@ -1596,6 +1603,7 @@ _aboutMisskey:
donate: "Misskeyに寄付"
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
patrons: "支援者"
projectMembers: ""
_displayOfSensitiveMedia:
respect: "きわどいのは見とうない"
ignore: "きわどいのも見たい"
@ -1727,16 +1735,6 @@ _time:
minute: "分"
hour: "時間"
day: "日"
_timelineTutorial:
title: "Misskeyってなんや"
step1_1: "これは「タイムライン」や。{name}に投稿された「ノート」が順番に表示されるで。"
step1_2: "タイムラインには何個か種類があってな、例えば「ホームタイムライン」だったらあんたのフォローしてる人のノート、「ローカルタイムライン」には{name}全部のノートが流れてくるで。"
step2_1: "試しに、何かノートを投稿してみ。画面の鉛筆マークのボタンでフォームが開くで。"
step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。"
step3_1: "投稿できた?"
step3_2: "あんたのノートがタイムラインに出てきたら成功や。"
step4_1: "ノートには、「ツッコミ」を付けれるで。"
step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶんやで。"
_2fa:
alreadyRegistered: "もう設定終わっとるわ。"
registerTOTP: "認証アプリの設定はじめる"
@ -2169,7 +2167,21 @@ _externalResourceInstaller:
_theme:
title: "このテーマインストールする?"
metaTitle: "テーマ情報"
_meta:
base: ""
_vendorInfo:
title: ""
endpoint: ""
hashVerify: ""
_errors:
_invalidParams:
title: ""
description: ""
_resourceTypeNotSupported:
title: ""
description: ""
_failedToFetch:
title: ""
_pluginParseFailed:
title: "AiScriptエラー起こしてもうたねん"
description: "データは取得できたものの、AiScript解析時にエラーがあったから読み込めへんかってん。すまんが、プラグインを作った人に問い合わせてくれへんごめんな。エラーの詳細はJavaScriptコンソール読んでな。"

View File

@ -1123,6 +1123,7 @@ edited: "수정됨"
notificationRecieveConfig: "알림 설정"
mutualFollow: "맞팔로우"
flip: "플립"
useGroupedNotifications: "알림을 그룹화하고 표시"
_announcement:
forExistingUsers: "기존 유저에게만 알림"
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
@ -1144,7 +1145,6 @@ _initialAccountSetting:
pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다."
initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!"
haveFun: "{name}와 함께 즐거운 시간 보내세요!"
ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요."
skipAreYouSure: "초기 설정을 중단하시겠습니까?"
laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?"
_serverRules:
@ -1699,16 +1699,6 @@ _time:
minute: "분"
hour: "시간"
day: "일"
_timelineTutorial:
title: "Misskey의 사용 방법"
step1_1: "이것은 '타임라인'입니다. {name}에 게시된 '노트'가 시간 순서대로 표시됩니다."
step1_2: "타임라인은 몇 가지 종류로 나뉩니다. 그 중에 '홈 타임라인'은 내가 팔로우한 사람의 노트가 표시되며, '로컬 타임라인'에는 {name} 의 모든 노트가 표시됩니다."
step2_1: "그럼 시험삼아 노트를 작성해 봅시다. 화면에 있는 연필 버튼을 눌러 보세요."
step2_2: "첫 노트이니까 자기소개, 혹은 가볍게 \"안녕 {name}\"라고 올려 보는 건 어떨까요?"
step3_1: "노트 작성을 끝내셨나요?"
step3_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다."
step4_1: "노트에는 '리액션'을 붙일 수 있습니다."
step4_2: "리액션을 붙이려면, 노트의 \"+\" 버튼을 클릭하고 원하는 이모지를 선택합니다."
_2fa:
alreadyRegistered: "이미 설정이 완료되었습니다."
registerTOTP: "인증 앱 설정 시작"
@ -2014,6 +2004,9 @@ _notification:
checkNotificationBehavior: "알림 표시를 체크하기"
sendTestNotification: "테스트 알림 보내기"
notificationWillBeDisplayedLikeThis: "알림이 이렇게 표시됩니다"
reactedBySomeUsers: "{n}명이 반응했습니다"
renotedBySomeUsers: "{n}명이 리노트했습니다"
followedBySomeUsers: "{n}명에게 팔로우됨"
_types:
all: "전부"
follow: "팔로잉"

View File

@ -45,6 +45,7 @@ pin: "Vastmaken aan profielpagina"
unpin: "Losmaken van profielpagina"
copyContent: "Kopiëren inhoud"
copyLink: "Kopiëren link"
copyLinkRenote: ""
delete: "Verwijderen"
deleteAndEdit: "Verwijderen en bewerken"
deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop."

View File

@ -601,9 +601,6 @@ _time:
minute: "Minutter"
hour: "Timer"
day: "Dager"
_timelineTutorial:
title: "Hvordan bruke Misskey"
step2_2: "Hva med å skrive en selvpresentasjon, eller bare \"Hei {name}!\" hvis du ikke har lyst?"
_2fa:
renewTOTPCancel: "Avbryt"
_weekday:

View File

@ -1323,8 +1323,6 @@ _sfx:
notification: "Notificações"
_ago:
invalid: "Não há nada aqui"
_timelineTutorial:
step1_2: "Existem vários tipos de linhas do tempo, por exemplo, na 'Linha do Tempo Principal', você verá as notas das pessoas que está seguindo, e na 'Linha do Tempo Local', verá todas as notas de {name}."
_2fa:
securityKeyInfo: "Além da autenticação por impressão digital ou PIN, você também pode configurar a autenticação por chaves de segurança de hardware compatível com FIDO2 para proteger ainda mais a sua conta."
removeKeyConfirm: "Deseja excluir {name}?"

View File

@ -1587,16 +1587,6 @@ _time:
minute: "мин"
hour: "ч"
day: "сут"
_timelineTutorial:
title: "Как пользоваться Misskey"
step1_1: "Это лицо Misskey, так называемая лента. Ваш инстанс, {name}, покажет тут все опубликованные на нём заметки в хронологическом порядке."
step1_2: "Здесь есть несколько лент. К примеру «персональная» лента отображает заметки тех, на кого вы подписаны. А «местная» — заметки тех, кого приютил {name}."
step2_1: "Что ж, теперь самое время опубликовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста."
step2_2: "Почему бы не написать немного о себе? Ну, или хотя бы «Привет, {name}»?"
step3_1: "Справились с первой заметкой?"
step3_2: "Отлично, теперь она должна появиться в вашей ленте."
step4_1: "А ещё здесь можно делиться своими реакциями на заметки."
step4_2: "Отмечайте реакции, нажимая на символ «+» под заметкой и выбирая значок по душе."
_2fa:
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
registerTOTP: "Начните настраивать приложение-аутентификатор"

View File

@ -1154,7 +1154,6 @@ _initialAccountSetting:
pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ"
initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!"
haveFun: "ขอให้สนุก {name}!"
ifYouNeedLearnMore: "ถ้าหากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ {ชื่อ} (Misskey) กรุณาไปที่ {link}"
skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?"
laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?"
_serverRules:
@ -1713,16 +1712,6 @@ _time:
minute: "นาที"
hour: "ชั่วโมง"
day: "วัน"
_timelineTutorial:
title: "วิธีใช้งาน Misskey"
step1_1: "นี่คือ \"ไทม์ไลน์\" \"โน้ต\" ทั้งหมดที่ส่งใน {name} จะแสดงรายการตามลำดับเวลาที่นี่นะ"
step1_2: "อาจจะมีไทม์ไลน์ที่แตกต่างกันเล็กน้อยยกตัวอย่างเช่น \"ไทม์ไลน์หน้าแรก\" จะมีโน้ตของผู้ใช้ที่คุณติดตามและ \"ไทม์ไลน์ท้องถิ่น\" จะมีโน้ตจากผู้ใช้ทั้งหมดของ {name}"
step2_1: "มาลองโพสต์โน้ตต่อไปกัน คุณสามารถทำได้โดยการกดปุ่มที่มีไอคอนดินสอ"
step2_2: "ยังไงไหนลองเขียนแนะนำตัวเองหรือแค่ \"สวัสดี {name}!\" ถ้าคุณไม่รู้สึกเหมือนมัน?"
step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?"
step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ"
step4_1: "คุณสามารถเพิ่ม \"การตอบสนอง\" ในโน้ตได้"
step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย"
_2fa:
alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว"
registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์"

View File

@ -1067,7 +1067,6 @@ _initialAccountSetting:
pushNotificationDescription: "Bật thông báo đẩy sẽ cho phép bạn nhận thông báo từ {name} trực tiếp từ thiết bị của bạn."
initialAccountSettingCompleted: "Thiết lập tài khoản thành công!"
haveFun: "Hãy tận hưởng {name} nhé!"
ifYouNeedLearnMore: "Nếu bạn muốn tìm hiểu thêm về cách sử dụng {name} (Misskey), hãy vào {link}."
skipAreYouSure: "Bạn thực sự muốn bỏ qua mục thiết lập tài khoản?"
laterAreYouSure: "Bạn thực sự muốn thiết lập tài khoản vào lúc khác?"
_serverSettings:
@ -1503,9 +1502,6 @@ _time:
minute: "phút"
hour: "giờ"
day: "ngày"
_timelineTutorial:
step4_1: "Bạn có thể thêm \"Reaction\" vào ghi chú"
step4_2: "Khi thêm biểu cảm hãy nhấn dấu \"+\""
_2fa:
alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước."
registerTOTP: "Đăng ký ứng dụng xác thực"

View File

@ -1153,7 +1153,6 @@ _initialAccountSetting:
pushNotificationDescription: "启用推送通知的话,就可以在设备上接收来自 {name} 的通知了。"
initialAccountSettingCompleted: "初始设定已经完成了!"
haveFun: "希望 {name} 在这里玩得开心!"
ifYouNeedLearnMore: "关于 {name}(Misskey) 的使用方法,详见 {link}。"
skipAreYouSure: "要跳过初始设置吗?"
laterAreYouSure: "要稍后再进行初始设定吗?"
_serverRules:
@ -1712,16 +1711,6 @@ _time:
minute: "分"
hour: "小时"
day: "日"
_timelineTutorial:
title: "Misskey 的使用方法"
step1_1: "这个画面是「时间线」。{name}的投稿会按照帖子的发布时间顺序来显示。"
step1_2: "时间线有许多种类,比如在「首页时间线」中展现的是你关注的人的贴文;而在「本地时间线」中展现的是{name}里全部用户的贴文。"
step2_1: "那么接下来,试着写一些什么东西来发布吧!你可以通过点击屏幕上的铅笔图标来打开投稿页面。"
step2_2: "第一次发布的帖子内容,建议包含自我介绍,以及「开始使用{name}了」。"
step3_1: "将想说的话发出去了吗?"
step3_2: "太棒了!现在你可以在你的时间线中看到刚刚发布的帖子了。"
step4_1: "试着对帖子使用「回应」吧!"
step4_2: "在他人的帖子上按下「+」图标,即可选择想要的表情来进行「回应」。"
_2fa:
alreadyRegistered: "此设备已被注册"
registerTOTP: "开始设置认证应用"

View File

@ -161,7 +161,7 @@ youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
flagAsBot: "此使用者是機器人"
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Misskey內部系統將本帳戶識別為機器人"
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整 Misskey 內部系統將本帳戶識別為機器人"
flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
@ -979,6 +979,7 @@ assign: "指派"
unassign: "取消指派"
color: "顏色"
manageCustomEmojis: "管理自訂表情符號"
manageAvatarDecorations: "管理頭像裝飾"
youCannotCreateAnymore: "您無法再建立更多了。"
cannotPerformTemporary: "暫時無法進行"
cannotPerformTemporaryDescription: "由於超過操作次數限制,因此暫時無法進行。請稍後再嘗試。"
@ -1030,7 +1031,7 @@ retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端使用者的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "新增摘錄至貼文"
reactionsDisplaySize: "表情回應的顯示尺寸"
reactionsDisplaySize: "應的顯示尺寸"
noteIdOrUrl: "貼文ID或URL"
video: "影片"
videos: "影片"
@ -1125,7 +1126,7 @@ unnotifyNotes: "關閉貼文通知"
authentication: "驗證"
authenticationRequiredToContinue: "請於繼續前完成驗證"
dateAndTime: "日期與時間"
showRenotes: "顯示轉發貼文"
showRenotes: "顯示其他人的轉發貼文"
edited: "已編輯"
notificationRecieveConfig: "接受通知的設定"
mutualFollow: "互相追隨"
@ -1149,6 +1150,12 @@ detach: "取下"
angle: "角度"
flip: "翻轉"
showAvatarDecorations: "顯示頭像裝飾"
releaseToRefresh: "放開以更新內容"
refreshing: "載入更新中"
pullDownToRefresh: "往下拉來更新內容"
disableStreamingTimeline: "停用時間軸的即時更新"
useGroupedNotifications: "分組顯示通知訊息"
cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。"
_announcement:
forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
@ -1170,7 +1177,6 @@ _initialAccountSetting:
pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。"
initialAccountSettingCompleted: "初始設定完成了!"
haveFun: "盡情享受{name}吧!"
ifYouNeedLearnMore: "請瀏覽{link}以更瞭解{name}Misskey的使用方法。"
skipAreYouSure: "要略過初始設定嗎?"
laterAreYouSure: "稍後再重新進行初始設定嗎?"
_serverRules:
@ -1485,6 +1491,7 @@ _role:
inviteLimitCycle: "邀請碼的發放間隔"
inviteExpirationTime: "邀請碼的有效日期"
canManageCustomEmojis: "管理自訂表情符號"
canManageAvatarDecorations: "管理頭像裝飾"
driveCapacity: "雲端硬碟容量"
alwaysMarkNsfw: "總是將檔案標記為NSFW"
pinMax: "置頂貼文的最大數量"
@ -1604,6 +1611,7 @@ _aboutMisskey:
donate: "贊助 Misskey"
morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
patrons: "贊助者"
projectMembers: "專案成員"
_displayOfSensitiveMedia:
respect: "隱藏敏感檔案"
ignore: "顯示敏感檔案"
@ -1735,16 +1743,6 @@ _time:
minute: "分鐘"
hour: "小時"
day: "日"
_timelineTutorial:
title: "Misskey 的使用方法"
step1_1: "這個畫面是「時間軸」。發佈到{name}的「貼文」會按照時間順序顯示。"
step1_2: "時間軸有多種類型,例如「首頁時間軸」是您追蹤帳戶的貼文、「本地時間軸」是{name}內所有帳戶的貼文。"
step2_1: "不如現在就嘗試發文吧!按鉛筆圖示的按鈕開啟發文頁面。"
step2_2: "您可以在第一篇貼文裡寫自我介紹,或是「我來到 {name} 了」之類的話。"
step3_1: "貼文發出去了嗎?"
step3_2: "如果您的貼文出現在時間軸上,就代表發文成功。"
step4_1: "可以對貼文標記「反應」。"
step4_2: "點擊貼文的「+」圖示,即可選擇表情符號來反應。"
_2fa:
alreadyRegistered: "此裝置已被註冊過了"
registerTOTP: "開始設定驗證應用程式"
@ -2054,6 +2052,9 @@ _notification:
checkNotificationBehavior: "確認通知的顯示行為"
sendTestNotification: "發送測試通知"
notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示"
reactedBySomeUsers: "{n}人做出了反應"
renotedBySomeUsers: "{n}人做了轉發"
followedBySomeUsers: "被{n}人追隨了"
_types:
all: "全部 "
note: "使用者的最新貼文"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2023.11.0-beta.8-PrisMisskey.1",
"version": "2023.11.0-beta.9-PrisMisskey.1",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AddAllowRenoteToExternal1698840138000 {
name = 'AddAllowRenoteToExternal1698840138000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" ADD "allowRenoteToExternal" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "allowRenoteToExternal"`);
}
}

View File

@ -72,9 +72,9 @@
"@fastify/multipart": "8.0.0",
"@fastify/static": "6.12.0",
"@fastify/view": "8.2.0",
"@nestjs/common": "10.2.7",
"@nestjs/core": "10.2.7",
"@nestjs/testing": "10.2.7",
"@nestjs/common": "10.2.8",
"@nestjs/core": "10.2.8",
"@nestjs/testing": "10.2.8",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.3.5",
"@sinonjs/fake-timers": "11.2.2",
@ -87,7 +87,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.12.7",
"bullmq": "4.12.8",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@ -100,7 +100,7 @@
"deep-email-validator": "0.1.21",
"fastify": "4.24.3",
"feed": "4.2.2",
"file-type": "18.5.0",
"file-type": "18.6.0",
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "13.0.0",

View File

@ -86,6 +86,7 @@ export const ACHIEVEMENT_TYPES = [
'cookieClicked',
'brainDiver',
'smashTestNotificationButton',
'tutorialCompleted',
] as const;
@Injectable()

View File

@ -64,6 +64,7 @@ import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js';
import { FunoutTimelineService } from './FunoutTimelineService.js';
import { ChannelFollowingService } from './ChannelFollowingService.js';
import { RegistryApiService } from './RegistryApiService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.js';
@ -195,6 +196,7 @@ const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipServic
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
@ -330,6 +332,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FeaturedService,
FunoutTimelineService,
ChannelFollowingService,
RegistryApiService,
ChartLoggerService,
FederationChart,
NotesChart,
@ -458,6 +461,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FeaturedService,
$FunoutTimelineService,
$ChannelFollowingService,
$RegistryApiService,
$ChartLoggerService,
$FederationChart,
$NotesChart,
@ -587,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FeaturedService,
FunoutTimelineService,
ChannelFollowingService,
RegistryApiService,
FederationChart,
NotesChart,
UsersChart,
@ -714,6 +719,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FeaturedService,
$FunoutTimelineService,
$ChannelFollowingService,
$RegistryApiService,
$FederationChart,
$NotesChart,
$UsersChart,

View File

@ -0,0 +1,147 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { MiRegistryItem, RegistryItemsRepository } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { MiUser } from '@/models/User.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class RegistryApiService {
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
}
@bindThis
public async set(userId: MiUser['id'], domain: string | null, scope: string[], key: string, value: any) {
// TODO: 作成できるキーの数を制限する
const query = this.registryItemsRepository.createQueryBuilder('item');
if (domain) {
query.where('item.domain = :domain', { domain: domain });
} else {
query.where('item.domain IS NULL');
}
query.andWhere('item.userId = :userId', { userId: userId });
query.andWhere('item.key = :key', { key: key });
query.andWhere('item.scope = :scope', { scope: scope });
const existingItem = await query.getOne();
if (existingItem) {
await this.registryItemsRepository.update(existingItem.id, {
updatedAt: new Date(),
value: value,
});
} else {
await this.registryItemsRepository.insert({
id: this.idService.gen(),
updatedAt: new Date(),
userId: userId,
domain: domain,
scope: scope,
key: key,
value: value,
});
}
if (domain == null) {
// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
this.globalEventService.publishMainStream(userId, 'registryUpdated', {
scope: scope,
key: key,
value: value,
});
}
}
@bindThis
public async getItem(userId: MiUser['id'], domain: string | null, scope: string[], key: string): Promise<MiRegistryItem | null> {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain })
.andWhere('item.userId = :userId', { userId: userId })
.andWhere('item.key = :key', { key: key })
.andWhere('item.scope = :scope', { scope: scope });
const item = await query.getOne();
return item;
}
@bindThis
public async getAllItemsOfScope(userId: MiUser['id'], domain: string | null, scope: string[]): Promise<MiRegistryItem[]> {
const query = this.registryItemsRepository.createQueryBuilder('item');
query.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain });
query.andWhere('item.userId = :userId', { userId: userId });
query.andWhere('item.scope = :scope', { scope: scope });
const items = await query.getMany();
return items;
}
@bindThis
public async getAllKeysOfScope(userId: MiUser['id'], domain: string | null, scope: string[]): Promise<string[]> {
const query = this.registryItemsRepository.createQueryBuilder('item');
query.select('item.key');
query.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain });
query.andWhere('item.userId = :userId', { userId: userId });
query.andWhere('item.scope = :scope', { scope: scope });
const items = await query.getMany();
return items.map(x => x.key);
}
@bindThis
public async getAllScopeAndDomains(userId: MiUser['id']): Promise<{ domain: string | null; scopes: string[][] }[]> {
const query = this.registryItemsRepository.createQueryBuilder('item')
.select(['item.scope', 'item.domain'])
.where('item.userId = :userId', { userId: userId });
const items = await query.getMany();
const res = [] as { domain: string | null; scopes: string[][] }[];
for (const item of items) {
const target = res.find(x => x.domain === item.domain);
if (target) {
if (target.scopes.some(scope => scope.join('.') === item.scope.join('.'))) continue;
target.scopes.push(item.scope);
} else {
res.push({
domain: item.domain,
scopes: [item.scope],
});
}
}
return res;
}
@bindThis
public async remove(userId: MiUser['id'], domain: string | null, scope: string[], key: string) {
const query = this.registryItemsRepository.createQueryBuilder().delete();
if (domain) {
query.where('domain = :domain', { domain: domain });
} else {
query.where('domain IS NULL');
}
query.andWhere('userId = :userId', { userId: userId });
query.andWhere('key = :key', { key: key });
query.andWhere('scope = :scope', { scope: scope });
await query.execute();
}
}

View File

@ -85,6 +85,7 @@ export class ChannelEntityService {
usersCount: channel.usersCount,
notesCount: channel.notesCount,
isSensitive: channel.isSensitive,
allowRenoteToExternal: channel.allowRenoteToExternal,
...(me ? {
isFollowing,

View File

@ -351,6 +351,7 @@ export class NoteEntityService implements OnModuleInit {
name: channel.name,
color: channel.color,
isSensitive: channel.isSensitive,
allowRenoteToExternal: channel.allowRenoteToExternal,
} : undefined,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri ?? undefined,

View File

@ -93,4 +93,9 @@ export class MiChannel {
default: false,
})
public isSensitive: boolean;
@Column('boolean', {
default: true,
})
public allowRenoteToExternal: boolean;
}

View File

@ -76,5 +76,9 @@ export const packedChannelSchema = {
type: 'boolean',
optional: false, nullable: false,
},
allowRenoteToExternal: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View File

@ -233,7 +233,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js';
import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js';
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
@ -597,7 +597,7 @@ const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep__
const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default };
const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default };
const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default };
const $i_registry_scopes: Provider = { provide: 'ep:i/registry/scopes', useClass: ep___i_registry_scopes.default };
const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default };
const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default };
const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default };
const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default };
@ -964,7 +964,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_registry_keysWithType,
$i_registry_keys,
$i_registry_remove,
$i_registry_scopes,
$i_registry_scopesWithDomain,
$i_registry_set,
$i_revokeToken,
$i_signinHistory,
@ -1325,7 +1325,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_registry_keysWithType,
$i_registry_keys,
$i_registry_remove,
$i_registry_scopes,
$i_registry_scopesWithDomain,
$i_registry_set,
$i_revokeToken,
$i_signinHistory,

View File

@ -136,7 +136,20 @@ export class SignupApiService {
return;
}
if (ticket.usedAt) {
// メアド認証が有効の場合
if (instance.emailRequiredForSignup) {
// メアド認証済みならエラー
if (ticket.usedBy) {
reply.code(400);
return;
}
// 認証しておらず、メール送信から30分以内ならエラー
if (ticket.usedAt && ticket.usedAt.getTime() + (1000 * 60 * 30) > Date.now()) {
reply.code(400);
return;
}
} else if (ticket.usedAt) {
reply.code(400);
return;
}
@ -224,6 +237,10 @@ export class SignupApiService {
try {
const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code });
if (this.idService.parse(pendingUser.id).date.getTime() + (1000 * 60 * 30) < Date.now()) {
throw new FastifyReplyError(400, 'EXPIRED');
}
const { account, secret } = await this.signupService.signup({
username: pendingUser.username,
passwordHash: pendingUser.password,

View File

@ -232,7 +232,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js';
import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js';
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
@ -593,7 +593,7 @@ const eps = [
['i/registry/keys-with-type', ep___i_registry_keysWithType],
['i/registry/keys', ep___i_registry_keys],
['i/registry/remove', ep___i_registry_remove],
['i/registry/scopes', ep___i_registry_scopes],
['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain],
['i/registry/set', ep___i_registry_set],
['i/revoke-token', ep___i_revokeToken],
['i/signin-history', ep___i_signinHistory],

View File

@ -50,6 +50,7 @@ export const paramDef = {
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
color: { type: 'string', minLength: 1, maxLength: 16 },
isSensitive: { type: 'boolean', nullable: true },
allowRenoteToExternal: { type: 'boolean', nullable: true },
},
required: ['name'],
} as const;
@ -87,6 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
bannerId: banner ? banner.id : null,
isSensitive: ps.isSensitive ?? false,
...(ps.color !== undefined ? { color: ps.color } : {}),
allowRenoteToExternal: ps.allowRenoteToExternal ?? true,
} as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
return await this.channelEntityService.pack(channel, me);

View File

@ -61,6 +61,7 @@ export const paramDef = {
},
color: { type: 'string', minLength: 1, maxLength: 16 },
isSensitive: { type: 'boolean', nullable: true },
allowRenoteToExternal: { type: 'boolean', nullable: true },
},
required: ['channelId'],
} as const;
@ -115,6 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
...(banner ? { bannerId: banner.id } : {}),
...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}),
...(typeof ps.allowRenoteToExternal === 'boolean' ? { allowRenoteToExternal: ps.allowRenoteToExternal } : {}),
});
return await this.channelEntityService.pack(channel.id, me);

View File

@ -5,13 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -20,23 +17,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: [],
required: ['scope'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.scope = :scope', { scope: ps.scope });
const items = await query.getMany();
super(meta, paramDef, async (ps, me, accessToken) => {
const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope);
const res = {} as Record<string, any>;

View File

@ -5,15 +5,12 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: 'No such key.',
@ -30,24 +27,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: ['key'],
required: ['key', 'scope'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.key = :key', { key: ps.key })
.andWhere('item.scope = :scope', { scope: ps.scope });
const item = await query.getOne();
super(meta, paramDef, async (ps, me, accessToken) => {
const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key);
if (item == null) {
throw new ApiError(meta.errors.noSuchKey);

View File

@ -5,15 +5,12 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: 'No such key.',
@ -30,24 +27,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: ['key'],
required: ['key', 'scope'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.key = :key', { key: ps.key })
.andWhere('item.scope = :scope', { scope: ps.scope });
const item = await query.getOne();
super(meta, paramDef, async (ps, me, accessToken) => {
const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key);
if (item == null) {
throw new ApiError(meta.errors.noSuchKey);

View File

@ -5,13 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -20,36 +17,31 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: [],
required: ['scope'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.scope = :scope', { scope: ps.scope });
const items = await query.getMany();
super(meta, paramDef, async (ps, me, accessToken) => {
const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope);
const res = {} as Record<string, string>;
for (const item of items) {
const type = typeof item.value;
res[item.key] =
item.value === null ? 'null' :
Array.isArray(item.value) ? 'array' :
type === 'number' ? 'number' :
type === 'string' ? 'string' :
type === 'boolean' ? 'boolean' :
type === 'object' ? 'object' :
null as never;
item.value === null ? 'null' :
Array.isArray(item.value) ? 'array' :
type === 'number' ? 'number' :
type === 'string' ? 'string' :
type === 'boolean' ? 'boolean' :
type === 'object' ? 'object' :
null as never;
}
return res;

View File

@ -5,13 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -20,26 +17,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: [],
required: ['scope'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.select('item.key')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.scope = :scope', { scope: ps.scope });
const items = await query.getMany();
return items.map(x => x.key);
super(meta, paramDef, async (ps, me, accessToken) => {
return await this.registryApiService.getAllKeysOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope);
});
}
}

View File

@ -7,13 +7,12 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: 'No such key.',
@ -30,30 +29,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: ['key'],
required: ['key', 'scope'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.key = :key', { key: ps.key })
.andWhere('item.scope = :scope', { scope: ps.scope });
const item = await query.getOne();
if (item == null) {
throw new ApiError(meta.errors.noSuchKey);
}
await this.registryItemsRepository.remove(item);
super(meta, paramDef, async (ps, me, accessToken) => {
await this.registryApiService.remove(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key);
});
}
}

View File

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me) => {
return await this.registryApiService.getAllScopeAndDomains(me.id);
});
}
}

View File

@ -1,47 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.select('item.scope')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id });
const items = await query.getMany();
const res = [] as string[][];
for (const item of items) {
if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue;
res.push(item.scope);
}
return res;
});
}
}

View File

@ -5,15 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -24,51 +19,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: ['key', 'value'],
required: ['key', 'value', 'scope'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.key = :key', { key: ps.key })
.andWhere('item.scope = :scope', { scope: ps.scope });
const existingItem = await query.getOne();
if (existingItem) {
await this.registryItemsRepository.update(existingItem.id, {
updatedAt: new Date(),
value: ps.value,
});
} else {
await this.registryItemsRepository.insert({
id: this.idService.gen(),
updatedAt: new Date(),
userId: me.id,
domain: null,
scope: ps.scope,
key: ps.key,
value: ps.value,
});
}
// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
this.globalEventService.publishMainStream(me.id, 'registryUpdated', {
scope: ps.scope,
key: ps.key,
value: ps.value,
});
super(meta, paramDef, async (ps, me, accessToken) => {
await this.registryApiService.set(me.id, accessToken ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key, ps.value);
});
}
}

View File

@ -64,7 +64,7 @@ describe('api:notes/create', () => {
test('0 characters cw', () => {
expect(v({ text: 'Body', cw: '' }))
.toBe(VALID);
.toBe(INVALID);
});
test('reject only cw', () => {

View File

@ -19,6 +19,7 @@ import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import {noteVisibilities} from "@/types.js";
import { isPureRenote } from '@/misc/is-pure-renote.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
@ -100,6 +101,12 @@ export const meta = {
code: 'NO_SUCH_FILE',
id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
},
cannotRenoteOutsideOfChannel: {
message: 'Cannot renote outside of channel.',
code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
id: '33510210-8452-094c-6227-4a6c05d99f00',
},
},
} as const;
@ -110,7 +117,7 @@ export const paramDef = {
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
cw: { type: 'string', nullable: true, maxLength: 100 },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
noExtractMentions: { type: 'boolean', default: false },
@ -247,6 +254,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// specified / direct noteはreject
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
}
if (renote.channelId && renote.channelId !== ps.channelId) {
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
// リートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
const renoteChannel = await this.channelsRepository.findOneById(renote.channelId);
if (renoteChannel == null) {
// リノートしたいノートが書き込まれているチャンネルが無い
throw new ApiError(meta.errors.noSuchChannel);
} else if (!renoteChannel.allowRenoteToExternal) {
// リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
}
}
}
let visibility = ps.visibility;
let reply: MiNote | null = null;

View File

@ -253,8 +253,9 @@ export class ClientServerService {
decorateReply: false,
});
} else {
const port = (process.env.VITE_PORT ?? '5173');
fastify.register(fastifyProxy, {
upstream: 'http://localhost:5173', // TODO: port configuration
upstream: 'http://localhost:' + port,
prefix: '/vite',
rewritePrefix: '/vite',
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -71,7 +71,7 @@
"twemoji-parser": "14.0.0",
"typescript": "5.2.2",
"uuid": "9.0.1",
"v-code-diff": "1.7.1",
"v-code-diff": "1.7.2",
"vanilla-tilt": "1.8.1",
"vite": "4.5.0",
"vue": "3.3.7",

View File

@ -48,6 +48,7 @@ import { $i } from '@/account.js';
import { useRouter } from '@/router.js';
import { getDriveFileMenu, getDriveMultiFileMenu } from '@/scripts/get-drive-file-menu.js';
import { isTouchUsing } from '@/scripts/touch.js';
import { deviceKind } from '@/scripts/device-kind.js';
const router = useRouter();
@ -88,7 +89,11 @@ function onClick(ev: MouseEvent) {
} else if (isTouchUsing && !isSelectedFile.value && props.SelectFiles.length === 0) {
os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}else {
router.push(`/my/drive/file/${props.file.id}`);
if (deviceKind === 'desktop') {
router.push(`/my/drive/file/${props.file.id}`);
} else {
os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
}
}

View File

@ -7,7 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.root, { [$style.warn]: warn }]">
<i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i>
<i v-else class="ti ti-info-circle" :class="$style.i"></i>
<slot></slot>
<div><slot></slot></div>
<button v-if="closable" :class="$style.button" class="_button" @click="close()"><i class="ti ti-x"></i></button>
</div>
</template>
@ -16,11 +17,23 @@ import { } from 'vue';
const props = defineProps<{
warn?: boolean;
closable?: boolean;
}>();
const emit = defineEmits<{
(ev: 'close'): void;
}>();
function close() {
//
emit('close');
}
</script>
<style lang="scss" module>
.root {
display: flex;
align-items: center;
padding: 12px 14px;
font-size: 90%;
background: var(--infoBg);
@ -37,4 +50,9 @@ const props = defineProps<{
.i {
margin-right: 4px;
}
.button {
margin-left: auto;
padding: 4px;
}
</style>

View File

@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<div :class="$style.main">
<MkNoteHeader :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
@ -92,7 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
</div>
<MkReactionsViewer :note="appearNote" :maxNumber="16">
<MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
<template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
@ -146,7 +146,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import {computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent} from 'vue';
import {computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue';
@ -169,7 +169,7 @@ import {reactionPicker} from '@/scripts/reaction-picker.js';
import {extractUrlFromMfm} from '@/scripts/extract-url-from-mfm.js';
import {$i} from '@/account.js';
import {i18n} from '@/i18n.js';
import {getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu} from '@/scripts/get-note-menu.js';
import {getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu} from '@/scripts/get-note-menu.js';
import {useNoteCapture} from '@/scripts/use-note-capture.js';
import {deepClone} from '@/scripts/clone.js';
import {useTooltip} from '@/scripts/use-tooltip.js';
@ -180,9 +180,19 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
import {showMovedDialog} from '@/scripts/show-moved-dialog.js';
import {shouldCollapsed} from '@/scripts/collapsed.js';
const props = defineProps<{
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
pinned?: boolean;
mock?: boolean;
}>(), {
mock: false,
});
provide('mock', props.mock);
const emit = defineEmits<{
(ev: 'reaction', emoji: string): void;
(ev: 'removeReaction', emoji: string): void;
}>();
const inChannel = inject('inChannel', null);
@ -242,126 +252,54 @@ const keymap = {
's': () => showContent.value !== showContent.value,
};
useNoteCapture({
rootEl: el,
note: $$(appearNote),
pureNote: $$(note),
isDeletedRef: isDeleted,
});
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
noteId: appearNote.id,
limit: 11,
if (props.mock) {
watch(() => props.note, (to) => {
note = deepClone(to);
}, { deep: true });
} else {
useNoteCapture({
rootEl: el,
note: $$(appearNote),
pureNote: $$(note),
isDeletedRef: isDeleted,
});
}
const users = renotes.map(x => x.user);
if (!props.mock) {
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
noteId: appearNote.id,
limit: 11,
});
if (users.length < 1) return;
const users = renotes.map(x => x.user);
os.popup(MkUsersTooltip, {
showing,
users,
count: appearNote.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
if (users.length < 1) return;
type Visibility = 'public' | 'home' | 'followers' | 'specified';
// defaultStore.state.visibilitystringstring
function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
if (a === 'specified' || b === 'specified') return 'specified';
if (a === 'followers' || b === 'followers') return 'followers';
if (a === 'home' || b === 'home') return 'home';
// if (a === 'public' || b === 'public')
return 'public';
os.popup(MkUsersTooltip, {
showing,
users,
count: appearNote.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
}
function renote(viaKeyboard = false) {
pleaseLogin();
showMovedDialog();
let items = [] as MenuItem[];
if (appearNote.channel) {
items = items.concat([{
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, {x, y}, {}, 'end');
}
os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
});
},
}, {
text: i18n.ts.inChannelQuote,
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
channel: appearNote.channel,
});
},
}, null]);
}
items = items.concat([{
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, {x, y}, {}, 'end');
}
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
let visibility = appearNote.visibility;
visibility = smallerVisibility(visibility, configuredVisibility);
if (appearNote.channel?.isSensitive) {
visibility = smallerVisibility(visibility, 'home');
}
os.api('notes/create', {
localOnly,
visibility,
renoteId: appearNote.id,
}).then(() => {
os.toast(i18n.ts.renoted);
});
},
}, {
text: i18n.ts.quote,
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
});
},
}]);
os.popupMenu(items, renoteButton.value, {
const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock });
os.popupMenu(menu, renoteButton.value, {
viaKeyboard,
});
}).then(focus);
}
function reply(viaKeyboard = false): void {
pleaseLogin();
if (props.mock) {
return;
}
os.post({
reply: appearNote,
channel: appearNote.channel,
@ -375,6 +313,10 @@ function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') {
if (props.mock) {
return;
}
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: '❤️',
@ -389,6 +331,11 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
if (props.mock) {
emit('reaction', reaction);
return;
}
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction,
@ -405,12 +352,22 @@ function react(viaKeyboard = false): void {
function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
if (props.mock) {
emit('removeReaction', oldReaction);
return;
}
os.api('notes/reactions/delete', {
noteId: note.id,
});
}
function onContextmenu(ev: MouseEvent): void {
if (props.mock) {
return;
}
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
// Audio
@ -439,6 +396,10 @@ function onContextmenu(ev: MouseEvent): void {
}
function menu(viaKeyboard = false): void {
if (props.mock) {
return;
}
const {menu, cleanup} = getNoteMenu({
note: note,
translating,
@ -453,6 +414,10 @@ function menu(viaKeyboard = false): void {
}
async function clip() {
if (props.mock) {
return;
}
os.popupMenu(await getNoteClipMenu({
note: note,
isDeleted,
@ -461,6 +426,10 @@ async function clip() {
}
function showRenoteMenu(viaKeyboard = false): void {
if (props.mock) {
return;
}
function getUnrenote(): MenuItem {
return {
text: i18n.ts.unrenote,
@ -518,6 +487,14 @@ function readPromo() {
});
isDeleted.value = true;
}
function emitUpdReaction(emoji: string, delta: number) {
if (delta < 0) {
emit('removeReaction', emoji);
} else if (delta > 0) {
emit('reaction', emoji);
}
}
</script>
<style lang="scss" module>

View File

@ -200,7 +200,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
@ -352,71 +352,10 @@ function renote(viaKeyboard = false) {
pleaseLogin();
showMovedDialog();
let items = [] as MenuItem[];
if (appearNote.channel) {
items = items.concat([{
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
});
},
}, {
text: i18n.ts.inChannelQuote,
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
channel: appearNote.channel,
});
},
}, null]);
}
items = items.concat([{
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
os.api('notes/create', {
renoteId: appearNote.id,
}).then(() => {
os.toast(i18n.ts.renoted);
});
},
}, {
text: i18n.ts.quote,
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
});
},
}]);
os.popupMenu(items, renoteButton.value, {
const { menu } = getRenoteMenu({ note: note, renoteButton });
os.popupMenu(menu, renoteButton.value, {
viaKeyboard,
});
}).then(focus);
}
function reply(viaKeyboard = false): void {

View File

@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<header :class="$style.root">
<MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
<div v-if="mock" :class="$style.name">
<MkUserName :user="note.user"/>
</div>
<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
<MkUserName :user="note.user"/>
</MkA>
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
@ -16,6 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.info">
<span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span>
<MkA :to="notePage(note)">
<div v-if="mock">
<MkTime :time="note.createdAt" colored/>
</div>
<MkA v-else :to="notePage(note)">
<MkTime :time="note.createdAt" colored/>
</MkA>
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
@ -30,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import { inject } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import { notePage } from '@/filters/note.js';
@ -39,6 +46,8 @@ import { userPage } from '@/filters/user.js';
defineProps<{
note: Misskey.entities.Note;
}>();
const mock = inject<boolean>('mock', false);
</script>
<style lang="scss" module>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div ref="elRef" :class="$style.root">
<div :class="$style.root">
<div :class="$style.head">
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon
v-else-if="notification.type === 'reaction'"
ref="reactionRef"
:withTooltip="true"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
:noStyle="true"
style="width: 100%; height: 100%;"
@ -111,6 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/>
<div :class="$style.reactionsItemReaction">
<MkReactionIcon
:withTooltip="true"
:reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction"
:noStyle="true"
style="width: 100%; height: 100%;"
@ -133,14 +134,12 @@ import { ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
import MkButton from '@/components/MkButton.vue';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
import { notePage } from '@/filters/note.js';
import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { $i } from '@/account.js';
import { infoImageUrl } from '@/instance.js';
@ -153,9 +152,6 @@ const props = withDefaults(defineProps<{
full: false,
});
const elRef = shallowRef<HTMLElement>(null);
const reactionRef = ref(null);
const followRequestDone = ref(false);
const acceptFollowRequest = () => {
@ -167,15 +163,6 @@ const rejectFollowRequest = () => {
followRequestDone.value = true;
os.api('following/requests/reject', { userId: props.notification.user.id });
};
useTooltip(reactionRef, (showing) => {
os.popup(XReactionTooltip, {
showing,
reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
emojis: props.notification.note.emojis,
targetElement: reactionRef.value.$el,
}, {}, 'closed');
});
</script>
<style lang="scss" module>

View File

@ -4,25 +4,27 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPagination ref="pagingComponent" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noNotifications }}</div>
</div>
</template>
<MkPullToRefresh :refresher="() => reload()">
<MkPagination ref="pagingComponent" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.noNotifications }}</div>
</div>
</template>
<template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
</template>
</MkPagination>
<template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
</template>
</MkPagination>
</MkPullToRefresh>
</template>
<script lang="ts" setup>
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef } from 'vue';
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
@ -33,6 +35,7 @@ import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js';
import { defaultStore } from '@/store.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][];
@ -54,7 +57,7 @@ const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
})),
};
const onNotification = (notification) => {
function onNotification(notification) {
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
if (isMuted || document.visibilityState === 'visible') {
useStream().send('readNotification');
@ -63,7 +66,15 @@ const onNotification = (notification) => {
if (!isMuted) {
pagingComponent.value.prepend(notification);
}
};
}
function reload() {
return new Promise<void>((res) => {
pagingComponent.value?.reload().then(() => {
res();
});
});
}
let connection;
@ -72,6 +83,12 @@ onMounted(() => {
connection.on('notification', onNotification);
});
onActivated(() => {
pagingComponent.value?.reload();
connection = useStream().useChannel('main');
connection.on('notification', onNotification);
});
onUnmounted(() => {
if (connection) connection.dispose();
});

View File

@ -100,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import {inject, watch, nextTick, onMounted, defineAsyncComponent, computed, ref} from 'vue';
import { inject, watch, nextTick, onMounted, defineAsyncComponent , computed, ref , provide } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
@ -139,31 +139,38 @@ let gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
const props = withDefaults(defineProps<{
reply?: Misskey.entities.Note;
renote?: Misskey.entities.Note;
channel?: Misskey.entities.Channel; // TODO
mention?: Misskey.entities.User;
specified?: Misskey.entities.User;
initialText?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.User[];
initialNote?: Misskey.entities.Note;
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
updateMode?: boolean;
reply?: Misskey.entities.Note;
renote?: Misskey.entities.Note;
channel?: Misskey.entities.Channel; // TODO
mention?: Misskey.entities.User;
specified?: Misskey.entities.User;
initialText?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.User[];
initialNote?: Misskey.entities.Note;
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
mock?: boolean;
updateMode?: boolean;
}>(), {
initialVisibleUsers: () => [],
autofocus: true,
mock: false,
});
provide('mock', props.mock);
const emit = defineEmits<{
(ev: 'posted'): void;
(ev: 'cancel'): void;
(ev: 'esc'): void;
// Mock
(ev: 'fileChangeSensitive', fileId: string, to: boolean): void;
}>();
const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null);
@ -251,7 +258,7 @@ const maxTextLength = $computed((): number => {
});
const canPost = $computed((): boolean => {
return !posting && !posted &&
return !props.mock && !posting && !posted &&
(1 <= textLength || 1 <= files.length || !!poll || !!props.renote) &&
(textLength <= maxTextLength) &&
(!poll || poll.choices.length >= 2);
@ -304,6 +311,10 @@ if (props.reply && props.reply.text != null) {
}
}
if ($i?.isSilenced && visibility === 'public') {
visibility = 'home';
}
if (props.channel) {
visibility = 'public';
localOnly = true; // TODO:
@ -408,7 +419,7 @@ function focus() {
}
function chooseFileFrom(ev) {
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
if (props.mock) return;selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
for (const file of files_) {
files.push(file);
}
@ -420,7 +431,10 @@ function detachFile(id) {
}
function updateFileSensitive(file, sensitive) {
files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
if (props.mock) {
emit('fileChangeSensitive', file.id, sensitive);
}
files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
}
function updateFileName(file, name) {
@ -432,7 +446,7 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities
}
function upload(file: File, name?: string): void {
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
if (props.mock) return;uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
files.push(res);
});
}
@ -446,7 +460,7 @@ function setVisibility() {
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
currentVisibility: visibility,
localOnly: localOnly,
isSilenced: $i?.isSilenced,localOnly: localOnly,
src: visibilityButton,
}, {
changeVisibility: v => {
@ -557,7 +571,7 @@ function onCompositionEnd(ev: CompositionEvent) {
}
async function onPaste(ev: ClipboardEvent) {
for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) {
if (props.mock) return;for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) {
if (item.kind === 'file') {
const file = item.getAsFile();
const lio = file.name.lastIndexOf('.');
@ -641,7 +655,7 @@ function onDrop(ev): void {
}
function saveDraft() {
if (props.instant) return;
if (props.instant || props.mock) return;
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
@ -670,7 +684,13 @@ function deleteDraft() {
}
async function post(ev?: MouseEvent) {
if (ev) {
if (useCw && (cw == null || cw.trim() === '')) {
os.alert({
type: 'error',
text: i18n.ts.cwNotationRequired,
});
return;
}if (ev) {
const el = ev.currentTarget ?? ev.target;
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
@ -678,7 +698,7 @@ async function post(ev?: MouseEvent) {
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
const annoying =
if (props.mock) return;const annoying =
text.includes('$[x2') ||
text.includes('$[x3') ||
text.includes('$[x4') ||
@ -850,7 +870,7 @@ function showActions(ev) {
let postAccount = $ref<Misskey.entities.UserDetailed | null>(null);
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
if (props.mock) return;openAccountMenu_({
withExtraOperation: false,
includeCurrentAccount: true,
active: postAccount != null ? postAccount.id : $i.id,
@ -880,7 +900,7 @@ onMounted(() => {
nextTick(() => {
// 稿
if (!props.instant && !props.mention && !props.specified) {
if (!props.instant && !props.mention && !props.specified && !props.mock) {
const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey];
if (draft) {
text = draft.data.text;

View File

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { defineAsyncComponent, inject } from 'vue';
import * as Misskey from 'misskey-js';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import * as os from '@/os.js';
@ -33,6 +33,8 @@ const props = defineProps<{
detachMediaFn?: (id: string) => void;
}>();
const mock = inject<boolean>('mock', false);
const emit = defineEmits<{
(ev: 'update:modelValue', value: any[]): void;
(ev: 'detach', id: string): void;
@ -44,6 +46,8 @@ const emit = defineEmits<{
let menuShowing = false;
function detachMedia(id: string) {
if (mock) return;
if (props.detachMediaFn) {
props.detachMediaFn(id);
} else {
@ -52,6 +56,11 @@ function detachMedia(id: string) {
}
function toggleSensitive(file) {
if (mock) {
emit('changeSensitive', file, !file.isSensitive);
return;
}
os.api('drive/files/update', {
fileId: file.id,
isSensitive: !file.isSensitive,
@ -61,6 +70,8 @@ function toggleSensitive(file) {
}
async function rename(file) {
if (mock) return;
const { canceled, result } = await os.inputText({
title: i18n.ts.enterFileName,
default: file.name,
@ -77,6 +88,8 @@ async function rename(file) {
}
async function describe(file) {
if (mock) return;
os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
default: file.comment !== null ? file.comment : '',
file: file,
@ -94,6 +107,8 @@ async function describe(file) {
}
async function crop(file: Misskey.entities.DriveFile): Promise<void> {
if (mock) return;
const newFile = await os.cropImage(file, { aspectRatio: NaN });
emit('replaceFile', file, newFile);
}

View File

@ -47,7 +47,13 @@ let scrollEl: HTMLElement | null = null;
let disabled = false;
const emits = defineEmits<{
const props = withDefaults(defineProps<{
refresher: () => Promise<void>;
}>(), {
refresher: () => Promise.resolve(),
});
const emit = defineEmits<{
(ev: 'refresh'): void;
}>();
@ -120,7 +126,12 @@ function moveEnd() {
if (isPullEnd) {
isPullEnd = false;
isRefreshing = true;
fixOverContent().then(() => emits('refresh'));
fixOverContent().then(() => {
emit('refresh');
props.refresher().then(() => {
refreshFinished();
});
});
} else {
closeContent().then(() => isPullStart = false);
}
@ -188,7 +199,6 @@ onUnmounted(() => {
});
defineExpose({
refreshFinished,
setDisabled,
});
</script>

View File

@ -4,16 +4,31 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/>
<MkEmoji v-else :emoji="reaction" :normal="true" :noStyle="noStyle"/>
<MkCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/>
<MkEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/>
</template>
<script lang="ts" setup>
import { } from 'vue';
import { defineAsyncComponent, shallowRef } from 'vue';
import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
const props = defineProps<{
reaction: string;
noStyle?: boolean;
emojiUrl?: string;
withTooltip?: boolean;
}>();
const elRef = shallowRef();
if (props.withTooltip) {
useTooltip(elRef, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
showing,
reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'),
targetElement: elRef.value.$el,
}, {}, 'closed');
});
}
</script>

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar :class="$style.avatar" :user="u"/>
<MkUserName :user="u" :nowrap="true"/>
</div>
<div v-if="users.length > 10" :class="$style.more">+{{ count - 10 }}</div>
<div v-if="count > 10" :class="$style.more">+{{ count - 10 }}</div>
</div>
</div>
</MkTooltip>

View File

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import {computed, onMounted, ref, shallowRef, watch} from 'vue';
import {computed, inject, onMounted, ref, shallowRef, watch} from 'vue';
import * as Misskey from 'misskey-js';
import XDetails from '@/components/MkReactionsViewer.details.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
@ -38,6 +38,12 @@ const props = defineProps<{
note: Misskey.entities.Note;
}>();
const mock = inject<boolean>('mock', false);
const emit = defineEmits<{
(ev: 'reactionToggled', emoji: string, newCount: number): void;
}>();
const buttonEl = shallowRef<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
@ -55,6 +61,11 @@ async function toggleReaction() {
});
if (confirm.canceled) return;
if (mock) {
emit('reactionToggled', props.reaction, (props.count - 1));
return;
}
os.api('notes/reactions/delete', {
noteId: props.note.id,
}).then(() => {
@ -66,6 +77,11 @@ async function toggleReaction() {
}
});
} else {
if (mock) {
emit('reactionToggled', props.reaction, (props.count + 1));
return;
}
os.api('notes/reactions/create', {
noteId: props.note.id,
reaction: props.reaction,
@ -94,24 +110,26 @@ onMounted(() => {
if (!props.isInitial) anime();
});
useTooltip(buttonEl, async (showing) => {
const reactions = await os.apiGet('notes/reactions', {
noteId: props.note.id,
type: props.reaction,
limit: 11,
_cacheKey_: props.count,
});
if (!mock) {
useTooltip(buttonEl, async (showing) => {
const reactions = await os.apiGet('notes/reactions', {
noteId: props.note.id,
type: props.reaction,
limit: 10,
_cacheKey_: props.count,
});
const users = reactions.map(x => x.user);
const users = reactions.map(x => x.user);
os.popup(XDetails, {
showing,
reaction: props.reaction,
users,
count: props.count,
targetElement: buttonEl.value,
}, {}, 'closed');
}, 100);
os.popup(XDetails, {
showing,
reaction: props.reaction,
users,
count: props.count,
targetElement: buttonEl.value,
}, {}, 'closed');
}, 100);
}
</script>
<style lang="scss" module>

View File

@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass="defaultStore.state.animation ? $style.transition_x_move : ''"
tag="div" :class="$style.root"
>
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note"/>
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
<slot v-if="hasMoreReactions" name="more"/>
</TransitionGroup>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { watch } from 'vue';
import { inject, watch } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { defaultStore } from '@/store.js';
@ -30,6 +30,12 @@ const props = withDefaults(defineProps<{
maxNumber: Infinity,
});
const mock = inject<boolean>('mock', false);
const emit = defineEmits<{
(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
}>();
const initialReactions = new Set(Object.keys(props.note.reactions));
let reactions = $ref<[string, number][]>([]);
@ -39,6 +45,15 @@ if (props.note.myReaction && !Object.keys(reactions).includes(props.note.myReact
reactions[props.note.myReaction] = props.note.reactions[props.note.myReaction];
}
function onMockToggleReaction(emoji: string, count: number) {
if (!mock) return;
const i = reactions.findIndex((item) => item[0] === emoji);
if (i < 0) return;
emit('mockUpdateMyReaction', emoji, (count - reactions[i][1]));
}
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
let newReactions: [string, number][] = [];
hasMoreReactions = Object.keys(newSource).length > maxNumber;

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPullToRefresh ref="prComponent" @refresh="() => reloadTimeline(true)">
<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()">
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/>
</MkPullToRefresh>
</template>
@ -209,25 +209,18 @@ const pagination = {
params: query,
};
const reloadTimeline = (fromPR = false) => {
tlNotesCount = 0;
function reloadTimeline() {
return new Promise<void>((res) => {
tlNotesCount = 0;
tlComponent.pagingComponent?.reload().then(() => {
reloadStream();
if (fromPR) prComponent.refreshFinished();
tlComponent.pagingComponent?.reload().then(() => {
reloadStream();
res();
});
});
};
//const pullRefresh = () => reloadTimeline(true);
}
defineExpose({
reloadTimeline,
});
/* TODO
const timetravel = (date?: Date) => {
this.date = date;
this.$refs.tl.reload();
};
*/
</script>

View File

@ -0,0 +1,117 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-if="phase === 'aboutNote'" class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.description }}</div>
<MkNote :class="$style.exampleNoteRoot" style="pointer-events: none;" :note="exampleNote" :mock="true"/>
<div class="_gaps_s">
<div><i class="ti ti-arrow-back-up"></i> <b>{{ i18n.ts.reply }}</b> {{ i18n.ts._initialTutorial._note.reply }}</div>
<div><i class="ti ti-repeat"></i> <b>{{ i18n.ts.renote }}</b> {{ i18n.ts._initialTutorial._note.renote }}</div>
<div><i class="ti ti-plus"></i> <b>{{ i18n.ts.reaction }}</b> {{ i18n.ts._initialTutorial._note.reaction }}</div>
<div><i class="ti ti-dots"></i> <b>{{ i18n.ts.menu }}</b> {{ i18n.ts._initialTutorial._note.menu }}</div>
</div>
</div>
<div v-else-if="phase === 'howToReact'" class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction" @updateReaction="updateReaction"/>
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
</div>
</template>
<script setup lang="ts">
import * as Misskey from 'misskey-js';
import { ref, reactive } from 'vue';
import { i18n } from '@/i18n.js';
import { globalEvents } from '@/events.js';
import { $i } from '@/account.js';
import MkNote from '@/components/MkNote.vue';
const props = defineProps<{
phase: 'aboutNote' | 'howToReact';
}>();
const emit = defineEmits<{
(ev: 'reacted'): void;
}>();
const exampleNote = reactive<Misskey.entities.Note>({
id: '0000000000',
createdAt: '2019-04-14T17:30:49.181Z',
userId: '0000000001',
user: {
id: '0000000001',
name: '藍',
username: 'ai',
host: null,
avatarDecorations: [],
avatarUrl: '/client-assets/tutorial/ai.webp',
avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB',
isBot: false,
isCat: true,
emojis: {},
onlineStatus: null,
badgeRoles: [],
},
text: 'just setting up my msky',
cw: null,
visibility: 'public',
localOnly: false,
reactionAcceptance: null,
renoteCount: 0,
repliesCount: 1,
reactions: {},
reactionEmojis: {},
fileIds: [],
files: [],
replyId: null,
renoteId: null,
});
const onceReacted = ref<boolean>(false);
function addReaction(emoji) {
onceReacted.value = true;
emit('reacted');
exampleNote.reactions[emoji] = 1;
exampleNote.myReaction = emoji;
doNotification(emoji);
}
function doNotification(emoji: string): void {
if (!$i || !emoji) return;
const notification: Misskey.entities.Notification = {
id: Math.random().toString(),
createdAt: new Date().toUTCString(),
isRead: false,
type: 'reaction',
reaction: emoji,
user: $i,
userId: $i.id,
note: exampleNote,
};
globalEvents.emit('clientNotification', notification);
}
function removeReaction(emoji) {
delete exampleNote.reactions[emoji];
exampleNote.myReaction = undefined;
}
</script>
<style lang="scss" module>
.exampleNoteRoot {
border-radius: var(--radius);
border: var(--panelBorder);
background: var(--panel);
}
.divider {
height: 1px;
background: var(--divider);
}
</style>

View File

@ -0,0 +1,135 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div>
<MkPostForm :class="$style.exampleRoot" :mock="true"/>
<MkFormSection>
<template #label>{{ i18n.ts.visibility }}</template>
<div class="_gaps">
<div>{{ i18n.ts._initialTutorial._postNote._visibility.description }}</div>
<div><i class="ti ti-world"></i> <b>{{ i18n.ts._visibility.public }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.public }}</div>
<div><i class="ti ti-home"></i> <b>{{ i18n.ts._visibility.home }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.home }}</div>
<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._visibility.followers }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.followers }}</div>
<div class="_gaps_s">
<div><i class="ti ti-mail"></i> <b>{{ i18n.ts._visibility.specified }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.direct }}</div>
<MkInfo :warn="true">
<b>{{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect1 }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect2 }}
</MkInfo>
</div>
<div><i class="ti ti-rocket-off"></i> <b>{{ i18n.ts._visibility.disableFederation }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}</div>
</div>
</MkFormSection>
<MkFormSection>
<template #label>{{ i18n.ts._initialTutorial._postNote._cw.title }}</template>
<div class="_gaps">
<div>{{ i18n.ts._initialTutorial._postNote._cw.description }}</div>
<MkNote :class="$style.exampleRoot" :note="exampleCWNote" :mock="true"/>
<div>{{ i18n.ts._initialTutorial._postNote._cw.useCases }}</div>
</div>
</MkFormSection>
</div>
</template>
<script setup lang="ts">
import * as Misskey from 'misskey-js';
import { reactive } from 'vue';
import { i18n } from '@/i18n.js';
import MkNote from '@/components/MkNote.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import MkFormSection from '@/components/form/section.vue';
import MkInfo from '@/components/MkInfo.vue';
const exampleCWNote = reactive<Misskey.entities.Note>({
id: '0000000000',
createdAt: '2019-04-14T17:30:49.181Z',
userId: '0000000001',
user: {
id: '0000000001',
name: '藍',
username: 'ai',
host: null,
avatarDecorations: [],
avatarUrl: '/client-assets/tutorial/ai.webp',
avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB',
isBot: false,
isCat: true,
emojis: {},
onlineStatus: null,
badgeRoles: [],
},
text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note,
cw: i18n.ts._initialTutorial._postNote._cw._exampleNote.cw,
visibility: 'public',
localOnly: false,
reactionAcceptance: null,
renoteCount: 0,
repliesCount: 1,
reactions: {},
reactionEmojis: {},
fileIds: [],
files: [],
replyId: null,
renoteId: null,
});
</script>
<style lang="scss" module>
.exampleRoot {
max-width: none!important;
border-radius: var(--radius);
border: var(--panelBorder);
background: var(--panel);
}
.divider {
height: 1px;
background: var(--divider);
}
.image {
max-width: 300px;
margin: 0 auto;
}
.post {
position: relative;
display: block;
width: 100%;
height: 40px;
color: var(--fgOnAccent);
font-weight: bold;
text-align: left;
&:before {
content: "";
display: block;
width: calc(100% - 38px);
height: 100%;
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
}
.postIcon {
position: relative;
margin-left: 30px;
margin-right: 8px;
width: 32px;
}
.postText {
position: relative;
line-height: 40px;
}
</style>

View File

@ -0,0 +1,144 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.description }}</div>
<div>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.tryThisFile }}</div>
<MkInfo>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.method }}</MkInfo>
<MkPostForm
:class="$style.exampleRoot"
:mock="true"
:initialNote="exampleNote"
@fileChangeSensitive="doSucceeded"
></MkPostForm>
<div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
<MkFolder>
<template #label>{{ i18n.ts.previewNoteText }}</template>
<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
</MkFolder>
</div>
</template>
<script setup lang="ts">
import * as Misskey from 'misskey-js';
import { ref, reactive } from 'vue';
import { i18n } from '@/i18n.js';
import MkPostForm from '@/components/MkPostForm.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkNote from '@/components/MkNote.vue';
import { $i } from '@/account.js';
const emit = defineEmits<{
(ev: 'succeeded'): void;
}>();
const onceSucceeded = ref<boolean>(false);
function doSucceeded(fileId: string, to: boolean) {
if (fileId === exampleNote.fileIds[0] && to) {
onceSucceeded.value = true;
emit('succeeded');
}
}
const exampleNote = reactive<Misskey.entities.Note>({
id: '0000000000',
createdAt: '2019-04-14T17:30:49.181Z',
userId: '0000000001',
user: $i!,
text: i18n.ts._initialTutorial._howToMakeAttachmentsSensitive._exampleNote.note,
cw: null,
visibility: 'public',
localOnly: false,
reactionAcceptance: null,
renoteCount: 0,
repliesCount: 1,
reactions: {},
reactionEmojis: {},
fileIds: ['0000000002'],
files: [{
id: '0000000002',
createdAt: '2019-04-14T17:30:49.181Z',
name: 'natto_failed.webp',
type: 'image/webp',
md5: 'c44286cf152d0740be0ce5ad45ea85c3',
size: 827532,
isSensitive: false,
blurhash: 'LXNA3TD*XAIA%1%M%gt7.TofRioz',
properties: {
width: 256,
height: 256,
},
url: '/client-assets/tutorial/natto_failed.webp',
thumbnailUrl: '/client-assets/tutorial/natto_failed.webp',
comment: null,
folderId: null,
folder: null,
userId: null,
user: null,
}],
replyId: null,
renoteId: null,
});
</script>
<style lang="scss" module>
.exampleRoot {
border-radius: var(--radius);
border: var(--panelBorder);
background: var(--panel);
}
.divider {
height: 1px;
background: var(--divider);
}
.image {
max-width: 300px;
margin: 0 auto;
}
.post {
position: relative;
display: block;
width: 100%;
height: 40px;
color: var(--fgOnAccent);
font-weight: bold;
text-align: left;
&:before {
content: "";
display: block;
width: calc(100% - 38px);
height: 100%;
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
}
.postIcon {
position: relative;
margin-left: 30px;
margin-right: 8px;
width: 32px;
}
.postText {
position: relative;
line-height: 40px;
}
</style>

View File

@ -0,0 +1,87 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div>
<div class="_gaps_s">
<div><i class="ti ti-home"></i> <b>{{ i18n.ts._timelines.home }}</b> {{ i18n.ts._initialTutorial._timeline.home }}</div>
<div><i class="ti ti-planet"></i> <b>{{ i18n.ts._timelines.local }}</b> {{ i18n.ts._initialTutorial._timeline.local }}</div>
<div><i class="ti ti-universe"></i> <b>{{ i18n.ts._timelines.social }}</b> {{ i18n.ts._initialTutorial._timeline.social }}</div>
<div><i class="ti ti-whirl"></i> <b>{{ i18n.ts._timelines.global }}</b> {{ i18n.ts._initialTutorial._timeline.global }}</div>
</div>
<div class="_gaps_s">
<div>{{ i18n.ts._initialTutorial._timeline.description2 }}</div>
<img :class="$style.image" src="/client-assets/tutorial/timeline_tab.png"/>
</div>
<div :class="$style.divider"></div>
<I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;">
<template #link>
<a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
</div>
</template>
<script setup lang="ts">
import { i18n } from '@/i18n.js';
</script>
<style lang="scss" module>
.exampleNoteRoot {
border-radius: var(--radius);
border: var(--panelBorder);
background: var(--panel);
}
.divider {
height: 1px;
background: var(--divider);
}
.image {
max-width: 300px;
margin: 0 auto;
}
.post {
position: relative;
display: block;
width: 100%;
height: 40px;
color: var(--fgOnAccent);
font-weight: bold;
text-align: left;
&:before {
content: "";
display: block;
width: calc(100% - 38px);
height: 100%;
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
}
.postIcon {
position: relative;
margin-left: 30px;
margin-right: 8px;
width: 32px;
}
.postText {
position: relative;
line-height: 40px;
}
</style>

View File

@ -0,0 +1,260 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="600"
:height="650"
@close="close(true)"
@closed="emit('closed')"
>
<template v-if="page === 1" #header><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</template>
<template v-else-if="page === 2" #header><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template>
<template v-else-if="page === 3" #header><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template>
<template v-else-if="page === 4" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template>
<template v-else-if="page === 5" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template>
<template v-else #header>{{ i18n.ts._initialTutorial.title }}</template>
<div style="overflow-x: clip;">
<Transition
mode="out-in"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="page === 0">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div>
<div>{{ i18n.ts._initialTutorial._landing.description }}</div>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
<MkButton style="margin: 0 auto;" transparent rounded @click="close(true)">{{ i18n.ts.close }}</MkButton>
</div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 1">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XNote phase="aboutNote"/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 2">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<div class="_gaps">
<XNote phase="howToReact" @reacted="isReactionTutorialPushed = true"/>
<div v-if="!isReactionTutorialPushed">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</div>
</div>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate :disabled="!isReactionTutorialPushed" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 3">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XTimeline/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 4">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XPostNote/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 5">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<div class="_gaps">
<XSensitive @succeeded="isSensitiveTutorialSucceeded = true"/>
<div v-if="!isSensitiveTutorialSucceeded">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</div>
</div>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate :disabled="!isSensitiveTutorialSucceeded" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 6">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
<template #link>
<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
</div>
</div>
</MkSpacer>
</div>
</template>
</Transition>
</div>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { ref, shallowRef, watch } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import XNote from '@/components/MkTutorialDialog.Note.vue';
import XTimeline from '@/components/MkTutorialDialog.Timeline.vue';
import XPostNote from '@/components/MkTutorialDialog.PostNote.vue';
import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
import { claimAchievement } from '@/scripts/achievements.js';
import * as os from '@/os.js';
const props = defineProps<{
initialPage?: number;
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
// eslint-disable-next-line vue/no-setup-props-destructure
const page = ref(props.initialPage ?? 0);
watch(page, (to) => {
//
if (to === 6) {
claimAchievement('tutorialCompleted');
}
});
const isReactionTutorialPushed = ref<boolean>(false);
const isSensitiveTutorialSucceeded = ref<boolean>(false);
async function close(skip: boolean) {
if (skip) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._initialTutorial.skipAreYouSure,
});
if (canceled) return;
}
dialog.value?.close();
}
</script>
<style lang="scss" module>
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
}
.transition_x_enterFrom {
opacity: 0;
transform: translateX(50px);
}
.transition_x_leaveTo {
opacity: 0;
transform: translateX(-50px);
}
.progressBar {
position: absolute;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 4px;
}
.progressBarValue {
height: 100%;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
transition: all 0.5s cubic-bezier(0,.5,.5,1);
}
.centerPage {
display: flex;
justify-content: center;
align-items: center;
height: 100cqh;
padding-bottom: 30px;
box-sizing: border-box;
}
.pageRoot {
display: flex;
flex-direction: column;
min-height: 100%;
}
.pageMain {
flex-grow: 1;
line-height: 1.5;
}
.pageFooter {
position: sticky;
bottom: 0;
left: 0;
flex-shrink: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
-webkit-backdrop-filter: blur(15px);
backdrop-filter: blur(15px);
}
</style>

View File

@ -46,24 +46,32 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template v-else-if="page === 1">
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :marginMin="20" :marginMax="28">
<XProfile/>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XProfile/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</MkSpacer>
</div>
</div>
</template>
<template v-else-if="page === 2">
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :marginMin="20" :marginMax="28">
<XPrivacy/>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XPrivacy/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</MkSpacer>
</div>
</div>
</template>
<template v-else-if="page === 3">
@ -102,16 +110,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps" style="text-align: center;">
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
<I18n :src="i18n.ts._initialAccountSetting.ifYouNeedLearnMore" tag="div" style="padding: 0 16px;">
<template #name>{{ instance.name ?? host }}</template>
<template #link>
<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
<div>{{ i18n.t('_initialAccountSetting.youCanContinueTutorial', { name: instance.name ?? host }) }}</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
<div class="_buttonsCenter">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton>
<MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton>
</div>
</div>
</MkSpacer>
@ -123,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import {computed, ref, shallowRef, watch} from 'vue';
import {computed, ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
@ -144,6 +149,7 @@ const emit = defineEmits<{
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
// eslint-disable-next-line vue/no-setup-props-destructure
const page = ref(defaultStore.state.accountSetupWizard);
watch(page, () => {
@ -159,10 +165,24 @@ async function close(skip: boolean) {
if (canceled) return;
}
dialog.value.close();
dialog.value?.close();
defaultStore.set('accountSetupWizard', -1);
}
function setupComplete() {
defaultStore.set('accountSetupWizard', -1);
dialog.value?.close();
}
function launchTutorial() {
setupComplete();
nextTick(() => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {
initialPage: 1,
}, {}, 'closed');
});
}
async function later(later: boolean) {
if (later) {
const { canceled } = await os.confirm({
@ -172,7 +192,7 @@ async function later(later: boolean) {
if (canceled) return;
}
dialog.value.close();
dialog.value?.close();
defaultStore.set('accountSetupWizard', 0);
}
</script>
@ -229,10 +249,21 @@ async function later(later: boolean) {
box-sizing: border-box;
}
.pageRoot {
display: flex;
flex-direction: column;
min-height: 100%;
}
.pageMain {
flex-grow: 1;
}
.pageFooter {
position: sticky;
bottom: 0;
left: 0;
flex-shrink: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
-webkit-backdrop-filter: blur(15px);

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.label, $style.item]">
{{ i18n.ts.visibility }}
</div>
<button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
<button key="public" :disabled="isSilenced" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
<div :class="$style.icon"><i class="ti ti-world"></i></div>
<div :class="$style.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
@ -51,6 +51,7 @@ const modal = $shallowRef<InstanceType<typeof MkModal>>();
const props = withDefaults(defineProps<{
currentVisibility: typeof Misskey.noteVisibilities[number];
isSilenced: boolean;
localOnly: boolean;
src?: HTMLElement;
}>(), {

View File

@ -17,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { toUnicode } from 'punycode/';
import MkCondensedLine from './MkCondensedLine.vue';
import { host as hostRaw } from '@/config.js';
import { defaultStore } from '@/store.js';

View File

@ -1045,7 +1045,7 @@
["⌛", "hourglass", 6],
["📡", "satellite", 6],
["🔋", "battery", 6],
["🪫", "battery", 6],
["🪫", "low_battery", 6],
["🔌", "electric_plug", 6],
["💡", "bulb", 6],
["🔦", "flashlight", 6],

View File

@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
</div>
<FormSection>
<div class="_formLinks">
<div class="_gaps_s">
<FormLink to="https://github.com/misskey-dev/misskey" external>
<template #icon><i class="ti ti-code"></i></template>
{{ i18n.ts._aboutMisskey.source }}

View File

@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue>
</FormSplit>
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
<div class="_formLinks">
<div class="_gaps_s">
<MkFolder v-if="instance.serverRules.length > 0">
<template #label>{{ i18n.ts.serverRules }}</template>
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<template #label>Well-known resources</template>
<div class="_formLinks">
<div class="_gaps_s">
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>

View File

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900">
<div class="_gaps">
<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
<MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
<MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null">
@ -43,6 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="banner">{{ i18n.ts.banner }}</option>
<option value="dialog">{{ i18n.ts.dialog }}</option>
</MkRadios>
<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
{{ i18n.ts._announcement.forExistingUsers }}
</MkSwitch>

View File

@ -24,6 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.sensitive }}</template>
</MkSwitch>
<MkSwitch v-model="allowRenoteToExternal">
<template #label>{{ i18n.ts._channel.allowRenoteToExternal }}</template>
</MkSwitch>
<div>
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
<div v-else-if="bannerUrl">
@ -76,7 +80,7 @@ import { useRouter } from '@/router.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from "@/components/MkSwitch.vue";
import MkSwitch from '@/components/MkSwitch.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@ -93,6 +97,7 @@ let bannerUrl = $ref<string | null>(null);
let bannerId = $ref<string | null>(null);
let color = $ref('#000');
let isSensitive = $ref(false);
let allowRenoteToExternal = $ref(true);
const pinnedNotes = ref([]);
watch(() => bannerId, async () => {
@ -121,6 +126,7 @@ async function fetchChannel() {
id,
}));
color = channel.color;
allowRenoteToExternal = channel.allowRenoteToExternal;
}
fetchChannel();
@ -150,6 +156,7 @@ function save() {
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
color: color,
isSensitive: isSensitive,
allowRenoteToExternal: allowRenoteToExternal,
};
if (props.channelId) {

View File

@ -56,6 +56,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>{{ i18n.ts._fileViewer.size }}</template>
<template #value>{{ bytes(file.size) }}</template>
</MkKeyValue>
<MkKeyValue :class="$style.fileMetaDataChildren" :copy="file.url">
<template #key>URL</template>
<template #value>{{ file.url }}</template>
</MkKeyValue>
</div>
</div>
<div v-else class="_fullinfo">

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts._registry.domain }}</template>
<template #value>{{ i18n.ts.system }}</template>
<template #value>{{ props.domain === '@' ? i18n.ts.system : props.domain.toUpperCase() }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts._registry.scope }}</template>
@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection v-if="keys">
<template #label>{{ i18n.ts.keys }}</template>
<div class="_formLinks">
<FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
<div class="_gaps_s">
<FormLink v-for="key in keys" :to="`/registry/value/${props.domain}/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
</div>
</FormSection>
</div>
@ -46,15 +46,17 @@ import FormSplit from '@/components/form/split.vue';
const props = defineProps<{
path: string;
domain: string;
}>();
const scope = $computed(() => props.path.split('/'));
const scope = $computed(() => props.path ? props.path.split('/') : []);
let keys = $ref(null);
function fetchKeys() {
os.api('i/registry/keys-with-type', {
scope: scope,
domain: props.domain === '@' ? null : props.domain,
}).then(res => {
keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
});

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts._registry.domain }}</template>
<template #value>{{ i18n.ts.system }}</template>
<template #value>{{ props.domain === '@' ? i18n.ts.system : props.domain.toUpperCase() }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts._registry.scope }}</template>
@ -58,6 +58,7 @@ import FormInfo from '@/components/MkInfo.vue';
const props = defineProps<{
path: string;
domain: string;
}>();
const scope = $computed(() => props.path.split('/').slice(0, -1));
@ -70,6 +71,7 @@ function fetchValue() {
os.api('i/registry/get-detail', {
scope,
key,
domain: props.domain === '@' ? null : props.domain,
}).then(res => {
value = res;
valueForEditor = JSON5.stringify(res.value, null, '\t');
@ -95,6 +97,7 @@ async function save() {
scope,
key,
value: JSON5.parse(valueForEditor),
domain: props.domain === '@' ? null : props.domain,
});
});
}
@ -108,6 +111,7 @@ function del() {
os.apiWithDialog('i/registry/remove', {
scope,
key,
domain: props.domain === '@' ? null : props.domain,
});
});
}

View File

@ -9,12 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="600" :marginMin="16">
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
<FormSection v-if="scopes">
<template #label>{{ i18n.ts.system }}</template>
<div class="_formLinks">
<FormLink v-for="scope in scopes" :to="`/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
</div>
</FormSection>
<div v-if="scopesWithDomain" class="_gaps_m">
<FormSection v-for="domain in scopesWithDomain" :key="domain.domain">
<template #label>{{ domain.domain ? domain.domain.toUpperCase() : i18n.ts.system }}</template>
<div class="_gaps_s">
<FormLink v-for="scope in domain.scopes" :to="`/registry/keys/${domain.domain ?? '@'}/${scope.join('/')}`" class="_monospace">{{ scope.length === 0 ? '(root)' : scope.join('/') }}</FormLink>
</div>
</FormSection>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
@ -28,11 +30,11 @@ import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
let scopes = $ref(null);
let scopesWithDomain = $ref(null);
function fetchScopes() {
os.api('i/registry/scopes').then(res => {
scopes = res.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
os.api('i/registry/scopes-with-domain').then(res => {
scopesWithDomain = res;
});
}

View File

@ -51,7 +51,8 @@ function submit() {
os.alert({
type: 'error',
text: i18n.ts.somethingHappened,
title: i18n.ts.somethingHappened,
text: i18n.ts.signupPendingError,
});
});
}

View File

@ -1,124 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.container">
<div :class="$style.title">
<div :class="$style.titleText"><i class="ti ti-info-circle"></i> {{ i18n.ts._timelineTutorial.title }}</div>
<div :class="$style.step">
<button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--">
<i class="ti ti-chevron-left"></i>
</button>
<span :class="$style.stepNumber">{{ tutorial + 1 }} / {{ tutorialsNumber }}</span>
<button class="_button" :class="$style.stepArrow" :disabled="tutorial === tutorialsNumber - 1" @click="tutorial++">
<i class="ti ti-chevron-right"></i>
</button>
</div>
</div>
<div v-if="tutorial === 0" :class="$style.body">
<div>{{ i18n.t('_timelineTutorial.step1_1', { name: instance.name ?? host }) }}</div>
<div>{{ i18n.t('_timelineTutorial.step1_2', { name: instance.name ?? host }) }}</div>
<div>{{ i18n.t('_timelineTutorial.step1_3', { name: instance.name ?? host }) }}</div>
</div>
<div v-else-if="tutorial === 1" :class="$style.body">
<div>{{ i18n.ts._timelineTutorial.step2_1 }}</div>
<div>{{ i18n.t('_timelineTutorial.step2_2', { name: instance.name ?? host }) }}</div>
</div>
<div v-else-if="tutorial === 2" :class="$style.body">
<div>{{ i18n.ts._timelineTutorial.step3_1 }}</div>
<div>{{ i18n.ts._timelineTutorial.step3_2 }}</div>
</div>
<div v-else-if="tutorial === 3" :class="$style.body">
<div>{{ i18n.ts._timelineTutorial.step4_1 }}</div>
<div>{{ i18n.ts._timelineTutorial.step4_2 }}</div>
</div>
<div :class="$style.footer">
<template v-if="tutorial === tutorialsNumber - 1">
<MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial = -1">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton>
</template>
<template v-else>
<MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial++">{{ i18n.ts.next }} <i class="ti ti-arrow-right"></i></MkButton>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
const tutorialsNumber = 4;
const tutorial = computed({
get() { return defaultStore.reactiveState.timelineTutorial.value || 0; },
set(value) { defaultStore.set('timelineTutorial', value); },
});
</script>
<style lang="scss" module>
.small {
opacity: 0.7;
}
.container {
border: solid 2px var(--accent);
}
.title {
display: flex;
flex-wrap: wrap;
padding: 22px 32px;
font-weight: bold;
&Text {
margin: 4px 0;
padding-right: 4px;
}
}
.step {
margin-left: auto;
&Arrow {
padding: 4px;
&:disabled {
opacity: 0.5;
}
&:first-child {
padding-right: 8px;
}
&:last-child {
padding-left: 8px;
}
}
&Number {
font-weight: normal;
margin: 4px;
}
}
.body {
padding: 0 32px;
}
.footer {
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: right;
padding: 22px 32px;
&Item {
margin: 4px;
}
}
</style>

View File

@ -8,7 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
<MkSpacer :contentMax="800">
<div ref="rootEl" v-hotkey.global="keymap">
<XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
{{ i18n.ts._timelineDescription[src] }}
</MkInfo>
<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
@ -31,9 +33,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent, computed, watch, provide } from 'vue';
import { computed, watch, provide } from 'vue';
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import { scroll } from '@/scripts/scroll.js';
import * as os from '@/os.js';
@ -48,8 +51,6 @@ import { deviceKind } from '@/scripts/device-kind.js';
provide('shouldOmitHeaderTitle', true);
const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable && defaultStore.state.showGlobalTimeline) || ($i != null && $i.policies.gtlAvailable && defaultStore.state.showGlobalTimeline);
const keymap = {
@ -141,6 +142,13 @@ function focus(): void {
tlComponent.focus();
}
function closeTutorial(): void {
if (!['home', 'local', 'social', 'global'].includes(src)) return;
const before = defaultStore.state.timelineTutorials;
before[src] = true;
defaultStore.set('timelineTutorials', before);
}
const headerActions = $computed(() => {
const tmp = [
{icon: 'ti ti-dots',

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MarqueeText :duration="40">
<MkA v-for="instance in instances" :key="instance.id" :class="$style.federationInstance" :to="`/instance-info/${instance.host}`" behavior="window">
<!--<MkInstanceCardMini :instance="instance"/>-->
<img v-if="instance.iconUrl" class="icon" :src="instance.iconUrl" alt=""/>
<img v-if="instance.iconUrl" class="icon" :src="getInstanceIcon(instance)" alt=""/>
<span class="name _monospace">{{ instance.host }}</span>
</MkA>
</MarqueeText>
@ -46,10 +46,15 @@ import { instance } from '@/instance.js';
import number from '@/filters/number.js';
import MkNumber from '@/components/MkNumber.vue';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
let meta = $ref<Misskey.entities.Instance>();
let instances = $ref<any[]>();
function getInstanceIcon(instance): string {
return getProxiedImageUrl(instance.iconUrl, 'preview');
}
os.api('meta', { detail: true }).then(_meta => {
meta = _meta;
});

View File

@ -4,7 +4,7 @@
*/
import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue';
import { Router } from '@/nirax';
import { Router } from '@/nirax.js';
import { $i, iAmModerator } from '@/account.js';
import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue';
@ -322,10 +322,10 @@ export const routes = [{
name: 'avatarDecorations',
component: page(() => import('./pages/avatar-decorations.vue')),
}, {
path: '/registry/keys/system/:path(*)?',
path: '/registry/keys/:domain/:path(*)?',
component: page(() => import('./pages/registry.keys.vue')),
}, {
path: '/registry/value/system/:path(*)?',
path: '/registry/value/:domain/:path(*)?',
component: page(() => import('./pages/registry.value.vue')),
}, {
path: '/registry',

View File

@ -82,6 +82,7 @@ export const ACHIEVEMENT_TYPES = [
'cookieClicked',
'brainDiver',
'smashTestNotificationButton',
'tutorialCompleted',
] as const;
export const ACHIEVEMENT_BADGES = {
@ -465,6 +466,11 @@ export const ACHIEVEMENT_BADGES = {
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'tutorialCompleted': {
img: '/fluent-emoji/1f393.png',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
img: string;

View File

@ -105,6 +105,11 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
const isImage = file.type.startsWith('image/');
let menu;
menu = [{
type: 'link',
to: `/my/drive/file/${file.id}`,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
}, null, {
text: i18n.ts.rename,
icon: 'ti ti-forms',
action: () => rename(file),
@ -140,11 +145,6 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
text: i18n.ts.download,
icon: 'ti ti-download',
download: file.name,
}, null, {
type: 'link',
to: `/my/drive/file/${file.id}`,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-file',
}, null, {
text: i18n.ts.delete,
icon: 'ti ti-trash',

View File

@ -17,6 +17,7 @@ import { miLocalStorage } from '@/local-storage.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import { clipsCache } from '@/cache.js';
import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
export async function getNoteClipMenu(props: {
note: Misskey.entities.Note;
@ -427,3 +428,122 @@ export function getNoteMenu(props: {
cleanup,
};
}
type Visibility = 'public' | 'home' | 'followers' | 'specified';
// defaultStore.state.visibilityがstringなためstringも受け付けている
function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
if (a === 'specified' || b === 'specified') return 'specified';
if (a === 'followers' || b === 'followers') return 'followers';
if (a === 'home' || b === 'home') return 'home';
// if (a === 'public' || b === 'public')
return 'public';
}
export function getRenoteMenu(props: {
note: Misskey.entities.Note;
renoteButton: Ref<HTMLElement>;
mock?: boolean;
}) {
const isRenote = (
props.note.renote != null &&
props.note.text == null &&
props.note.fileIds.length === 0 &&
props.note.poll == null
);
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
const channelRenoteItems: MenuItem[] = [];
const normalRenoteItems: MenuItem[] = [];
if (appearNote.channel) {
channelRenoteItems.push(...[{
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
const el = props.renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
if (!props.mock) {
os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
});
}
},
}, {
text: i18n.ts.inChannelQuote,
icon: 'ti ti-quote',
action: () => {
if (!props.mock) {
os.post({
renote: appearNote,
channel: appearNote.channel,
});
}
},
}]);
}
if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
normalRenoteItems.push(...[{
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
const el = props.renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
let visibility = appearNote.visibility;
visibility = smallerVisibility(visibility, configuredVisibility);
if (appearNote.channel?.isSensitive) {
visibility = smallerVisibility(visibility, 'home');
}
if (!props.mock) {
os.api('notes/create', {
localOnly,
visibility,
renoteId: appearNote.id,
}).then(() => {
os.toast(i18n.ts.renoted);
});
}
},
}, (props.mock) ? undefined : {
text: i18n.ts.quote,
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
});
},
}]);
}
// nullを挟むことで区切り線を出せる
const renoteItems = [
...normalRenoteItems,
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [],
...channelRenoteItems,
];
return {
menu: renoteItems,
};
}

View File

@ -37,7 +37,7 @@ export function useTooltip(
};
autoHidingTimer = window.setInterval(() => {
if (!document.body.contains(elRef.value)) {
if (elRef.value == null || !document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) {
if (!isHovering) return;
isHovering = false;
window.clearTimeout(timeoutId);

View File

@ -54,9 +54,14 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account',
default: 0,
},
timelineTutorial: {
timelineTutorials: {
where: 'account',
default: 0,
default: {
home: false,
local: false,
social: false,
global: false,
},
},
keepCw: {
where: 'account',

View File

@ -352,12 +352,6 @@ hr {
grid-gap: 12px;
}
._formLinks {
> *:not(:last-child) {
margin-bottom: 8px;
}
}
._beta {
margin-left: 0.7em;
font-size: 65%;

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineAsyncComponent } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { instance } from '@/instance.js';
@ -102,7 +103,13 @@ export function openInstanceMenu(ev: MouseEvent) {
action: () => {
window.open('https://misskey-hub.net/help.html', '_blank');
},
}, {
}, ($i) ? {
text: i18n.ts._initialTutorial.launchTutorial,
icon: 'ti ti-presentation',
action: () => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {}, 'closed');
},
} : undefined, {
type: 'link',
text: i18n.ts.aboutMisskey,
to: '/about-misskey',

View File

@ -62,6 +62,12 @@ watch($$(withRenotes), v => {
});
});
watch($$(withReplies), v => {
updateColumn(props.column.id, {
withReplies: v,
});
});
watch($$(onlyFiles), v => {
updateColumn(props.column.id, {
onlyFiles: v,

View File

@ -15,7 +15,7 @@ Issueを作成する前に、以下をご確認ください:
- 重複を防ぐため、既に同様の内容のIssueが作成されていないか検索してから新しいIssueを作ってください。
- Issueを質問に使わないでください。
- Issueは、要望、提案、問題の報告にのみ使用してください。
- 質問は、[Misskey Forum](https://forum.misskey.io/)や[Discord](https://discord.gg/Wp8gVStHW3)でお願いします。
- 質問は、[GitHub Discussions](https://github.com/misskey-dev/misskey/discussions)や[Discord](https://discord.gg/Wp8gVStHW3)でお願いします。
## PRの作成
PRを作成する前に、以下をご確認ください:

View File

@ -11,7 +11,7 @@ Before creating an issue, please check the following:
- To avoid duplication, please search for similar issues before creating a new issue.
- Do not use Issues as a question.
- Issues should only be used to feature requests, suggestions, and report problems.
- Please ask questions in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
- Please ask questions in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3).
## Creating a PR
Thank you for your PR! Before creating a PR, please check the following:

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