Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2023-11-06 17:21:36 +09:00
commit b45b833013
54 changed files with 701 additions and 186 deletions

View file

@ -12,7 +12,10 @@
-->
## 2023.11.0 (unreleased)
## 2023.11.0
### Note
- iOS 16.4未満を使用している場合はiOS 16.4以上にアップデートをお願いします
### General
- Feat: アイコンデコレーション機能
@ -26,6 +29,7 @@
- ユーザーが誤ったメールアドレスを入力した場合に招待コードが失効してしまう問題が解消されます。
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
- Enhance: 未読の通知数を表示できるように
- Enhance: 通知されず、確認の必要もないお知らせ(silence)を作成可能になりました
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
- Change: CWを使用する場合、注釈を空にすることは許可されなくなりました
@ -47,6 +51,7 @@
- Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでートを非表示にできるようになりました
- Enhance: AiScript関数`Mk:nyaize()`が追加されました
- Enhance: 情報→ツール はナビゲーションバーにツールとして独立した項目になりました
- Enhance: ノート内の絵文字をクリックすることで、コピーおよびリアクションができるように
- Enhance: その他細かなブラッシュアップ
- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正
- Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう
@ -59,6 +64,8 @@
- Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174
- Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224
- Fix: In deck layout, replies option is not saved after refresh
- Fix: アーカイブしたお知らせがコントロールパネルに表示される問題を修正
- Note: アップデート後、サウンドに関する設定が初期化されます
### Server
- Feat: Registry APIがサードパーティから利用可能になりました
@ -67,6 +74,7 @@
- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました
- 相手がMisskey v2023.11.0以降である必要があります
- Enhance: チャンネル取得時のパフォーマンスを向上
- Enhance: AP: ApplicationタイプのアカウントをisBotとして扱うように
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正
- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正
@ -78,6 +86,7 @@
- Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正
- Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正
- Fix: サーバーサイドからのテスト通知を正しく行えるように修正
- Fix: GTLの「リートを表示」オプションが機能しないのを修正 #12233
## 2023.10.2

View file

@ -1245,7 +1245,9 @@ refreshing: "Refreshing..."
pullDownToRefresh: "Pull down to refresh"
disableStreamingTimeline: "Disable real-time timeline updates"
useGroupedNotifications: "Display grouped notifications"
signupPendingError: "There was a problem verifying the email address. The link may have expired."
cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided."
doReaction: "Add reaction"
showUnreadNotificationsCount: "Show the number of unread notifications"
showCatOnly: "Show only cats"
additionalPermissionsForFlash: "Allow to add permission to Play"
@ -1277,6 +1279,8 @@ _announcement:
tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete."
readConfirmTitle: "Mark as read?"
readConfirmText: "This will mark the contents of \"{title}\" as read."
shouldNotBeUsedToPresentPermanentInfo: "As it may significantly impact the user experience for new users, it is recommended to use notifications in the flow information rather than stock information."
dialogAnnouncementUxWarn: "Having two or more dialog-style notifications simultaneously can significantly impact the user experience, so please use them carefully."
_group:
leader: "Group owner"
banish: "Banish"
@ -1333,9 +1337,80 @@ _initialAccountSetting:
pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device."
initialAccountSettingCompleted: "Profile setup complete!"
haveFun: "Enjoy {name}!"
youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (CherryPick) or you can exit the setup here and start using it immediately."
startTutorial: "Start Tutorial"
skipAreYouSure: "Really skip profile setup?"
skipAreYouSureDescription: "If you interrupt initialization now, you can resume it at [More! - Help - Replay initial setting]."
laterAreYouSure: "Really do profile setup later?"
_initialTutorial:
launchTutorial: "Start Tutorial"
title: "Tutorial"
wellDone: "Well done!"
skipAreYouSure: "Quit Tutorial?"
_landing:
title: "Welcome to the Tutorial"
description: "Here, you can learn the basics of using CherryPick and its features."
_note:
title: "What is a Note?"
description: "Posts on CherryPick are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time."
reply: "Click on this button to reply to a message. It's also possible to reply to replies, continuing the conversation like a thread."
renote: "You can share that note to your own timeline. You can also quote them with your comments."
like: "You can add heart reactions to the Note. which is useful if you want to quickly leave a \"like!\"."
reaction: "You can add reactions to the Note. More details will be explained on the next page."
quote: "You can add a quote. Which is useful if you want to add a comment based on something."
menu: "You can view Note details, copy links, and perform various other actions."
_reaction:
title: "What are Reactions?"
description: "Notes can be reacted to with various emojis. Reactions allow you to express nuances that may not be conveyed with just a 'like.'"
letsTryReacting: "Reactions can be added by clicking the '+' button on the note. Try reacting to this sample note!"
reactToContinue: "Add a reaction to proceed."
reactNotification: "You'll receive real-time notifications when someone reacts to your note."
reactDone: "You can undo a reaction by pressing the '-' button."
_timeline:
title: "The Concept of Timelines"
description1: "CherryPick provides multiple timelines based on usage (some may not be available depending on the server's policies)."
home: "You can view notes from accounts you follow."
local: "You can view notes from all users on this server."
social: "Notes from the Home and Local timelines will be displayed."
global: "You can view notes from all connected servers."
description2: "You can switch between timelines at the top of the screen at any time."
description3: "Additionally, there are list timelines and channel timelines. For more details, please refer to {link}."
_postNote:
title: "Note Posting Settings"
description1: "When posting a note on CherryPick, various options are available. The posting form looks like this."
_visibility:
description: "You can limit who can view your note."
public: "Your note will be visible for all users."
home: "Public only on the Home timeline. People visiting your profile, via followers, and through renotes can see it."
followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be renoted by others."
direct: "Visible only to specified users, and the recipient will be notified. It can be used as an alternative to direct messaging."
doNotSendConfidencialOnDirect1: "Be careful when sending sensitive information!"
doNotSendConfidencialOnDirect2: "Administrators of the server can see what you write. Be careful with sensitive information when sending direct notes to users on untrusted servers."
localOnly: "Posting with this flag will not federate the note to other servers. Users on other servers will not be able to view these notes directly, regardless of the display settings above."
_cw:
title: "Content Warning"
description: "Instead of the body, the content written in 'comments' field will be displayed. Pressing \"read more\" will reveal the body."
_exampleNote:
cw: "This will surely make you hungry!"
note: "Just had a chocolate-glazed donut 🍩😋"
useCases: "This is used when following the server guidelines for necessary notes or for self-restriction of spoiler or sensitive text."
_howToMakeAttachmentsSensitive:
title: "How to Mark Attachments as Sensitive?"
description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag."
tryThisFile: "Try marking the image attached in this form as sensitive!"
_exampleNote:
note: "Oops, messed up opening the natto lid..."
method: "To mark an attachment as sensitive, click the file thumbnail, open the menu, and click \"Mark as Sensitive.\""
sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines."
doItToContinue: "Mark the attachment file as sensitive to proceed."
_done:
title: "The tutorial is complete! 🎉"
description: "The functions introduced here are just a small part. For a more detailed understanding of using CherryPick, please refer to {link}."
_timelineDescription:
home: "In the Home timeline, you can see notes from accounts you follow."
local: "In the Local timeline, you can see notes from all users on this server."
social: "The Social timeline displays notes from both the Home and Local timelines."
global: "In the Global timeline, you can see notes from all connected servers."
_serverRules:
description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended."
_event:
@ -1631,6 +1706,9 @@ _achievements:
_smashTestNotificationButton:
title: "Test overflow"
description: "Trigger the notification test repeatedly within an extremely short time"
_tutorialCompleted:
title: "CherryPick Elementary Course Diploma"
description: "Tutorial completed"
_role:
new: "New role"
edit: "Edit role"
@ -1912,6 +1990,7 @@ _channel:
notesCount: "{n} Notes"
nameAndDescription: "Name and description"
nameOnly: "Name only"
allowRenoteToExternal: "Allow renote and quote outside the channel"
_menuDisplay:
sideFull: "Side"
sideIcon: "Side (Icons)"

View file

@ -974,7 +974,7 @@ numberOfLikes: "Favoris"
show: "Affichage"
neverShow: "Ne plus afficher"
remindMeLater: "Peut-être plus tard"
didYouLikeMisskey: "Avez-vous aimé Misskey ?"
didYouLikeMisskey: "Avez-vous aimé CherryPick ?"
roles: "Rôles"
role: "Rôles"
noRole: "Aucun rôle"
@ -1102,7 +1102,26 @@ _initialAccountSetting:
profileSetting: "Paramètres du profil"
privacySetting: "Paramètres de confidentialité"
initialAccountSettingCompleted: "Configuration du profil terminée avec succès !"
startTutorial: "Démarrer le tutoriel"
skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?"
_initialTutorial:
title: "Tutoriel"
wellDone: "Bien joué !"
skipAreYouSure: "Quitter le tutoriel ?"
_landing:
title: "Bienvenue dans le tutoriel"
description: "Ici, vous pouvez apprendre l'utilisation de base de CherryPick et ses fonctionnalités."
_note:
title: "Qu'est-ce que les notes ?"
description: "Les messages sur CherryPick sont appelés des « notes » . Les notes sont classées par ordre chronologique sur le fil et sont mises à jour en temps réel."
reply: "Vous pouvez répondre aux messages. Vous pouvez également répondre aux réponses et poursuivre la conversation comme un fil de discussion."
renote: "Vous pouvez partager cette note sur votre propre fil. Vous pouvez aussi ajouter du texte en citant."
reaction: "Vous pouvez ajouter des réactions. Les détails sont expliqués à la page suivante."
menu: "Vous pouvez afficher les détails de la note, copier le lien et effectuer d'autres actions."
_reaction:
title: "Qu'est-ce que les réactions ?"
description: "Vous pouvez ajouter des « réactions » aux notes. Les réactions vous permettent d'exprimer à l'aise des nuances qui ne peuvent pas être exprimées par des mentions j'aime."
letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « + » de la note. Essayez d'ajouter une réaction à cet exemple de note !"
_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."
@ -1237,7 +1256,7 @@ _achievements:
_cookieClicked:
flavor: "Attendez une minute, vous êtes sur le mauvais site web ?"
_brainDiver:
flavor: "Misskey-Misskey La-Tu-Ma"
flavor: "CherryPick-CherryPick La-Tu-Ma"
_role:
new: "Nouveau rôle"
edit: "Modifier le rôle"

3
locales/index.d.ts vendored
View file

@ -1250,6 +1250,7 @@ export interface Locale {
"useGroupedNotifications": string;
"signupPendingError": string;
"cwNotationRequired": string;
"doReaction": string;
"showUnreadNotificationsCount": string;
"showCatOnly": string;
"additionalPermissionsForFlash": string;
@ -1287,6 +1288,8 @@ export interface Locale {
"readConfirmText": string;
"shouldNotBeUsedToPresentPermanentInfo": string;
"dialogAnnouncementUxWarn": string;
"silence": string;
"silenceDescription": string;
};
"_group": {
"leader": string;

View file

@ -54,6 +54,19 @@ const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g')
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => {
for (const [k, v] of Object.entries(obj)) {
if (v === '') {
delete obj[k];
} else if (typeof v === 'object') {
removeEmpty(v);
}
}
return obj;
};
removeEmpty(locales);
export default Object.entries(locales)
.reduce((a, [k ,v]) => (a[k] = (() => {
const [lang] = k.split('-');
@ -65,7 +78,7 @@ export default Object.entries(locales)
locales['ja-JP'],
locales['ko-KR'],
locales['en-US'],
locales[`${lang}-${primaries[lang]}`] || {},
locales[`${lang}-${primaries[lang]}`] ?? {},
v
);
}

View file

@ -602,7 +602,7 @@ invisibleNote: "Nota invisibile"
enableInfiniteScroll: "Abilita scorrimento infinito"
visibility: "Visibilità"
poll: "Sondaggio"
useCw: "Content Warning"
useCw: "Contenuto esplicito"
enablePlayer: "Visualizza"
disablePlayer: "Chiudi"
expandTweet: "Espandi tweet"
@ -820,8 +820,8 @@ user: "Profilo"
administration: "Gestione"
accounts: "Profilo"
switch: "Cambia"
noMaintainerInformationWarning: "Le informazioni amministratore non sono impostate."
noBotProtectionWarning: "Nessuna protezione impostata contro i bot."
noMaintainerInformationWarning: "Mancano le informazioni sull'amministratore."
noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot"
configure: "Imposta"
postToGallery: "Pubblicare nella galleria"
postToHashtag: "Pubblica a questo hashtag"
@ -859,7 +859,7 @@ accountDeletionInProgress: "È in corso l'eliminazione del profilo"
usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito."
aiChanMode: "Modalità Ai"
devMode: "Modalità sviluppatori"
keepCw: "Mantieni il Content Warning"
keepCw: "Mostra i contenuti espliciti"
pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente"
resolved: "Risolto"
@ -1169,6 +1169,9 @@ refreshing: "Aggiornamento..."
pullDownToRefresh: "Trascina per aggiornare"
disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo reale"
useGroupedNotifications: "Mostra le notifiche raggruppate"
signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo."
cwNotationRequired: "Devi indicare perché il contenuto è indicato come esplicito."
doReaction: "Reagisci"
_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."
@ -1178,6 +1181,8 @@ _announcement:
tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi."
readConfirmTitle: "Segnare come già letto?"
readConfirmText: "Hai già letto \"{title}˝?"
shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte."
dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte."
_initialAccountSetting:
accountCreated: "Il tuo profilo è stato creato!"
letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo."
@ -1190,8 +1195,77 @@ _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}!"
youCanContinueTutorial: "Puoi continuare con l'esercitazione su come usare {name} (CherryPick), oppure interrompere, iniziando subito a usarlo."
startTutorial: "Avvia l'esercitazione"
skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?"
laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?"
_initialTutorial:
launchTutorial: "Guarda il tutorial"
title: "Tutorial"
wellDone: "Ottimo lavoro!"
skipAreYouSure: "Vuoi davvero interrompere il tutorial?"
_landing:
title: "Eccoci nel tutorial"
description: "Qui puoi verificare l'uso delle funzionalità base di CherryPick."
_note:
title: "Cosa sono le Note?"
description: "Gli status su CherryPick sono chiamati \"Note\". Le Note sono elencate in ordine cronologico nelle timeline e vengono aggiornate in tempo reale."
reply: "Puoi rispondere alle Note. Puoi anche rispondere alle risposte e continuare i dialoghi come un conversazioni."
renote: "Puoi ri-condividere le Note, facendole rifluire sulla Timeline. Puoi anche aggiungere testo e citare altri profili."
reaction: "Puoi aggiungere una reazione. Nella pagina successiva spiegheremo i dettagli."
menu: "Puoi svolgere varie attività, come visualizzare i dettagli delle Note o copiare i collegamenti."
_reaction:
title: "Cosa sono le Reazioni?"
description: "Puoi reagire alle Note. Le sensazioni che non si riescono a trasmettere con i \"Mi piace\" si possono esprimere facilmente inviando una reazione."
letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"+\" (più) della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!"
reactToContinue: "Aggiungere la Reazione ti consentirà di procedere col tutorial."
reactNotification: "Quando qualcuno reagisce alle tue Note, ricevi una notifica in tempo reale."
reactDone: "Puoi annullare la tua Reazione premendo il bottone \"ー\" (meno)"
_timeline:
title: "Come funziona la Timeline"
description1: "CherryPick fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori."
home: "Puoi vedere le Note provenienti dai profili che segui (follow)."
local: "Puoi vedere tutte le Note pubblicate dai profili di questa istanza."
social: "Puoi vedere sia le Note della Timeline Home che quelle della Timeline Locale, insieme!"
global: "Puoi vedere le Note da pubblicate da tutte le altre istanze federate con la nostra."
description2: "Nella parte superiore dello schermo, puoi scegliere una Timeline o l'altra in qualsiasi momento."
description3: "Ci sono anche sequenze temporali di elenchi, sequenze temporali di canali, ecc. Per ulteriori dettagli, consultare il {link}.\nPuoi vedere anche Timeline delle liste di profili (se ne hai create), canali, ecc... Per i dettagli, visita {link}."
_postNote:
title: "La Nota e le sue impostazioni"
description1: "Quando scrivi una Nota su CherryPick, hai a disposizione varie opzioni. Il modulo di invio è simile a questo."
_visibility:
description: "Puoi limitare chi può vedere la tua Nota."
public: "Visibile a tutti."
home: "Pubblicato solo sulla Timeline Home (personale). Visibile anche da profili remoti follower, visitatori del tuo profilo e tramite i Rinota dei follower."
followers: "Visibile solo ai profili tuoi follower (locali o remoti). Nessun altro oltre a te può \"Rinotare\"."
direct: "Visibile solo ai profili specificati, i quali riceveranno una notifica. Puoi usarlo come se fossero messaggi diretti."
doNotSendConfidencialOnDirect1: "Attenzione, quando si inviano informazioni confidenziali."
doNotSendConfidencialOnDirect2: "Poiché le Note non sono crittografate, l'amministratore del server di destinazione potrebbe leggere cosa è stato scritto, quindi se spedisci una Nota diretta a un profilo che risiede su un server non attendibile, evita di scrivere informazioni riservate."
localOnly: "Indipendentemente dalla visualizzazione sopra indicata, i profili su altri server non saranno in grado di visualizzare la Nota, se questa impostazione è attivata. Non non verrà comunicata ad altri server."
_cw:
title: "Nascondere il contenuto esplicito"
description: "Verrà visualizzato il testo scritto nel campo \"Annotazione preventiva\" al posto del testo principale della Nota. Premere il bottone \"Continua la lettura\" se si intende davvero leggere il testo."
_exampleNote:
cw: "Attenzione: contiene zuccheri"
note: "Ho appena mangiato una ciambella ricoperta di cioccolato 🍩😋"
useCases: "Utilizzalo per chiarire il contenuto della Nota, prima che sia letta. Come richiesto dal regolamento del server o per autoregolamentare spoiler e testi troppo espliciti."
_howToMakeAttachmentsSensitive:
title: "Come indicare che gli allegati sono espliciti?"
description: "Contrassegnare gli allegati come espliciti, va fatto quando è richiesto dal regolamento del server o quando gli allegati non devono essere immediatamente visibili."
tryThisFile: "Prova a rendere esplicite le immagini allegate a questo modulo!"
_exampleNote:
note: "Ho fatto un errore aprendo il coperchio del natto... (fagioli di soia fermentati, particolarmente appiccicosi)"
method: "Per indicare che un allegato è esplicito, tocca il file per aprirne il menu e scegliere la voce \"Segna come esplicito\"."
sensitiveSucceeded: "Quando alleghi file, assicurati di indicare se è materiale esplicito, in modo appropriato, in base al regolamento del tuo server."
doItToContinue: "Impostando l'immagine come esplicita, potrai procedere col tutorial."
_done:
title: "Il tutorial è finito! 🎉"
description: "Queste sono solamente alcune delle funzionalità principali di CherryPick. Per ulteriori informazioni, {link}."
_timelineDescription:
home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (follow)."
local: "La Timeline Locale, è una cronologia di Note pubblicate da tutti i profili iscritti su questo server."
social: "La Timeline Sociale, unisce in ordine cronologico l'elenco di Note presenti nella Timeline Home e quella Locale."
global: "La Timeline Federata ti consente di vedere le Note pubblicate dai profili di tutti gli altri server federati a questo."
_serverRules:
description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio."
_serverSettings:
@ -1461,6 +1535,9 @@ _achievements:
_smashTestNotificationButton:
title: "Prove eccessive"
description: "Hai provato le notifiche consecutivamente in un periodo di tempo molto breve"
_tutorialCompleted:
title: "Attestato di partecipazione al corso per principianti di CherryPick"
description: "Ha completato il tutorial"
_role:
new: "Nuovo ruolo"
edit: "Modifica ruolo"
@ -1715,6 +1792,7 @@ _channel:
notesCount: "{n} note"
nameAndDescription: "Nome e descrizione"
nameOnly: "Solo il nome"
allowRenoteToExternal: "Consenti i Rinota e le citazioni all'esterno del canale"
_menuDisplay:
sideFull: "Laterale"
sideIcon: "Laterale (solo icone)"

View file

@ -1247,6 +1247,7 @@ disableStreamingTimeline: "タイムラインのリアルタイム更新を無
useGroupedNotifications: "通知をグルーピングして表示する"
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
doReaction: "リアクションする"
showUnreadNotificationsCount: "未読の通知の数を表示する"
showCatOnly: "キャット付きのみ"
additionalPermissionsForFlash: "Playへの追加許可"
@ -1285,6 +1286,8 @@ _announcement:
readConfirmText: "「{title}」の内容を読み、既読にします。"
shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。"
dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。"
silence: "非通知"
silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。"
_group:
leader: "グループオーナー"
@ -1365,7 +1368,7 @@ _initialTutorial:
description: "CherryPickでの投稿は「ート」と呼びます。ートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。"
reply: "返信することができます。返信に対しての返信も可能で、スレッドのように会話を続けることもできます。"
renote: "そのノートを自分のタイムラインに流して共有することができます。テキストを追加して引用することも可能です。"
like: "ハートリアクションを送ることができます。"
like: "ハートリアクションを送ることができます。「いいね!」を素早く残したいときに便利です。"
reaction: "リアクションをつけることができます。詳しくは次のページで解説します。"
quote: "引用を付けることができます。何らかの内容に基づき意見を付け加えたいときに便利です。"
menu: "ノートの詳細を表示したり、リンクをコピーしたりなどの様々な操作が行えます。"

View file

@ -604,6 +604,7 @@ serverLogs: "서버 로그"
deleteAll: "모두 삭제"
showFixedPostForm: "타임라인 상단에 글 작성란 표시"
showFixedPostFormInChannel: "채널 타임라인 상단에 글 작성란 표시"
withRepliesByDefaultForNewlyFollowed: "팔로우 할 때 기본적으로 답글을 타임라인에 표시"
newNoteRecived: "새 노트가 있어요!"
newNoteRecivedCount: "{n}개의 새 노트가 있어요!"
sounds: "소리"
@ -1159,8 +1160,8 @@ update: "업데이트"
rolesThatCanBeUsedThisEmojiAsReaction: "이 이모지를 리액션으로 사용할 수 있는 역할"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "역할을 지정하지 않으면 누구나 이 이모지를 리액션으로 사용할 수 있어요."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "역할은 공개로 설정되어 있어야 해요."
cancelReactionConfirm: "리액션을 취소하시겠습니까?"
changeReactionConfirm: "리액션을 변경하시겠습니까?"
cancelReactionConfirm: "리액션을 취소할까요?"
changeReactionConfirm: "리액션을 변경할까요?"
later: "나중에"
goToMisskey: "CherryPick으로"
additionalEmojiDictionary: "이모지 추가 사전"
@ -1213,13 +1214,30 @@ showRenotes: "리노트 표시"
edited: "수정됨"
notificationRecieveConfig: "알림 설정"
mutualFollow: "맞팔로우"
showRepliesToOthersInTimeline: "타임라인에 다른 사람에게 보내는 답글을 포함"
hideRepliesToOthersInTimeline: "타임라인에 다른 사람에게 보내는 답글을 포함하지 않음"
showRepliesToOthersInTimelineAll: "타임라인에 팔로우 중인 모든 사람의 답글 표시"
hideRepliesToOthersInTimelineAll: "타임라인에 팔로우 중인 모든 사람의 답글 표시하지 않기"
confirmShowRepliesAll: "이 조작은 되돌릴 수 없어요! 정말로 타임라인에 현재 팔로우 중인 모든 사람의 답글을 표시하도록 설정할까요?"
confirmHideRepliesAll: "이 조작은 되돌릴 수 없어요! 정말로 타임라인에 현재 팔로우 중인 모든 사람의 답글을 표시하지 않도록 설정할까요?"
externalServices: "외부 서비스"
impressum: "운영자 정보"
impressumUrl: "운영자 정보 URL"
impressumDescription: "독일 등의 일부 나라와 지역에서는 꼭 표시해야 해요(Impressum)."
avatarDecorations: "아이콘 장식"
attach: "붙이기"
detach: "떼기"
angle: "각도"
flip: "플립"
showAvatarDecorations: "아이콘 장식 표시"
disableStreamingTimeline: "타임라인 실시간 업데이트 비활성화"
useGroupedNotifications: "알림을 묶어서 표시"
signupPendingError: "메일 주소 확인중에 문제가 발생했어요. 링크의 유효기간이 지났을 수도 있어요."
cwNotationRequired: "'내용 숨기기'를 체크했을 경우 주석을 작성해야 해요."
fileAttachedOnly: "파일이 포함된 노트만"
releaseToRefresh: "놓아서 새로 고침"
refreshing: "새로 고침 중"
pullDownToRefresh: "당겨서 새로 고침"
disableStreamingTimeline: "타임라인 실시간 업데이트 비활성화"
showUnreadNotificationsCount: "읽지 않은 알림 수 표시"
showCatOnly: "고양이만 보기"
additionalPermissionsForFlash: "Play에 대한 추가 권한"
@ -1307,9 +1325,28 @@ _initialAccountSetting:
pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있어요."
initialAccountSettingCompleted: "초기 설정을 모두 완료했어요!"
haveFun: "{name}와 함께 즐거운 시간 보내세요!"
youCanContinueTutorial: "여기서 {name}(CherryPick)의 사용법을 배워볼 수 있지만, 튜토리얼을 진행하지 않고 바로 시작할 수도 있어요."
startTutorial: "튜토리얼 시작"
skipAreYouSure: "초기 설정을 중단하시겠어요?"
skipAreYouSureDescription: "지금 초기 설정을 중단해도 [더 보기! - 도움말 - 초기 설정 다시 보기]에서 다시 진행할 수 있어요."
laterAreYouSure: "초기 설정을 나중에 진행할까요?"
_initialTutorial:
launchTutorial: "튜토리얼 보기"
title: "튜토리얼"
wellDone: "잘 하셨어요!"
skipAreYouSure: "튜토리얼을 종료할까요?"
_landing:
title: "튜토리얼에 어서오세요!"
description: "여기서는 CherryPick의 기본적인 사용법이나 기능을 확인할 수 있어요."
_note:
title: "노트란 무엇인가요?"
description: "CherryPick에서는 게시물을 '노트'라고 해요. 노트는 타임라인에 시간순으로 정렬되어 실시간으로 갱신돼요."
reply: "답글을 달 수 있어요. 답글에 답글을 다는 것도 할 수 있고, 스레드처럼 대화를 계속하는 것도 가능해요."
renote: "그 노트를 내 타임라인에 가져와서 공유할 수 있어요. 글을 추가해서 인용하는 것도 할 수 있답니다."
like: "하트 리액션을 달 수 있어요. '좋아요!'를 빠르게 남기고 싶을 때 유용해요."
reaction: "리액션을 달 수 있어요. 다음 페이지에서 자세하게 소개해 드릴게요!"
quote: "인용문을 달 수 있어요. 어떤 내용을 바탕으로 의견을 덧붙이고 싶을 때 유용해요."
menu: "노트의 세부 정보를 표시하거나 링크를 복사하는 등 다양한 조작을 할 수 있어요."
_serverRules:
description: "회원 가입 이전에 간단하게 표시할 서버 규칙이에요. 이용 약관의 요약으로 구성하는 것을 추천해요."
_event:

View file

@ -161,7 +161,7 @@ youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
flagAsBot: "此使用者是機器人"
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整 Misskey 內部系統將本帳戶識別為機器人。"
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整 CherryPick 內部系統將本帳戶識別為機器人。"
flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
@ -1169,7 +1169,9 @@ refreshing: "載入更新中"
pullDownToRefresh: "往下拉來更新內容"
disableStreamingTimeline: "停用時間軸的即時更新"
useGroupedNotifications: "分組顯示通知訊息"
signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。"
cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。"
doReaction: "做出反應"
_announcement:
forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
@ -1179,6 +1181,8 @@ _announcement:
tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。"
readConfirmTitle: "標記為已讀嗎?"
readConfirmText: "閱讀「{title}」的內容並標記為已讀。"
shouldNotBeUsedToPresentPermanentInfo: "由於可能會破壞使用者體驗,尤其是對於新使用者而言,我們建議使用公告來發布有時效性的資訊而不是固定不變的資訊。"
dialogAnnouncementUxWarn: "如果同時有 2 個以上對話方塊形式的公告存在,對於使用者體驗很可能會有不良的影響,因此建議謹慎使用。"
_initialAccountSetting:
accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
@ -1191,8 +1195,77 @@ _initialAccountSetting:
pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。"
initialAccountSettingCompleted: "初始設定完成了!"
haveFun: "盡情享受{name}吧!"
youCanContinueTutorial: "您可以繼續學習如何使用{name}(CherryPick),也可以就此打住,立即開始使用。"
startTutorial: "開始教學課程"
skipAreYouSure: "要略過初始設定嗎?"
laterAreYouSure: "稍後再重新進行初始設定嗎?"
_initialTutorial:
launchTutorial: "觀看教學課程"
title: "新手教學"
wellDone: "做得好"
skipAreYouSure: "結束教學模式?"
_landing:
title: "歡迎使用本教學課程"
description: "在這裡您可以查看CherryPick的基本使用方法和功能。"
_note:
title: "什麼是貼文?"
description: "在CherryPick上發布的內容稱為「貼文」。貼文在時間軸上按時間順序排列並即時更新。"
reply: "您可以回覆貼文,並像討論串一樣繼續對話。"
renote: "您可以將此貼文分享到自己的時間軸。您也可以在引用時添加文字。"
reaction: "您可以添加反應。詳細資訊將在下一頁進行說明。"
menu: "可執行各種操作,如查看貼文詳細資訊和複製連結。"
_reaction:
title: "什麼是反應?"
description: "您可以在貼文中添加「反應」。您可以使用反應輕鬆隨意地表達「最愛/大心」所無法傳達的細微差別。"
letsTryReacting: "可以透過點擊貼文上的「+」按鈕來添加反應。請嘗試在此範例貼文添加反應!"
reactToContinue: "添加反應以繼續教學課程。"
reactNotification: "當有人對您的貼文做出反應時會即時接收到通知。"
reactDone: "按下「-」按鈕可以取消反應。"
_timeline:
title: "時間軸如何運作"
description1: "CherryPick根據使用方式提供了多個時間軸伺服器可能會將部份時間軸停用。"
home: "您可以查看您追隨的使用者的貼文。"
local: "您可以看到此伺服器上所有使用者的貼文。"
social: "來自首頁時間軸和本地時間軸的貼文都會顯示。"
global: "可以看到其他已連接伺服器的貼文。"
description2: "您可以隨時在螢幕上方切換對應的時間軸。"
description3: "除此之外還有清單時間軸、頻道時間軸等。請參閱{link}以了解更多詳情。"
_postNote:
title: "貼文的發布設定"
description1: "在CherryPick上發布貼文時可以設定各種選項。發布表單如下所示。"
_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: "這裡介紹的功能只是其中的一小部分。要了解更多有關如何使用CherryPick的資訊請瀏覽{link}。"
_timelineDescription:
home: "在首頁時間線上,可以看到您追隨的使用者的貼文。"
local: "在本地時間軸上,可以看到此伺服器所有使用者的貼文。"
social: "在社交時間軸上,可以看到首頁與本地時間軸的貼文。"
global: "在公開時間軸上,可以看到其他已連接伺服器的貼文。\n"
_serverRules:
description: "設定在註冊頁面顯示的伺服器簡要規則。建議是服務條款的摘要。"
_serverSettings:
@ -1462,6 +1535,9 @@ _achievements:
_smashTestNotificationButton:
title: "過度測試"
description: "極短時間內連續測試通知"
_tutorialCompleted:
title: "CherryPick新手講座 結業證書"
description: "已完成教學課程"
_role:
new: "建立角色"
edit: "編輯角色"
@ -1717,6 +1793,7 @@ _channel:
notesCount: "有 {n} 篇貼文"
nameAndDescription: "名稱與說明"
nameOnly: "僅名稱"
allowRenoteToExternal: "允許在頻道外轉發和引用"
_menuDisplay:
sideFull: "橫向"
sideIcon: "橫向(圖示)"
@ -1949,7 +2026,7 @@ _widgets:
clicker: "點擊器"
_cw:
hide: "隱藏"
show: "瀏覽更多"
show: "顯示內容"
chars: "{count} 個字元"
files: "{count} 個檔案"
_poll:

View file

@ -1,7 +1,7 @@
{
"name": "cherrypick",
"version": "4.5.0-beta.4",
"basedMisskeyVersion": "2023.11.0-beta.9",
"basedMisskeyVersion": "2023.11.0",
"codename": "nasubi",
"repository": {
"type": "git",

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AnnouncementSilence1699141698112 {
name = 'AnnouncementSilence1699141698112'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" ADD "silence" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`CREATE INDEX "IDX_7b8d9225168e962f94ea517e00" ON "announcement" ("silence") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_7b8d9225168e962f94ea517e00"`);
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "silence"`);
}
}

View file

@ -47,6 +47,7 @@ export class AnnouncementService {
const q = this.announcementsRepository.createQueryBuilder('announcement')
.where('announcement.isActive = true')
.andWhere('announcement.silence = false')
.andWhere(new Brackets(qb => {
qb.orWhere('announcement.userId = :userId', { userId: user.id });
qb.orWhere('announcement.userId IS NULL');
@ -73,6 +74,7 @@ export class AnnouncementService {
icon: values.icon,
display: values.display,
forExistingUsers: values.forExistingUsers,
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
userId: values.userId,
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
@ -126,6 +128,7 @@ export class AnnouncementService {
display: values.display,
icon: values.icon,
forExistingUsers: values.forExistingUsers,
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
isActive: values.isActive,
});
@ -212,6 +215,7 @@ export class AnnouncementService {
icon: announcement.icon,
display: announcement.display,
needConfirmationToRead: announcement.needConfirmationToRead,
silence: announcement.silence,
forYou: announcement.userId === me?.id,
isRead: reads.some(read => read.announcementId === announcement.id),
}));

View file

@ -284,7 +284,7 @@ export class ApPersonService implements OnModuleInit {
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const isBot = getApType(object) === 'Service';
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@ -609,7 +609,7 @@ export class ApPersonService implements OnModuleInit {
emojis: emojiNames,
name: truncate(person.name, nameLength),
tags,
isBot: getApType(object) === 'Service',
isBot: getApType(object) === 'Service' || getApType(object) === 'Application',
isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null,

View file

@ -66,6 +66,12 @@ export class MiAnnouncement {
})
public forExistingUsers: boolean;
@Index()
@Column('boolean', {
default: false,
})
public silence: boolean;
@Index()
@Column({
...id(),

View file

@ -58,6 +58,7 @@ export const paramDef = {
icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' },
display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' },
forExistingUsers: { type: 'boolean', default: false },
silence: { type: 'boolean', default: false },
needConfirmationToRead: { type: 'boolean', default: false },
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
},
@ -78,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
icon: ps.icon,
display: ps.display,
forExistingUsers: ps.forExistingUsers,
silence: ps.silence,
needConfirmationToRead: ps.needConfirmationToRead,
userId: ps.userId,
}, me);

View file

@ -86,6 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
query.andWhere('announcement.isActive = true');
if (ps.userId) {
query.andWhere('announcement.userId = :userId', { userId: ps.userId });
} else {
@ -113,6 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
display: announcement.display,
isActive: announcement.isActive,
forExistingUsers: announcement.forExistingUsers,
silence: announcement.silence,
needConfirmationToRead: announcement.needConfirmationToRead,
userId: announcement.userId,
reads: reads.get(announcement)!,

View file

@ -35,6 +35,7 @@ export const paramDef = {
icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'] },
display: { type: 'string', enum: ['normal', 'banner', 'dialog'] },
forExistingUsers: { type: 'boolean' },
silence: { type: 'boolean' },
needConfirmationToRead: { type: 'boolean' },
isActive: { type: 'boolean' },
},
@ -63,6 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
display: ps.display,
icon: ps.icon,
forExistingUsers: ps.forExistingUsers,
silence: ps.silence,
needConfirmationToRead: ps.needConfirmationToRead,
isActive: ps.isActive,
}, me);

View file

@ -89,6 +89,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.where('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.where('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}

View file

@ -34,7 +34,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, shallowRef, watch } from 'vue';
import * as Misskey from 'cherrypick-js';
import { soundConfigStore } from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{

View file

@ -69,14 +69,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="container-type: inline-size;">
<MkEvent v-if="appearNote.event" :note="appearNote"/>
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/>
<Mfm v-if="appearNote.cw != ''" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
<MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;"/>
</p>
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts._ffVisibility.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
<Mfm
v-if="appearNote.text"
:parsedNodes="parsed"
:text="appearNote.text"
:author="appearNote.user"
:nyaize="'account'"
:emojiUrls="appearNote.emojis"
:enableEmojiMenu="true"
:enableEmojiMenuReaction="true"
/>
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && appearNote.text" style="padding-top: 5px; color: var(--accent);">
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @mousedown="translate()">{{ i18n.ts.translateNote }}</button>
<button v-else class="_button" @mousedown="translation = null">{{ i18n.ts.close }}</button>
@ -85,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/>
<div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}:</b><hr style="margin: 10px 0;">
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/>
<div v-if="translation.translator == 'ctav3'" style="margin-top: 10px; padding: 0 0 15px;">
<img v-if="!defaultStore.state.darkMode" src="/client-assets/color-short.svg" alt="" style="float: right;">
<img v-else src="/client-assets/white-short.svg" alt="" style="float: right;"/>
@ -299,6 +308,13 @@ const keymap = {
's': () => showContent.value !== showContent.value,
};
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction,
});
});
onMounted(() => {
globalEvents.on('showEl', (showEl_receive) => {
showEl = showEl_receive;

View file

@ -89,13 +89,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.noteContent">
<MkEvent v-if="appearNote.event" :note="appearNote"/>
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/>
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
<MkCwButton v-model="showContent" :note="appearNote"/>
</p>
<div v-show="appearNote.cw == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts._ffVisibility.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
<Mfm
v-if="appearNote.text"
:parsedNodes="parsed"
:text="appearNote.text"
:author="appearNote.user"
:nyaize="'account'"
:emojiUrls="appearNote.emojis"
:enableEmojiMenu="true"
:enableEmojiMenuReaction="true"
/>
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && appearNote.text" style="padding-top: 5px; color: var(--accent);">
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @mousedown="translate()">{{ i18n.ts.translateNote }}</button>
@ -105,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/>
<div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}:</b><hr style="margin: 10px 0;">
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/>
<div v-if="translation.translator == 'ctav3'" style="margin-top: 10px; padding: 0 0 15px;">
<img v-if="!defaultStore.state.darkMode" src="/client-assets/color-short.svg" alt="" style="float: right;">
<img v-else src="/client-assets/white-short.svg" alt="" style="float: right;"/>
@ -226,7 +235,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div>
<div>
<Mfm :text="text.trim()" :author="appearNote.user" :i="$i"/>
<Mfm :text="text.trim()" :author="appearNote.user"/>
</div>
<CodeDiff
:oldString="appearNote.noteEditHistory[index - 1] || ''"
@ -258,7 +267,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, ref, shallowRef } from 'vue';
import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
import * as mfm from 'cherrypick-mfm-js';
import * as Misskey from 'cherrypick-js';
import { CodeDiff } from 'v-code-diff';
@ -360,6 +369,13 @@ const keymap = {
's': () => showContent.value !== showContent.value,
};
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction,
});
});
let tab = $ref('replies');
let reactionTabType = $ref(null);

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<MkEvent v-if="note.event" :note="note"/>
<p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :i="$i" :emojiUrls="note.emojis"/>
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/>
<MkCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent">

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<MkEvent v-if="note.event" :note="note"/>
<p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :i="$i"/>
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'"/>
<MkCwButton v-model="showContent" style="width: 100%" :note="note"/>
</p>
<div v-show="note.cw == null || showContent">

View file

@ -166,7 +166,7 @@ defineExpose({
<style lang="scss" module>
.root {
overscroll-behavior: none;
overscroll-behavior: contain;
min-height: 100%;
background: var(--bg);

View file

@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted } from 'vue';
import { deviceKind } from '@/scripts/device-kind.js';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js';
import { vibrate } from '@/scripts/vibrate.js';
import { ColdDeviceStorage } from '@/store.js';
@ -33,8 +34,8 @@ const SCROLL_STOP = 10;
const MAX_PULL_DISTANCE = Infinity;
const FIRE_THRESHOLD = 230;
const RELEASE_TRANSITION_DURATION = 200;
const PULL_BRAKE_BASE = 2;
const PULL_BRAKE_FACTOR = 200;
const PULL_BRAKE_BASE = 1.5;
const PULL_BRAKE_FACTOR = 170;
let isPullStart = $ref(false);
let isPullEnd = $ref(false);
@ -61,18 +62,6 @@ const emit = defineEmits<{
(ev: 'refresh'): void;
}>();
function getScrollableParentElement(node) {
if (node == null) {
return null;
}
if (node.scrollHeight > node.clientHeight) {
return node;
} else {
return getScrollableParentElement(node.parentNode);
}
}
function getScreenY(event) {
if (supportPointerDesktop) {
return event.screenY;
@ -142,12 +131,9 @@ function moveEnd() {
}
}
function moving(event) {
function moving(event: TouchEvent | PointerEvent) {
if (!isPullStart || isRefreshing || disabled) return;
if (!scrollEl) {
scrollEl = getScrollableParentElement(rootEl);
}
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
pullDistance = 0;
isPullEnd = false;
@ -163,6 +149,10 @@ function moving(event) {
const moveHeight = moveScreenY - startScreenY!;
pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
if (pullDistance > 0) {
if (event.cancelable) event.preventDefault();
}
isPullEnd = pullDistance >= FIRE_THRESHOLD;
if (!isPullEnd) isVibrate = false;
@ -188,24 +178,48 @@ function setDisabled(value) {
disabled = value;
}
onMounted(() => {
// pull to refresh便
//supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';
function onScrollContainerScroll() {
const scrollPos = scrollEl!.scrollTop;
if (supportPointerDesktop) {
rootEl.addEventListener('pointerdown', moveStart);
// downup
window.addEventListener('pointerup', moveEnd);
rootEl.addEventListener('pointermove', moving, { passive: true });
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
if (scrollPos === 0) {
scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
registerEventListenersForReadyToPull();
} else {
rootEl.addEventListener('touchstart', moveStart);
rootEl.addEventListener('touchend', moveEnd);
rootEl.addEventListener('touchmove', moving, { passive: true });
scrollEl!.style.touchAction = 'auto';
unregisterEventListenersForReadyToPull();
}
}
function registerEventListenersForReadyToPull() {
if (rootEl == null) return;
rootEl.addEventListener('touchstart', moveStart, { passive: true });
rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falsepreventDefault使
}
function unregisterEventListenersForReadyToPull() {
if (rootEl == null) return;
rootEl.removeEventListener('touchstart', moveStart);
rootEl.removeEventListener('touchmove', moving);
}
onMounted(() => {
if (rootEl == null) return;
scrollEl = getScrollContainer(rootEl);
if (scrollEl == null) return;
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
rootEl.addEventListener('touchend', moveEnd, { passive: true });
registerEventListenersForReadyToPull();
});
onUnmounted(() => {
if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd);
if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
unregisterEventListenersForReadyToPull();
});
defineExpose({

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts._ffVisibility.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emojiUrls="note.emojis"/>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :emojiUrls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && note.text" style="padding-top: 5px; color: var(--accent);">
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @mousedown="translate()">{{ i18n.ts.translateNote }}</button>
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/>
<div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}:</b><hr style="margin: 10px 0;">
<Mfm :text="translation.text" :author="note.user" :i="$i" :emojiUrls="note.emojis"/>
<Mfm :text="translation.text" :author="note.user" :emojiUrls="note.emojis"/>
<div v-if="translation.translator == 'ctav3'" style="margin-top: 10px; padding: 0 0 15px;">
<img v-if="!defaultStore.state.darkMode" src="/client-assets/color-short.svg" alt="" style="float: right;">
<img v-else src="/client-assets/white-short.svg" alt="" style="float: right;"/>

View file

@ -6,7 +6,7 @@ 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"/>
<MkPostForm :class="$style.exampleRoot" :mock="true" :autofocus="false"/>
<MkFormSection>
<template #label>{{ i18n.ts.visibility }}</template>
<div class="_gaps">

View file

@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPostForm
:class="$style.exampleRoot"
:mock="true"
:autofocus="false"
:initialNote="exampleNote"
@fileChangeSensitive="doSucceeded"
></MkPostForm>

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
<div :class="$style.description">
<div v-if="user.description" :class="$style.mfm">
<Mfm :text="user.description" :author="user" :i="$i"/>
<Mfm :text="user.description" :author="user"/>
</div>
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
</div>

View file

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.username"><MkAcct :user="user"/></div>
</div>
<div :class="$style.description">
<Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/>
<Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user"/>
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
</div>
<div :class="$style.status">

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.description">
<div v-if="user.description" :class="$style.mfm">
<Mfm :text="user.description" :author="user" :i="$i"/>
<Mfm :text="user.description" :author="user"/>
</div>
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
</div>

View file

@ -19,9 +19,9 @@ interface Props {
const contentSymbol = Symbol();
const observer = new ResizeObserver((entries) => {
const results: {
container: HTMLSpanElement;
transform: string;
}[] = [];
container: HTMLSpanElement;
transform: string;
}[] = [];
for (const entry of entries) {
const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement;
const props: Required<Props> = content[contentSymbol];

View file

@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
decoding="async"
@error="errored = true"
@load="errored = false"
@click="onClick"
@mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''"
@mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"
@touchstart="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''"
@ -22,10 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted } from 'vue';
import { computed, onMounted, onUnmounted, inject } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
name: string;
@ -34,8 +38,12 @@ const props = defineProps<{
host?: string | null;
url?: string;
useOriginalSize?: boolean;
menu?: boolean;
menuReaction?: boolean;
}>();
const react = inject<((name: string) => void) | null>('react', null);
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
@ -72,6 +80,28 @@ const url = computed(() => {
const alt = computed(() => `:${customEmojiName.value}:`);
let errored = $ref(url.value == null);
function onClick(ev: MouseEvent) {
if (props.menu) {
os.popupMenu([{
type: 'label',
text: `:${props.name}:`,
}, {
text: i18n.ts.copy,
icon: 'ti ti-copy',
action: () => {
copyToClipboard(`:${props.name}:`);
os.success();
},
}, ...(props.menuReaction && react ? [{
text: i18n.ts.doReaction,
icon: 'ti ti-plus',
action: () => {
react(`:${props.name}:`);
},
}] : [])], ev.currentTarget ?? ev.target);
}
}
function resetTimer() {
playAnimation = true;
clearTimeout(playAnimationTimer);

View file

@ -4,21 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<img v-if="!useOsNativeEmojis" :class="[$style.root, { [$style.large]: defaultStore.state.largeNoteReactions }]" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle"/>
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle">{{ props.emoji }}</span>
<img v-if="!useOsNativeEmojis" :class="[$style.root, { [$style.large]: defaultStore.state.largeNoteReactions }]" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span>
<span v-else>{{ emoji }}</span>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, inject } from 'vue';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
import { defaultStore } from '@/store.js';
import { getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
emoji: string;
menu?: boolean;
menuReaction?: boolean;
}>();
const react = inject<((name: string) => void) | null>('react', null);
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
@ -31,6 +38,28 @@ function computeTitle(event: PointerEvent): void {
const title = getEmojiName(props.emoji as string) ?? props.emoji as string;
(event.target as HTMLElement).title = title;
}
function onClick(ev: MouseEvent) {
if (props.menu) {
os.popupMenu([{
type: 'label',
text: props.emoji,
}, {
text: i18n.ts.copy,
icon: 'ti ti-copy',
action: () => {
copyToClipboard(props.emoji);
os.success();
},
}, ...(props.menuReaction && react ? [{
text: i18n.ts.doReaction,
icon: 'ti ti-plus',
action: () => {
react(props.emoji);
},
}] : [])], ev.currentTarget ?? ev.target);
}
}
</script>
<style lang="scss" module>

View file

@ -34,12 +34,13 @@ type MfmProps = {
plain?: boolean;
nowrap?: boolean;
author?: Misskey.entities.UserLite;
i?: Misskey.entities.UserLite | null;
isNote?: boolean;
emojiUrls?: string[];
rootScale?: number;
nyaize: boolean | 'account';
parsedNodes?: mfm.MfmNode[] | null;
enableEmojiMenu?: boolean;
enableEmojiMenuReaction?: boolean;
};
// eslint-disable-next-line import/no-default-export
@ -353,6 +354,8 @@ export default function(props: MfmProps) {
normal: props.plain,
host: null,
useOriginalSize: scale >= 2.5,
menu: props.enableEmojiMenu,
menuReaction: props.enableEmojiMenuReaction,
})];
} else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -376,6 +379,8 @@ export default function(props: MfmProps) {
return [h(MkEmoji, {
key: Math.random(),
emoji: token.props.emoji,
menu: props.enableEmojiMenu,
menuReaction: props.enableEmojiMenuReaction,
})];
}

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<Mfm :text="block.text" :isNote="false" :i="$i"/>
<Mfm :text="block.text" :isNote="false"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
</div>
</template>

View file

@ -116,18 +116,82 @@ SPDX-License-Identifier: AGPL-3.0-only
</a>
<a href="https://github.com/kakkokari-gtyih" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@kakkokari-gtyih</span>
<span :class="$style.contributorUsername">@kakkokari-gtyih
<span :class="$style.contributorClient">
<span :class="$style.misskey">Misskey</span>
</span>
</span>
</a>
<a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@taichanNE30</span>
<span :class="$style.contributorUsername">@taichanNE30
<span :class="$style.contributorClient">
<span :class="$style.misskey">Misskey</span>
</span>
</span>
</a>
<a href="https://github.com/noridev" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/11006910?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@noridev
<span :class="$style.contributorClient">
<span :class="$style.cherry">Cherry</span><span :class="$style.pick">Pick</span>
</span>
</span>
</a>
</div>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
<div :class="$style.contributors" style="margin-bottom: 8px;">
<a href="https://github.com/mei23" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/30769358?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@mei23
<span :class="$style.contributorClient">
<span :class="$style.misskey">Misskey</span>
</span>
</span>
</a>
<a href="https://github.com/rinsuki" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/6533808?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@rinsuki
<span :class="$style.contributorClient">
<span :class="$style.misskey">Misskey</span>
</span>
</span>
</a>
<a href="https://github.com/robflop" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/8159402?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@robflop
<span :class="$style.contributorClient">
<span :class="$style.misskey">Misskey</span>
</span>
</span>
</a>
<a href="https://github.com/caipira113" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/54402969?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@caipira113
<span :class="$style.contributorClient">
<span :class="$style.cherry">Cherry</span><span :class="$style.pick">Pick</span>
</span>
</span>
</a>
</div>
<MkLink url="https://github.com/kokonect-link/cherrypick/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink>
</FormSection>
<FormSection>
<template #label>Special thanks</template>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));grid-gap:24px;align-items:center;">
<div>
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
</div>
<div>
<a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/xserver.png" alt="XServer"></a>
</div>
<div>
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
</div>
</div>
</FormSection>
<FormSection>
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
<p style="font-weight: bold">CherryPick</p>
@ -152,23 +216,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<p>{{ i18n.ts._aboutMisskey.morePatrons }}</p>
</FormSection>
<FormSection>
<template #label>Special thanks</template>
<div class="_gaps" style="text-align: center;">
<div>
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
</div>
<div>
<a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/xserver.png" alt="XServer"></a>
</div>
<div>
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
</div>
<div>
<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="100" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
</div>
</div>
</FormSection>
</div>
</MkSpacer>
</div>

View file

@ -48,6 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
{{ i18n.ts._announcement.forExistingUsers }}
</MkSwitch>
<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
{{ i18n.ts._announcement.silence }}
</MkSwitch>
<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
{{ i18n.ts._announcement.needConfirmationToRead }}
</MkSwitch>
@ -97,6 +100,7 @@ function add() {
icon: 'info',
display: 'normal',
forExistingUsers: false,
silence: false,
needConfirmationToRead: false,
});
}

View file

@ -132,7 +132,7 @@ const menuDef = $computed(() => [{
}, {
icon: 'ti ti-sparkles',
text: i18n.ts.avatarDecorations,
to: '/avatar-decorations',
to: '/admin/avatar-decorations',
active: currentPage?.route.name === 'avatarDecorations',
}, {
icon: 'ti ti-world',

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
<div :class="$style.header">
<span v-if="$i && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
<span style="margin-right: 0.5em;">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
</div>
</div>
<div v-if="tab !== 'past' && $i && !announcement.isRead" :class="$style.footer">
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
</div>
</section>

View file

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.bannerFade"></div>
</div>
<div v-if="channel.description" :class="$style.description">
<Mfm :text="channel.description" :isNote="false" :i="$i"/>
<Mfm :text="channel.description" :isNote="false"/>
</div>
</div>

View file

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="clip" class="_gaps">
<div class="_panel">
<div v-if="clip.description" :class="$style.description">
<Mfm :text="clip.description" :isNote="false" :i="$i"/>
<Mfm :text="clip.description" :isNote="false"/>
</div>
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<img src="/client-assets/remove.png" alt="Delete"/>
</button>
<div v-if="!message.isDeleted" class="content">
<Mfm v-if="message.text" ref="text" class="text" :text="message.text" :i="$i"/>
<Mfm v-if="message.text" ref="text" class="text" :text="message.text"/>
<div v-if="message.file" class="file">
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>

View file

@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
<MkFolder>
<template #label>{{ i18n.ts.pinnedList }}</template>
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
@ -61,7 +60,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
<MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>
<MkRadios v-model="reactionsDisplaySize">
<template #label>{{ i18n.ts.reactionsDisplaySize }}</template>
<option value="small">{{ i18n.ts.small }}</option>
@ -245,6 +243,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<div class="_gaps_s">
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
<MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
@ -368,7 +367,6 @@ const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('
const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
const showUnreadNotificationsCount = computed(defaultStore.makeGetterSetter('showUnreadNotificationsCount'));

View file

@ -76,6 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<div class="_gaps_s">
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
<MkButton danger @click="updateRepliesAll(true)"><i class="ti ti-messages"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
<MkButton danger @click="updateRepliesAll(false)"><i class="ti ti-messages-off"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
</div>
@ -103,6 +104,7 @@ import { globalEvents } from '@/events.js';
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
function onChangeInjectFeaturedNote(v) {
os.api('i/update', {

View file

@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]"
@click="openDecoration(avatarDecoration)"
>
<div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="2 / 3">{{ avatarDecoration.name }}</MkCondensedLine></div>
<div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decoration="{ url: avatarDecoration.url }" forceShowDecoration/>
<i v-if="avatarDecoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => avatarDecoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.avatarDecorationLock" class="ti ti-lock"></i>
</div>

View file

@ -39,32 +39,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { Ref, computed, ref, watch } from 'vue';
import XSound from './sounds.sound.vue';
import * as os from '@/os.js';
import XSound from './sounds.sound.vue';
import MkRange from '@/components/MkRange.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { soundConfigStore } from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { ColdDeviceStorage } from '@/store.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume'));
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
const soundsKeys = ['note', 'noteMy', 'noteEdited', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const;
const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
note: soundConfigStore.reactiveState.sound_note,
noteMy: soundConfigStore.reactiveState.sound_noteMy,
noteEdited: soundConfigStore.reactiveState.sound_noteEdited,
notification: soundConfigStore.reactiveState.sound_notification,
chat: soundConfigStore.reactiveState.sound_chat,
chatBg: soundConfigStore.reactiveState.sound_chatBg,
antenna: soundConfigStore.reactiveState.sound_antenna,
channel: soundConfigStore.reactiveState.sound_channel,
note: defaultStore.reactiveState.sound_note,
noteMy: defaultStore.reactiveState.sound_noteMy,
noteEdited: defaultStore.reactiveState.sound_noteEdited,
notification: defaultStore.reactiveState.sound_notification,
chat: defaultStore.reactiveState.sound_chat,
chatBg: defaultStore.reactiveState.sound_chatBg,
antenna: defaultStore.reactiveState.sound_antenna,
channel: defaultStore.reactiveState.sound_channel,
});
const vibrate = computed(ColdDeviceStorage.makeGetterSetter('vibrate'));
@ -90,14 +89,14 @@ async function updated(type: keyof typeof sounds.value, sound) {
volume: sound.volume,
};
soundConfigStore.set(`sound_${type}`, v);
defaultStore.set(`sound_${type}`, v);
sounds.value[type] = v;
}
function reset() {
for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
const v = soundConfigStore.def[`sound_${sound}`].default;
soundConfigStore.set(`sound_${sound}`, v);
const v = defaultStore.def[`sound_${sound}`].default;
defaultStore.set(`sound_${sound}`, v);
sounds.value[sound] = v;
}
}

View file

@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="description">
<MkOmit>
<Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user" :i="$i"/>
<Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user"/>
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
<div v-if="user.description">
<MkButton v-if="!(translating || translation)" class="translateButton" small @click="translate"><i class="ti ti-language-hiragana"></i> {{ i18n.ts.translateProfile }}</MkButton>
@ -88,7 +88,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/>
<div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}:</b><hr style="margin: 10px 0;">
<Mfm :text="translation.text" :isNote="false" :author="user" :i="$i"/>
<Mfm :text="translation.text" :isNote="false" :author="user"/>
<div v-if="translation.translator == 'ctav3'" style="margin-top: 10px; padding: 0 0 15px;">
<img v-if="!defaultStore.state.darkMode" src="/client-assets/color-short.svg" alt="" style="float: right;">
<img v-else src="/client-assets/white-short.svg" alt="" style="float: right;"/>
@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="field.name" :plain="true" :colored="false"/>
</dt>
<dd class="value">
<Mfm :text="field.value" :author="user" :i="$i" :colored="false"/>
<Mfm :text="field.value" :author="user" :colored="false"/>
<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i>
</dd>
</dl>

View file

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_panel" :class="$style.content">
<div>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/>
<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<div v-if="note.files.length > 0" :class="$style.richcontent">

View file

@ -3,21 +3,29 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
const enRegex1 = /(?<=n)a/gi;
const enRegex2 = /(?<=morn)ing/gi;
const enRegex3 = /(?<=every)one/gi;
const enRegex4 = /non(?=[bcdfghjklmnpqrstvwxyz])/gi;
const koRegex1 = /[나-낳]/g;
const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm;
const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm;
export function nyaize(text: string): string {
return text
// ja-JP
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
.replace(/non(?=[bcdfghjklmnpqrstvwxyz])/gi, x => x === 'NON' ? 'NYAN' : 'nyan')
.replace(enRegex1, x => x === 'A' ? 'YA' : 'ya')
.replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan')
.replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan')
.replace(enRegex4, x => x === 'NON' ? 'NYAN' : 'nyan')
// ko-KR
.replace(/[나-낳]/g, match => String.fromCharCode(
.replace(koRegex1, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥')
.replace(koRegex2, '다냥')
.replace(koRegex3, '냥')
// el-GR
.replaceAll('να', 'νια')
.replaceAll('ΝΑ', 'ΝΙΑ')

View file

@ -3,59 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { markRaw } from 'vue';
import { Storage } from '@/pizzax.js';
export const soundConfigStore = markRaw(new Storage('sound', {
sound_masterVolume: {
where: 'device',
default: 0.3,
},
sound_note: {
where: 'account',
default: { type: 'syuilo/n-aec', volume: 1 },
},
sound_noteMy: {
where: 'account',
default: { type: 'syuilo/n-cea-4va', volume: 1 },
},
sound_noteEdited: {
where: 'account',
default: { type: 'syuilo/n-eca', volume: 1 },
},
sound_notification: {
where: 'account',
default: { type: 'syuilo/n-ea', volume: 1 },
},
sound_chat: {
where: 'account',
default: { type: 'syuilo/pope1', volume: 1 },
},
sound_chatBg: {
where: 'account',
default: { type: 'syuilo/waon', volume: 1 },
},
sound_antenna: {
where: 'account',
default: { type: 'syuilo/triple', volume: 1 },
},
sound_channel: {
where: 'account',
default: { type: 'syuilo/square-pico', volume: 1 },
},
}));
await soundConfigStore.ready;
//#region サウンドのColdDeviceStorage => indexedDBのマイグレーション
for (const target of Object.keys(soundConfigStore.state) as Array<keyof typeof soundConfigStore.state>) {
const value = localStorage.getItem(`miux:${target}`);
if (value) {
soundConfigStore.set(target, JSON.parse(value) as typeof soundConfigStore.def[typeof target]['default']);
localStorage.removeItem(`miux:${target}`);
}
}
//#endregion
import { defaultStore } from '@/store.js';
const cache = new Map<string, HTMLAudioElement>();
@ -124,13 +72,13 @@ export function getAudio(file: string, useCache = true): HTMLAudioElement {
}
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
const masterVolume = soundConfigStore.state.sound_masterVolume;
const masterVolume = defaultStore.state.sound_masterVolume;
audio.volume = masterVolume - ((1 - volume) * masterVolume);
return audio;
}
export function play(type: 'noteMy' | 'note' | 'noteEdited' | 'chat' | 'chatBg' | 'antenna' | 'channel' | 'notification') {
const sound = soundConfigStore.state[`sound_${type}`];
const sound = defaultStore.state[`sound_${type}`];
if (_DEV_) console.log('play', type, sound);
if (sound.type == null) return;
playFile(sound.type, sound.volume);

View file

@ -404,6 +404,43 @@ export const defaultStore = markRaw(new Storage('base', {
default: false,
},
sound_masterVolume: {
where: 'device',
default: 0.3,
},
sound_note: {
where: 'device',
default: { type: 'syuilo/n-aec', volume: 1 },
},
sound_noteMy: {
where: 'device',
default: { type: 'syuilo/n-cea-4va', volume: 1 },
},
sound_noteEdited: {
where: 'device',
default: { type: 'syuilo/n-eca', volume: 1 },
},
sound_notification: {
where: 'device',
default: { type: 'syuilo/n-ea', volume: 1 },
},
sound_chat: {
where: 'device',
default: { type: 'syuilo/pope1', volume: 1 },
},
sound_chatBg: {
where: 'device',
default: { type: 'syuilo/waon', volume: 1 },
},
sound_antenna: {
where: 'device',
default: { type: 'syuilo/triple', volume: 1 },
},
sound_channel: {
where: 'device',
default: { type: 'syuilo/square-pico', volume: 1 },
},
// #region CherryPick
// - Settings/General
newNoteReceivedNotificationBehavior: {

View file

@ -435,7 +435,7 @@ $float-button-size: 65px;
min-width: 0;
overflow: auto;
overflow-y: scroll;
overscroll-behavior: none;
overscroll-behavior: contain;
background: var(--bg);
}

View file

@ -351,7 +351,7 @@ $widgets-hide-threshold: 1090px;
min-width: 0;
overflow: auto;
overflow-y: scroll;
overscroll-behavior: none;
overscroll-behavior: contain;
background: var(--bg);
}