Merge branch 'develop' into enh-instanceticker-alt
This commit is contained in:
commit
e00cc789eb
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -1,14 +1,30 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- Feat: 非ログインでサーバーを閲覧された際に、サーバー内のコンテンツを非公開にすることができるようになりました
|
||||||
|
- モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます
|
||||||
|
- 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます
|
||||||
|
- デフォルト値は「ローカルのコンテンツだけ公開」になっています
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Feat: サーバー初期設定ウィザードが実装されました
|
||||||
|
- 簡単なウィザードに従うだけで、サーバーに最適な設定が適用されます
|
||||||
|
- Feat: Websocket接続を行わずにMisskeyを利用するNo Websocketモードが実装されました(beta)
|
||||||
|
- サーバーのパフォーマンス向上に寄与することが期待されます
|
||||||
|
- 何らの理由によりWebsocket接続が行えない環境でも快適に利用可能です
|
||||||
|
- 従来のWebsocket接続を行うモードはリアルタイムモードとして再定義されました
|
||||||
|
- チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます
|
||||||
|
- Enhance: メモリ使用量を軽減しました
|
||||||
|
- Enhance: リプライ元にアンケートがあることが表示されるように
|
||||||
- Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上
|
- Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上
|
||||||
(Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283)
|
(Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283)
|
||||||
|
- Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
- Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加
|
||||||
|
- Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正
|
||||||
|
- Fix: ユーザ除外アンテナをインポートできない問題を修正
|
||||||
|
- Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正
|
||||||
|
|
||||||
|
|
||||||
## 2025.5.0
|
## 2025.5.0
|
||||||
|
|
|
@ -2,11 +2,6 @@ import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
e2e: {
|
e2e: {
|
||||||
// We've imported your old cypress plugins here.
|
|
||||||
// You may want to clean this up later by importing these.
|
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
return require('./cypress/plugins/index.js')(on, config)
|
|
||||||
},
|
|
||||||
baseUrl: 'http://localhost:61812',
|
baseUrl: 'http://localhost:61812',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,6 +31,15 @@ describe('Before setup instance', () => {
|
||||||
// なぜか動かない
|
// なぜか動かない
|
||||||
//cy.wait('@signup').should('have.property', 'response.statusCode');
|
//cy.wait('@signup').should('have.property', 'response.statusCode');
|
||||||
cy.wait('@signup');
|
cy.wait('@signup');
|
||||||
|
|
||||||
|
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
|
||||||
|
|
||||||
|
cy.get('[data-cy-next]').click();
|
||||||
|
cy.get('[data-cy-next]').click();
|
||||||
|
cy.get('[data-cy-server-name] input').type('Testskey');
|
||||||
|
cy.get('[data-cy-server-setup-wizard-apply]').click();
|
||||||
|
|
||||||
|
cy.wait('@update-meta');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
/// <reference types="cypress" />
|
|
||||||
// ***********************************************************
|
|
||||||
// This example plugins/index.js can be used to load plugins
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off loading
|
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/plugins-guide
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// This function is called when a project is opened or re-opened (e.g. due to
|
|
||||||
// the project's config changing)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
|
||||||
// `config` is the resolved Cypress config
|
|
||||||
}
|
|
|
@ -676,7 +676,6 @@ experimental: "اختباري"
|
||||||
developer: "المطور"
|
developer: "المطور"
|
||||||
makeExplorable: "أظهر الحساب في صفحة \"استكشاف\""
|
makeExplorable: "أظهر الحساب في صفحة \"استكشاف\""
|
||||||
makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\""
|
makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\""
|
||||||
showGapBetweenNotesInTimeline: "أظهر فجوات بين المشاركات في الخيط الزمني"
|
|
||||||
left: "يسار"
|
left: "يسار"
|
||||||
center: "وسط"
|
center: "وسط"
|
||||||
wide: "عريض"
|
wide: "عريض"
|
||||||
|
|
|
@ -673,7 +673,6 @@ experimentalFeatures: "পরীক্ষামূলক বৈশিষ্ট
|
||||||
developer: "ডেভেলপার"
|
developer: "ডেভেলপার"
|
||||||
makeExplorable: "অ্যাকাউন্ট \"ঘুরে দেখুন\" পৃষ্ঠায় দেখান"
|
makeExplorable: "অ্যাকাউন্ট \"ঘুরে দেখুন\" পৃষ্ঠায় দেখান"
|
||||||
makeExplorableDescription: "আপনি এটি বন্ধ করলে, আপনার অ্যাকাউন্ট \"ঘুরে দেখুন\" পৃষ্ঠায় প্রদর্শিত হবে না।"
|
makeExplorableDescription: "আপনি এটি বন্ধ করলে, আপনার অ্যাকাউন্ট \"ঘুরে দেখুন\" পৃষ্ঠায় প্রদর্শিত হবে না।"
|
||||||
showGapBetweenNotesInTimeline: "টাইমলাইন এবং নোটের মাঝে ফাকা জায়গা রাখুন"
|
|
||||||
duplicate: "প্রতিরূপ"
|
duplicate: "প্রতিরূপ"
|
||||||
left: "বাম"
|
left: "বাম"
|
||||||
center: "মাঝখান"
|
center: "মাঝখান"
|
||||||
|
|
|
@ -785,7 +785,6 @@ thisIsExperimentalFeature: "Aquesta és una característica experimental. La sev
|
||||||
developer: "Programador"
|
developer: "Programador"
|
||||||
makeExplorable: "Fes que el compte sigui visible a la secció \"Explorar\""
|
makeExplorable: "Fes que el compte sigui visible a la secció \"Explorar\""
|
||||||
makeExplorableDescription: "Si desactives aquesta opció, el teu compte no sortirà a la secció \"Explorar\""
|
makeExplorableDescription: "Si desactives aquesta opció, el teu compte no sortirà a la secció \"Explorar\""
|
||||||
showGapBetweenNotesInTimeline: "Notes separades a la línia de temps"
|
|
||||||
duplicate: "Duplicat"
|
duplicate: "Duplicat"
|
||||||
left: "Esquerra"
|
left: "Esquerra"
|
||||||
center: "Centre"
|
center: "Centre"
|
||||||
|
@ -1238,7 +1237,6 @@ showAvatarDecorations: "Mostrar les decoracions dels avatars"
|
||||||
releaseToRefresh: "Deixar anar per actualitzar"
|
releaseToRefresh: "Deixar anar per actualitzar"
|
||||||
refreshing: "Recarregant..."
|
refreshing: "Recarregant..."
|
||||||
pullDownToRefresh: "Llisca cap a baix per recarregar"
|
pullDownToRefresh: "Llisca cap a baix per recarregar"
|
||||||
disableStreamingTimeline: "Desactivar l'actualització en temps real de les línies de temps"
|
|
||||||
useGroupedNotifications: "Mostrar les notificacions agrupades "
|
useGroupedNotifications: "Mostrar les notificacions agrupades "
|
||||||
signupPendingError: "Hi ha hagut un problema verificant l'adreça de correu electrònic. L'enllaç pot haver caducat."
|
signupPendingError: "Hi ha hagut un problema verificant l'adreça de correu electrònic. L'enllaç pot haver caducat."
|
||||||
cwNotationRequired: "Si està activat \"Amagar contingut\" s'ha d'escriure una descripció "
|
cwNotationRequired: "Si està activat \"Amagar contingut\" s'ha d'escriure una descripció "
|
||||||
|
@ -1434,6 +1432,7 @@ _preferencesProfile:
|
||||||
profileName: "Nom del perfil"
|
profileName: "Nom del perfil"
|
||||||
profileNameDescription: "Estableix un nom que identifiqui aquest dispositiu."
|
profileNameDescription: "Estableix un nom que identifiqui aquest dispositiu."
|
||||||
profileNameDescription2: "Per exemple: \"PC Principal\", \"Smartphone\", etc"
|
profileNameDescription2: "Per exemple: \"PC Principal\", \"Smartphone\", etc"
|
||||||
|
manageProfiles: "Gestionar perfils"
|
||||||
_preferencesBackup:
|
_preferencesBackup:
|
||||||
autoBackup: "Còpia de seguretat automàtica "
|
autoBackup: "Còpia de seguretat automàtica "
|
||||||
restoreFromBackup: "Restaurar des d'una còpia de seguretat"
|
restoreFromBackup: "Restaurar des d'una còpia de seguretat"
|
||||||
|
|
|
@ -726,7 +726,6 @@ thisIsExperimentalFeature: "Tohle je experimentální funkce. Její funkce se m
|
||||||
developer: "Vývojář"
|
developer: "Vývojář"
|
||||||
makeExplorable: "Udělat účet viditelný v \"Objevit\""
|
makeExplorable: "Udělat účet viditelný v \"Objevit\""
|
||||||
makeExplorableDescription: "Pokud tohle vypnete, tak se účet přestane zobrazovat v sekci \"Objevit\"."
|
makeExplorableDescription: "Pokud tohle vypnete, tak se účet přestane zobrazovat v sekci \"Objevit\"."
|
||||||
showGapBetweenNotesInTimeline: "Zobrazit mezeru mezi příspěvkama na časové ose"
|
|
||||||
duplicate: "Duplikovat"
|
duplicate: "Duplikovat"
|
||||||
left: "Vlevo"
|
left: "Vlevo"
|
||||||
center: "Uprostřed"
|
center: "Uprostřed"
|
||||||
|
|
|
@ -785,7 +785,6 @@ thisIsExperimentalFeature: "Dies ist eine experimentelle Funktion. Änderungen a
|
||||||
developer: "Entwickler"
|
developer: "Entwickler"
|
||||||
makeExplorable: "Benutzerkonto in „Erkunden“ sichtbar machen"
|
makeExplorable: "Benutzerkonto in „Erkunden“ sichtbar machen"
|
||||||
makeExplorableDescription: "Wenn diese Option deaktiviert ist, ist dein Benutzerkonto nicht im „Erkunden“-Bereich sichtbar."
|
makeExplorableDescription: "Wenn diese Option deaktiviert ist, ist dein Benutzerkonto nicht im „Erkunden“-Bereich sichtbar."
|
||||||
showGapBetweenNotesInTimeline: "Abstände zwischen Notizen auf der Chronik anzeigen"
|
|
||||||
duplicate: "Duplizieren"
|
duplicate: "Duplizieren"
|
||||||
left: "Links"
|
left: "Links"
|
||||||
center: "Mittig"
|
center: "Mittig"
|
||||||
|
@ -1238,7 +1237,6 @@ showAvatarDecorations: "Profilbilddekoration anzeigen"
|
||||||
releaseToRefresh: "Zum Aktualisieren loslassen"
|
releaseToRefresh: "Zum Aktualisieren loslassen"
|
||||||
refreshing: "Wird aktualisiert..."
|
refreshing: "Wird aktualisiert..."
|
||||||
pullDownToRefresh: "Zum Aktualisieren ziehen"
|
pullDownToRefresh: "Zum Aktualisieren ziehen"
|
||||||
disableStreamingTimeline: "Echtzeitaktualisierung der Chronik deaktivieren"
|
|
||||||
useGroupedNotifications: "Benachrichtigungen gruppieren"
|
useGroupedNotifications: "Benachrichtigungen gruppieren"
|
||||||
signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen. Der Link könnte abgelaufen sein."
|
signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen. Der Link könnte abgelaufen sein."
|
||||||
cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
|
cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
|
||||||
|
@ -1434,6 +1432,7 @@ _preferencesProfile:
|
||||||
profileName: "Profilname"
|
profileName: "Profilname"
|
||||||
profileNameDescription: "Lege einen Namen fest, der dieses Gerät identifiziert."
|
profileNameDescription: "Lege einen Namen fest, der dieses Gerät identifiziert."
|
||||||
profileNameDescription2: "Beispiel: \"Haupt-PC\", \"Smartphone\""
|
profileNameDescription2: "Beispiel: \"Haupt-PC\", \"Smartphone\""
|
||||||
|
manageProfiles: "Profile verwalten"
|
||||||
_preferencesBackup:
|
_preferencesBackup:
|
||||||
autoBackup: "Automatische Sicherung"
|
autoBackup: "Automatische Sicherung"
|
||||||
restoreFromBackup: "Wiederherstellen aus der Sicherung"
|
restoreFromBackup: "Wiederherstellen aus der Sicherung"
|
||||||
|
|
|
@ -785,7 +785,6 @@ thisIsExperimentalFeature: "This is an experimental feature. Its functionality i
|
||||||
developer: "Developer"
|
developer: "Developer"
|
||||||
makeExplorable: "Make account visible in \"Explore\""
|
makeExplorable: "Make account visible in \"Explore\""
|
||||||
makeExplorableDescription: "If you turn this off, your account will not show up in the \"Explore\" section."
|
makeExplorableDescription: "If you turn this off, your account will not show up in the \"Explore\" section."
|
||||||
showGapBetweenNotesInTimeline: "Show a gap between posts on the timeline"
|
|
||||||
duplicate: "Duplicate"
|
duplicate: "Duplicate"
|
||||||
left: "Left"
|
left: "Left"
|
||||||
center: "Center"
|
center: "Center"
|
||||||
|
@ -1238,7 +1237,6 @@ showAvatarDecorations: "Show avatar decorations"
|
||||||
releaseToRefresh: "Release to refresh"
|
releaseToRefresh: "Release to refresh"
|
||||||
refreshing: "Refreshing..."
|
refreshing: "Refreshing..."
|
||||||
pullDownToRefresh: "Pull down to refresh"
|
pullDownToRefresh: "Pull down to refresh"
|
||||||
disableStreamingTimeline: "Disable real-time timeline updates"
|
|
||||||
useGroupedNotifications: "Display grouped notifications"
|
useGroupedNotifications: "Display grouped notifications"
|
||||||
signupPendingError: "There was a problem verifying the email address. The link may have expired."
|
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."
|
cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided."
|
||||||
|
@ -1434,6 +1432,7 @@ _preferencesProfile:
|
||||||
profileName: "Profile name"
|
profileName: "Profile name"
|
||||||
profileNameDescription: "Set a name that identifies this device."
|
profileNameDescription: "Set a name that identifies this device."
|
||||||
profileNameDescription2: "Example: \"Main PC\", \"Smartphone\""
|
profileNameDescription2: "Example: \"Main PC\", \"Smartphone\""
|
||||||
|
manageProfiles: "Manage Profiles"
|
||||||
_preferencesBackup:
|
_preferencesBackup:
|
||||||
autoBackup: "Auto backup"
|
autoBackup: "Auto backup"
|
||||||
restoreFromBackup: "Restore from backup"
|
restoreFromBackup: "Restore from backup"
|
||||||
|
|
|
@ -784,7 +784,6 @@ thisIsExperimentalFeature: "Se trata de una función experimental. Las especific
|
||||||
developer: "Desarrolladores"
|
developer: "Desarrolladores"
|
||||||
makeExplorable: "Hacer visible la cuenta en \"Explorar\""
|
makeExplorable: "Hacer visible la cuenta en \"Explorar\""
|
||||||
makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la sección \"Explorar\"."
|
makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la sección \"Explorar\"."
|
||||||
showGapBetweenNotesInTimeline: "Mostrar un intervalo entre notas en la línea de tiempo"
|
|
||||||
duplicate: "Duplicar"
|
duplicate: "Duplicar"
|
||||||
left: "Izquierda"
|
left: "Izquierda"
|
||||||
center: "Centrar"
|
center: "Centrar"
|
||||||
|
@ -1237,7 +1236,6 @@ showAvatarDecorations: "Mostrar decoraciones de avatar"
|
||||||
releaseToRefresh: "Soltar para recargar"
|
releaseToRefresh: "Soltar para recargar"
|
||||||
refreshing: "Recargando..."
|
refreshing: "Recargando..."
|
||||||
pullDownToRefresh: "Tira hacia abajo para recargar"
|
pullDownToRefresh: "Tira hacia abajo para recargar"
|
||||||
disableStreamingTimeline: "Desactivar actualizaciones en tiempo real de la línea de tiempo"
|
|
||||||
useGroupedNotifications: "Mostrar notificaciones agrupadas"
|
useGroupedNotifications: "Mostrar notificaciones agrupadas"
|
||||||
signupPendingError: "Ha habido un problema al verificar tu dirección de correo electrónico. Es posible que el enlace haya caducado."
|
signupPendingError: "Ha habido un problema al verificar tu dirección de correo electrónico. Es posible que el enlace haya caducado."
|
||||||
cwNotationRequired: "Si se ha activado \"ocultar contenido\", es necesario proporcionar una descripción."
|
cwNotationRequired: "Si se ha activado \"ocultar contenido\", es necesario proporcionar una descripción."
|
||||||
|
|
|
@ -760,7 +760,6 @@ thisIsExperimentalFeature: "Ceci est une fonctionnalité expérimentale. Il y a
|
||||||
developer: "Développeur"
|
developer: "Développeur"
|
||||||
makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"."
|
makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"."
|
||||||
makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"."
|
makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"."
|
||||||
showGapBetweenNotesInTimeline: "Afficher un écart entre les notes sur la Timeline"
|
|
||||||
duplicate: "Duliquer"
|
duplicate: "Duliquer"
|
||||||
left: "Gauche"
|
left: "Gauche"
|
||||||
center: "Centrer"
|
center: "Centrer"
|
||||||
|
@ -1209,7 +1208,6 @@ showAvatarDecorations: "Afficher les décorations d'avatar"
|
||||||
releaseToRefresh: "Relâcher pour rafraîchir"
|
releaseToRefresh: "Relâcher pour rafraîchir"
|
||||||
refreshing: "Rafraîchissement..."
|
refreshing: "Rafraîchissement..."
|
||||||
pullDownToRefresh: "Tirer vers le bas pour rafraîchir"
|
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"
|
useGroupedNotifications: "Grouper les notifications"
|
||||||
signupPendingError: "Un problème est survenu lors de la vérification de votre adresse e-mail. Le lien a peut-être expiré."
|
signupPendingError: "Un problème est survenu lors de la vérification de votre adresse e-mail. Le lien a peut-être expiré."
|
||||||
cwNotationRequired: "Si « Masquer le contenu » est activé, une description doit être fournie."
|
cwNotationRequired: "Si « Masquer le contenu » est activé, une description doit être fournie."
|
||||||
|
|
|
@ -761,7 +761,6 @@ thisIsExperimentalFeature: "Fitur ini eksperimental. Fungsionalitas dari fitur i
|
||||||
developer: "Pengembang"
|
developer: "Pengembang"
|
||||||
makeExplorable: "Buat akun tampil di \"Jelajahi\""
|
makeExplorable: "Buat akun tampil di \"Jelajahi\""
|
||||||
makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di menu \"Jelajahi\""
|
makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di menu \"Jelajahi\""
|
||||||
showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada lini masa"
|
|
||||||
duplicate: "Duplikat"
|
duplicate: "Duplikat"
|
||||||
left: "Kiri"
|
left: "Kiri"
|
||||||
center: "Tengah"
|
center: "Tengah"
|
||||||
|
@ -1206,7 +1205,6 @@ showAvatarDecorations: "Tampilkan dekorasi avatar"
|
||||||
releaseToRefresh: "Lepaskan untuk memuat ulang"
|
releaseToRefresh: "Lepaskan untuk memuat ulang"
|
||||||
refreshing: "Sedang memuat ulang..."
|
refreshing: "Sedang memuat ulang..."
|
||||||
pullDownToRefresh: "Tarik ke bawah untuk memuat ulang"
|
pullDownToRefresh: "Tarik ke bawah untuk memuat ulang"
|
||||||
disableStreamingTimeline: "Nonaktifkan pembaharuan lini masa real-time"
|
|
||||||
useGroupedNotifications: "Tampilkan notifikasi secara dikelompokkan"
|
useGroupedNotifications: "Tampilkan notifikasi secara dikelompokkan"
|
||||||
signupPendingError: "Terdapat masalah ketika memverifikasi alamat surel. Tautan kemungkinan telah kedaluwarsa."
|
signupPendingError: "Terdapat masalah ketika memverifikasi alamat surel. Tautan kemungkinan telah kedaluwarsa."
|
||||||
cwNotationRequired: "Jika \"Sembunyikan konten\" diaktifkan, deskripsi harus disediakan."
|
cwNotationRequired: "Jika \"Sembunyikan konten\" diaktifkan, deskripsi harus disediakan."
|
||||||
|
|
|
@ -1022,10 +1022,6 @@ export interface Locale extends ILocale {
|
||||||
* これ以上ピン留めできません
|
* これ以上ピン留めできません
|
||||||
*/
|
*/
|
||||||
"pinLimitExceeded": string;
|
"pinLimitExceeded": string;
|
||||||
/**
|
|
||||||
* Misskeyのインストールが完了しました!管理者アカウントを作成しましょう。
|
|
||||||
*/
|
|
||||||
"intro": string;
|
|
||||||
/**
|
/**
|
||||||
* 完了
|
* 完了
|
||||||
*/
|
*/
|
||||||
|
@ -2322,6 +2318,10 @@ export interface Locale extends ILocale {
|
||||||
* 新しいノートがあります
|
* 新しいノートがあります
|
||||||
*/
|
*/
|
||||||
"newNoteRecived": string;
|
"newNoteRecived": string;
|
||||||
|
/**
|
||||||
|
* 新しいノート
|
||||||
|
*/
|
||||||
|
"newNote": string;
|
||||||
/**
|
/**
|
||||||
* サウンド
|
* サウンド
|
||||||
*/
|
*/
|
||||||
|
@ -3158,10 +3158,6 @@ export interface Locale extends ILocale {
|
||||||
* オフにすると、「みつける」にアカウントが載らなくなります。
|
* オフにすると、「みつける」にアカウントが載らなくなります。
|
||||||
*/
|
*/
|
||||||
"makeExplorableDescription": string;
|
"makeExplorableDescription": string;
|
||||||
/**
|
|
||||||
* タイムラインのノートを離して表示
|
|
||||||
*/
|
|
||||||
"showGapBetweenNotesInTimeline": string;
|
|
||||||
/**
|
/**
|
||||||
* 複製
|
* 複製
|
||||||
*/
|
*/
|
||||||
|
@ -4970,10 +4966,6 @@ export interface Locale extends ILocale {
|
||||||
* 引っ張ってリロード
|
* 引っ張ってリロード
|
||||||
*/
|
*/
|
||||||
"pullDownToRefresh": string;
|
"pullDownToRefresh": string;
|
||||||
/**
|
|
||||||
* タイムラインのリアルタイム更新を無効にする
|
|
||||||
*/
|
|
||||||
"disableStreamingTimeline": string;
|
|
||||||
/**
|
/**
|
||||||
* 通知をグルーピング
|
* 通知をグルーピング
|
||||||
*/
|
*/
|
||||||
|
@ -5417,6 +5409,22 @@ export interface Locale extends ILocale {
|
||||||
* スクロールして閉じる
|
* スクロールして閉じる
|
||||||
*/
|
*/
|
||||||
"scrollToClose": string;
|
"scrollToClose": string;
|
||||||
|
/**
|
||||||
|
* アドバイス
|
||||||
|
*/
|
||||||
|
"advice": string;
|
||||||
|
/**
|
||||||
|
* リアルタイムモード
|
||||||
|
*/
|
||||||
|
"realtimeMode": string;
|
||||||
|
/**
|
||||||
|
* オンにする
|
||||||
|
*/
|
||||||
|
"turnItOn": string;
|
||||||
|
/**
|
||||||
|
* オフにする
|
||||||
|
*/
|
||||||
|
"turnItOff": string;
|
||||||
"_chat": {
|
"_chat": {
|
||||||
/**
|
/**
|
||||||
* まだメッセージはありません
|
* まだメッセージはありません
|
||||||
|
@ -5721,6 +5729,22 @@ export interface Locale extends ILocale {
|
||||||
* マウスでは、ホイールを押し込みながらドラッグします。
|
* マウスでは、ホイールを押し込みながらドラッグします。
|
||||||
*/
|
*/
|
||||||
"enablePullToRefresh_description": string;
|
"enablePullToRefresh_description": string;
|
||||||
|
/**
|
||||||
|
* サーバーと接続を確立し、リアルタイムでコンテンツを更新します。通信量とバッテリーの消費が多くなる場合があります。
|
||||||
|
*/
|
||||||
|
"realtimeMode_description": string;
|
||||||
|
/**
|
||||||
|
* コンテンツの取得頻度
|
||||||
|
*/
|
||||||
|
"contentsUpdateFrequency": string;
|
||||||
|
/**
|
||||||
|
* 高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。
|
||||||
|
*/
|
||||||
|
"contentsUpdateFrequency_description": string;
|
||||||
|
/**
|
||||||
|
* リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。
|
||||||
|
*/
|
||||||
|
"contentsUpdateFrequency_description2": string;
|
||||||
"_chat": {
|
"_chat": {
|
||||||
/**
|
/**
|
||||||
* 送信者の名前を表示
|
* 送信者の名前を表示
|
||||||
|
@ -6388,6 +6412,40 @@ export interface Locale extends ILocale {
|
||||||
* 脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。
|
* 脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。
|
||||||
*/
|
*/
|
||||||
"deliverSuspendedSoftwareDescription": string;
|
"deliverSuspendedSoftwareDescription": string;
|
||||||
|
/**
|
||||||
|
* お一人様モード
|
||||||
|
*/
|
||||||
|
"singleUserMode": string;
|
||||||
|
/**
|
||||||
|
* このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。
|
||||||
|
*/
|
||||||
|
"singleUserMode_description": string;
|
||||||
|
/**
|
||||||
|
* 非利用者に対するユーザー作成コンテンツの公開範囲
|
||||||
|
*/
|
||||||
|
"userGeneratedContentsVisibilityForVisitor": string;
|
||||||
|
/**
|
||||||
|
* モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。
|
||||||
|
*/
|
||||||
|
"userGeneratedContentsVisibilityForVisitor_description": string;
|
||||||
|
/**
|
||||||
|
* サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。
|
||||||
|
*/
|
||||||
|
"userGeneratedContentsVisibilityForVisitor_description2": string;
|
||||||
|
"_userGeneratedContentsVisibilityForVisitor": {
|
||||||
|
/**
|
||||||
|
* 全て公開
|
||||||
|
*/
|
||||||
|
"all": string;
|
||||||
|
/**
|
||||||
|
* ローカルコンテンツのみ公開し、リモートコンテンツは非公開
|
||||||
|
*/
|
||||||
|
"localOnly": string;
|
||||||
|
/**
|
||||||
|
* 全て非公開
|
||||||
|
*/
|
||||||
|
"none": string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
"_accountMigration": {
|
"_accountMigration": {
|
||||||
/**
|
/**
|
||||||
|
@ -11620,6 +11678,166 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"serverHostPlaceholder": string;
|
"serverHostPlaceholder": string;
|
||||||
};
|
};
|
||||||
|
"_serverSetupWizard": {
|
||||||
|
/**
|
||||||
|
* Misskeyのインストールが完了しました!
|
||||||
|
*/
|
||||||
|
"installCompleted": string;
|
||||||
|
/**
|
||||||
|
* まずは、管理者アカウントを作成しましょう。
|
||||||
|
*/
|
||||||
|
"firstCreateAccount": string;
|
||||||
|
/**
|
||||||
|
* 管理者アカウントが作成されました!
|
||||||
|
*/
|
||||||
|
"accountCreated": string;
|
||||||
|
/**
|
||||||
|
* サーバーの設定
|
||||||
|
*/
|
||||||
|
"serverSetting": string;
|
||||||
|
/**
|
||||||
|
* このウィザードで簡単に最適なサーバーの設定が行えます。
|
||||||
|
*/
|
||||||
|
"youCanEasilyConfigureOptimalServerSettingsWithThisWizard": string;
|
||||||
|
/**
|
||||||
|
* ここでの設定は、あとからでも変更できます。
|
||||||
|
*/
|
||||||
|
"settingsYouMakeHereCanBeChangedLater": string;
|
||||||
|
/**
|
||||||
|
* Misskeyをどのように使いますか?
|
||||||
|
*/
|
||||||
|
"howWillYouUseMisskey": string;
|
||||||
|
"_use": {
|
||||||
|
/**
|
||||||
|
* お一人様サーバー
|
||||||
|
*/
|
||||||
|
"single": string;
|
||||||
|
/**
|
||||||
|
* 自分専用のサーバーとして、一人で使う
|
||||||
|
*/
|
||||||
|
"single_description": string;
|
||||||
|
/**
|
||||||
|
* お一人様サーバーとして運用する場合でも、アカウントは必要に応じて複数作成可能です。
|
||||||
|
*/
|
||||||
|
"single_youCanCreateMultipleAccounts": string;
|
||||||
|
/**
|
||||||
|
* グループサーバー
|
||||||
|
*/
|
||||||
|
"group": string;
|
||||||
|
/**
|
||||||
|
* 信頼できる他の利用者を招待して、複数人で使う
|
||||||
|
*/
|
||||||
|
"group_description": string;
|
||||||
|
/**
|
||||||
|
* オープンサーバー
|
||||||
|
*/
|
||||||
|
"open": string;
|
||||||
|
/**
|
||||||
|
* 不特定多数の利用者を受け入れる運営を行う
|
||||||
|
*/
|
||||||
|
"open_description": string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 不特定多数の利用者を受け入れることはリスクが伴います。トラブルに対処できるよう、確実なモデレーション体制で運営することを推奨します。
|
||||||
|
*/
|
||||||
|
"openServerAdvice": string;
|
||||||
|
/**
|
||||||
|
* 自サーバーがスパムの踏み台にならないように、reCAPTCHAといったアンチボット機能を有効にするなど、セキュリティについても細心の注意が必要です。
|
||||||
|
*/
|
||||||
|
"openServerAntiSpamAdvice": string;
|
||||||
|
/**
|
||||||
|
* どれくらいの人数を想定していますか?
|
||||||
|
*/
|
||||||
|
"howManyUsersDoYouExpect": string;
|
||||||
|
"_scale": {
|
||||||
|
/**
|
||||||
|
* 100人以下 (小規模)
|
||||||
|
*/
|
||||||
|
"small": string;
|
||||||
|
/**
|
||||||
|
* 100人以上1000人以下 (中規模)
|
||||||
|
*/
|
||||||
|
"medium": string;
|
||||||
|
/**
|
||||||
|
* 1000人以上 (大規模)
|
||||||
|
*/
|
||||||
|
"large": string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 大規模なサーバーでは、ロードバランシングやデータベースのレプリケーションなど、高度なインフラストラクチャーの知識が必要になる場合があります。
|
||||||
|
*/
|
||||||
|
"largeScaleServerAdvice": string;
|
||||||
|
/**
|
||||||
|
* Fediverseと接続しますか?
|
||||||
|
*/
|
||||||
|
"doYouConnectToFediverse": string;
|
||||||
|
/**
|
||||||
|
* 分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。
|
||||||
|
*/
|
||||||
|
"doYouConnectToFediverse_description1": string;
|
||||||
|
/**
|
||||||
|
* Fediverseと接続することは「連合」とも呼ばれます。
|
||||||
|
*/
|
||||||
|
"doYouConnectToFediverse_description2": string;
|
||||||
|
/**
|
||||||
|
* 連合可能なサーバーの指定など、高度な設定も後ほど可能です。
|
||||||
|
*/
|
||||||
|
"youCanConfigureMoreFederationSettingsLater": string;
|
||||||
|
/**
|
||||||
|
* 管理者情報
|
||||||
|
*/
|
||||||
|
"adminInfo": string;
|
||||||
|
/**
|
||||||
|
* 問い合わせを受け付けるために使用される管理者情報を設定します。
|
||||||
|
*/
|
||||||
|
"adminInfo_description": string;
|
||||||
|
/**
|
||||||
|
* オープンサーバー、または連合がオンの場合は必ず入力が必要です。
|
||||||
|
*/
|
||||||
|
"adminInfo_mustBeFilled": string;
|
||||||
|
/**
|
||||||
|
* 以下の設定が推奨されます
|
||||||
|
*/
|
||||||
|
"followingSettingsAreRecommended": string;
|
||||||
|
/**
|
||||||
|
* この設定を適用
|
||||||
|
*/
|
||||||
|
"applyTheseSettings": string;
|
||||||
|
/**
|
||||||
|
* 設定をスキップ
|
||||||
|
*/
|
||||||
|
"skipSettings": string;
|
||||||
|
/**
|
||||||
|
* 設定が完了しました!
|
||||||
|
*/
|
||||||
|
"settingsCompleted": string;
|
||||||
|
/**
|
||||||
|
* お疲れ様でした。準備が整ったので、さっそくサーバーの使用を開始できます。
|
||||||
|
*/
|
||||||
|
"settingsCompleted_description": string;
|
||||||
|
/**
|
||||||
|
* 詳細なサーバー設定は、「コントロールパネル」から行えます。
|
||||||
|
*/
|
||||||
|
"settingsCompleted_description2": string;
|
||||||
|
/**
|
||||||
|
* 寄付のお願い
|
||||||
|
*/
|
||||||
|
"donationRequest": string;
|
||||||
|
"_donationRequest": {
|
||||||
|
/**
|
||||||
|
* Misskeyは有志によって開発されている無料のソフトウェアです。
|
||||||
|
*/
|
||||||
|
"text1": string;
|
||||||
|
/**
|
||||||
|
* 今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。
|
||||||
|
*/
|
||||||
|
"text2": string;
|
||||||
|
/**
|
||||||
|
* 支援者向け特典もあります!
|
||||||
|
*/
|
||||||
|
"text3": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -784,7 +784,6 @@ thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe e
|
||||||
developer: "Sviluppatore"
|
developer: "Sviluppatore"
|
||||||
makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\""
|
makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\""
|
||||||
makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"."
|
makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"."
|
||||||
showGapBetweenNotesInTimeline: "Mostrare un intervallo tra le note sulla timeline"
|
|
||||||
duplicate: "Duplica"
|
duplicate: "Duplica"
|
||||||
left: "Sinistra"
|
left: "Sinistra"
|
||||||
center: "Centro"
|
center: "Centro"
|
||||||
|
@ -1237,7 +1236,6 @@ showAvatarDecorations: "Mostra decorazione della foto profilo"
|
||||||
releaseToRefresh: "Rilascia per aggiornare"
|
releaseToRefresh: "Rilascia per aggiornare"
|
||||||
refreshing: "Aggiornamento..."
|
refreshing: "Aggiornamento..."
|
||||||
pullDownToRefresh: "Trascinare per aggiornare"
|
pullDownToRefresh: "Trascinare per aggiornare"
|
||||||
disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo reale"
|
|
||||||
useGroupedNotifications: "Mostra le notifiche raggruppate"
|
useGroupedNotifications: "Mostra le notifiche raggruppate"
|
||||||
signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo."
|
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."
|
cwNotationRequired: "Devi indicare perché il contenuto è indicato come esplicito."
|
||||||
|
|
|
@ -251,7 +251,6 @@ noUsers: "ユーザーはいません"
|
||||||
editProfile: "プロフィールを編集"
|
editProfile: "プロフィールを編集"
|
||||||
noteDeleteConfirm: "このノートを削除しますか?"
|
noteDeleteConfirm: "このノートを削除しますか?"
|
||||||
pinLimitExceeded: "これ以上ピン留めできません"
|
pinLimitExceeded: "これ以上ピン留めできません"
|
||||||
intro: "Misskeyのインストールが完了しました!管理者アカウントを作成しましょう。"
|
|
||||||
done: "完了"
|
done: "完了"
|
||||||
processing: "処理中"
|
processing: "処理中"
|
||||||
preview: "プレビュー"
|
preview: "プレビュー"
|
||||||
|
@ -576,6 +575,7 @@ showFixedPostForm: "タイムライン上部に投稿フォームを表示する
|
||||||
showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)"
|
showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)"
|
||||||
withRepliesByDefaultForNewlyFollowed: "フォローする際、デフォルトで返信をTLに含むようにする"
|
withRepliesByDefaultForNewlyFollowed: "フォローする際、デフォルトで返信をTLに含むようにする"
|
||||||
newNoteRecived: "新しいノートがあります"
|
newNoteRecived: "新しいノートがあります"
|
||||||
|
newNote: "新しいノート"
|
||||||
sounds: "サウンド"
|
sounds: "サウンド"
|
||||||
sound: "サウンド"
|
sound: "サウンド"
|
||||||
listen: "聴く"
|
listen: "聴く"
|
||||||
|
@ -785,7 +785,6 @@ thisIsExperimentalFeature: "これは実験的な機能です。仕様が変更
|
||||||
developer: "開発者"
|
developer: "開発者"
|
||||||
makeExplorable: "アカウントを見つけやすくする"
|
makeExplorable: "アカウントを見つけやすくする"
|
||||||
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。"
|
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。"
|
||||||
showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示"
|
|
||||||
duplicate: "複製"
|
duplicate: "複製"
|
||||||
left: "左"
|
left: "左"
|
||||||
center: "中央"
|
center: "中央"
|
||||||
|
@ -1238,7 +1237,6 @@ showAvatarDecorations: "アイコンのデコレーションを表示"
|
||||||
releaseToRefresh: "離してリロード"
|
releaseToRefresh: "離してリロード"
|
||||||
refreshing: "リロード中"
|
refreshing: "リロード中"
|
||||||
pullDownToRefresh: "引っ張ってリロード"
|
pullDownToRefresh: "引っ張ってリロード"
|
||||||
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
|
|
||||||
useGroupedNotifications: "通知をグルーピング"
|
useGroupedNotifications: "通知をグルーピング"
|
||||||
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
|
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
|
||||||
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
|
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
|
||||||
|
@ -1349,6 +1347,10 @@ goToDeck: "デッキへ戻る"
|
||||||
federationJobs: "連合ジョブ"
|
federationJobs: "連合ジョブ"
|
||||||
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。"
|
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。"
|
||||||
scrollToClose: "スクロールして閉じる"
|
scrollToClose: "スクロールして閉じる"
|
||||||
|
advice: "アドバイス"
|
||||||
|
realtimeMode: "リアルタイムモード"
|
||||||
|
turnItOn: "オンにする"
|
||||||
|
turnItOff: "オフにする"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "まだメッセージはありません"
|
noMessagesYet: "まだメッセージはありません"
|
||||||
|
@ -1430,6 +1432,10 @@ _settings:
|
||||||
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
|
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
|
||||||
enablePullToRefresh: "ひっぱって更新"
|
enablePullToRefresh: "ひっぱって更新"
|
||||||
enablePullToRefresh_description: "マウスでは、ホイールを押し込みながらドラッグします。"
|
enablePullToRefresh_description: "マウスでは、ホイールを押し込みながらドラッグします。"
|
||||||
|
realtimeMode_description: "サーバーと接続を確立し、リアルタイムでコンテンツを更新します。通信量とバッテリーの消費が多くなる場合があります。"
|
||||||
|
contentsUpdateFrequency: "コンテンツの取得頻度"
|
||||||
|
contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。"
|
||||||
|
contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "送信者の名前を表示"
|
showSenderName: "送信者の名前を表示"
|
||||||
|
@ -1623,6 +1629,16 @@ _serverSettings:
|
||||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
|
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
|
||||||
deliverSuspendedSoftware: "配信停止中のソフトウェア"
|
deliverSuspendedSoftware: "配信停止中のソフトウェア"
|
||||||
deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。"
|
deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。"
|
||||||
|
singleUserMode: "お一人様モード"
|
||||||
|
singleUserMode_description: "このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。"
|
||||||
|
userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲"
|
||||||
|
userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。"
|
||||||
|
userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。"
|
||||||
|
|
||||||
|
_userGeneratedContentsVisibilityForVisitor:
|
||||||
|
all: "全て公開"
|
||||||
|
localOnly: "ローカルコンテンツのみ公開し、リモートコンテンツは非公開"
|
||||||
|
none: "全て非公開"
|
||||||
|
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||||
|
@ -3107,3 +3123,46 @@ _search:
|
||||||
pleaseEnterServerHost: "サーバーのホストを入力してください"
|
pleaseEnterServerHost: "サーバーのホストを入力してください"
|
||||||
pleaseSelectUser: "ユーザーを選択してください"
|
pleaseSelectUser: "ユーザーを選択してください"
|
||||||
serverHostPlaceholder: "例: misskey.example.com"
|
serverHostPlaceholder: "例: misskey.example.com"
|
||||||
|
|
||||||
|
_serverSetupWizard:
|
||||||
|
installCompleted: "Misskeyのインストールが完了しました!"
|
||||||
|
firstCreateAccount: "まずは、管理者アカウントを作成しましょう。"
|
||||||
|
accountCreated: "管理者アカウントが作成されました!"
|
||||||
|
serverSetting: "サーバーの設定"
|
||||||
|
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "このウィザードで簡単に最適なサーバーの設定が行えます。"
|
||||||
|
settingsYouMakeHereCanBeChangedLater: "ここでの設定は、あとからでも変更できます。"
|
||||||
|
howWillYouUseMisskey: "Misskeyをどのように使いますか?"
|
||||||
|
_use:
|
||||||
|
single: "お一人様サーバー"
|
||||||
|
single_description: "自分専用のサーバーとして、一人で使う"
|
||||||
|
single_youCanCreateMultipleAccounts: "お一人様サーバーとして運用する場合でも、アカウントは必要に応じて複数作成可能です。"
|
||||||
|
group: "グループサーバー"
|
||||||
|
group_description: "信頼できる他の利用者を招待して、複数人で使う"
|
||||||
|
open: "オープンサーバー"
|
||||||
|
open_description: "不特定多数の利用者を受け入れる運営を行う"
|
||||||
|
openServerAdvice: "不特定多数の利用者を受け入れることはリスクが伴います。トラブルに対処できるよう、確実なモデレーション体制で運営することを推奨します。"
|
||||||
|
openServerAntiSpamAdvice: "自サーバーがスパムの踏み台にならないように、reCAPTCHAといったアンチボット機能を有効にするなど、セキュリティについても細心の注意が必要です。"
|
||||||
|
howManyUsersDoYouExpect: "どれくらいの人数を想定していますか?"
|
||||||
|
_scale:
|
||||||
|
small: "100人以下 (小規模)"
|
||||||
|
medium: "100人以上1000人以下 (中規模)"
|
||||||
|
large: "1000人以上 (大規模)"
|
||||||
|
largeScaleServerAdvice: "大規模なサーバーでは、ロードバランシングやデータベースのレプリケーションなど、高度なインフラストラクチャーの知識が必要になる場合があります。"
|
||||||
|
doYouConnectToFediverse: "Fediverseと接続しますか?"
|
||||||
|
doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。"
|
||||||
|
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。"
|
||||||
|
youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。"
|
||||||
|
adminInfo: "管理者情報"
|
||||||
|
adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。"
|
||||||
|
adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。"
|
||||||
|
followingSettingsAreRecommended: "以下の設定が推奨されます"
|
||||||
|
applyTheseSettings: "この設定を適用"
|
||||||
|
skipSettings: "設定をスキップ"
|
||||||
|
settingsCompleted: "設定が完了しました!"
|
||||||
|
settingsCompleted_description: "お疲れ様でした。準備が整ったので、さっそくサーバーの使用を開始できます。"
|
||||||
|
settingsCompleted_description2: "詳細なサーバー設定は、「コントロールパネル」から行えます。"
|
||||||
|
donationRequest: "寄付のお願い"
|
||||||
|
_donationRequest:
|
||||||
|
text1: "Misskeyは有志によって開発されている無料のソフトウェアです。"
|
||||||
|
text2: "今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。"
|
||||||
|
text3: "支援者向け特典もあります!"
|
||||||
|
|
|
@ -781,7 +781,6 @@ thisIsExperimentalFeature: "これは実験的な機能やから、仕様が変
|
||||||
developer: "開発者やで"
|
developer: "開発者やで"
|
||||||
makeExplorable: "アカウントを見つけやすくするで"
|
makeExplorable: "アカウントを見つけやすくするで"
|
||||||
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。"
|
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。"
|
||||||
showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示するで"
|
|
||||||
duplicate: "複製"
|
duplicate: "複製"
|
||||||
left: "左"
|
left: "左"
|
||||||
center: "真ん中"
|
center: "真ん中"
|
||||||
|
@ -1233,7 +1232,6 @@ showAvatarDecorations: "アイコンのデコレーション映す"
|
||||||
releaseToRefresh: "離したらリロード"
|
releaseToRefresh: "離したらリロード"
|
||||||
refreshing: "リロードしとる"
|
refreshing: "リロードしとる"
|
||||||
pullDownToRefresh: "引っ張ってリロードするで"
|
pullDownToRefresh: "引っ張ってリロードするで"
|
||||||
disableStreamingTimeline: "タイムラインのリアルタイム更新をやめるで"
|
|
||||||
useGroupedNotifications: "通知をグループ分けして出すで"
|
useGroupedNotifications: "通知をグループ分けして出すで"
|
||||||
signupPendingError: "メアド確認してたらなんか変なことなったわ。リンクの期限切れてるかもしれん。"
|
signupPendingError: "メアド確認してたらなんか変なことなったわ。リンクの期限切れてるかもしれん。"
|
||||||
cwNotationRequired: "「内容を隠す」んやったら注釈書かなアカンで。"
|
cwNotationRequired: "「内容を隠す」んやったら注釈書かなアカンで。"
|
||||||
|
|
|
@ -220,6 +220,7 @@ silenceThisInstance: "서버를 사일런스"
|
||||||
mediaSilenceThisInstance: "서버의 미디어를 사일런스"
|
mediaSilenceThisInstance: "서버의 미디어를 사일런스"
|
||||||
operations: "작업"
|
operations: "작업"
|
||||||
software: "소프트웨어"
|
software: "소프트웨어"
|
||||||
|
softwareName: "소프트웨어 이름"
|
||||||
version: "버전"
|
version: "버전"
|
||||||
metadata: "메타데이터"
|
metadata: "메타데이터"
|
||||||
withNFiles: "{n}개의 파일"
|
withNFiles: "{n}개의 파일"
|
||||||
|
@ -784,7 +785,6 @@ thisIsExperimentalFeature: "이 기능은 실험적인 기능입니다. 사양
|
||||||
developer: "개발자"
|
developer: "개발자"
|
||||||
makeExplorable: "계정을 쉽게 발견하도록 하기"
|
makeExplorable: "계정을 쉽게 발견하도록 하기"
|
||||||
makeExplorableDescription: "비활성화하면 \"발견하기\"에 나의 계정을 표시하지 않습니다."
|
makeExplorableDescription: "비활성화하면 \"발견하기\"에 나의 계정을 표시하지 않습니다."
|
||||||
showGapBetweenNotesInTimeline: "타임라인의 노트 사이를 띄워서 표시"
|
|
||||||
duplicate: "복제"
|
duplicate: "복제"
|
||||||
left: "왼쪽"
|
left: "왼쪽"
|
||||||
center: "가운데"
|
center: "가운데"
|
||||||
|
@ -1237,7 +1237,6 @@ showAvatarDecorations: "아바타 장식 표시"
|
||||||
releaseToRefresh: "놓아서 새로고침"
|
releaseToRefresh: "놓아서 새로고침"
|
||||||
refreshing: "새로고침 중"
|
refreshing: "새로고침 중"
|
||||||
pullDownToRefresh: "아래로 내려서 새로고침"
|
pullDownToRefresh: "아래로 내려서 새로고침"
|
||||||
disableStreamingTimeline: "타임라인의 실시간 갱신을 무효화하기"
|
|
||||||
useGroupedNotifications: "알림을 그룹화하고 표시"
|
useGroupedNotifications: "알림을 그룹화하고 표시"
|
||||||
signupPendingError: "메일 주소 확인중에 문제가 발생했습니다. 링크의 유효기간이 지났을 가능성이 있습니다."
|
signupPendingError: "메일 주소 확인중에 문제가 발생했습니다. 링크의 유효기간이 지났을 가능성이 있습니다."
|
||||||
cwNotationRequired: "'내용을 숨기기'를 체크한 경우 주석을 써야 합니다."
|
cwNotationRequired: "'내용을 숨기기'를 체크한 경우 주석을 써야 합니다."
|
||||||
|
@ -1346,6 +1345,8 @@ settingsMigrating: "설정을 이전하는 중입니다. 잠시 기다려주십
|
||||||
readonly: "읽기 전용"
|
readonly: "읽기 전용"
|
||||||
goToDeck: "덱으로 돌아가기"
|
goToDeck: "덱으로 돌아가기"
|
||||||
federationJobs: "연합 작업"
|
federationJobs: "연합 작업"
|
||||||
|
driveAboutTip: "드라이브는 이전에 업로드한 파일 목록을 표시해요. <br>\n노트에 첨부할 때 다시 사용하거나 나중에 게시할 파일을 미리 업로드할 수 있어요. <br>\n<b>파일을 삭제하면, 지금까지 그 파일을 사용한 모든 장소(노트, 페이지, 아바타, 배너 등)에서도 보이지 않게 되므로 주의해 주세요. 폴더를 만들고 정리할 수도 있어요.</b><br>"
|
||||||
|
scrollToClose: "스크롤하여 닫기"
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "아직 메시지가 없습니다"
|
noMessagesYet: "아직 메시지가 없습니다"
|
||||||
newMessage: "새로운 메시지"
|
newMessage: "새로운 메시지"
|
||||||
|
@ -1422,6 +1423,8 @@ _settings:
|
||||||
ifOn: "켜져 있을 때"
|
ifOn: "켜져 있을 때"
|
||||||
ifOff: "꺼져 있을 때"
|
ifOff: "꺼져 있을 때"
|
||||||
enableSyncThemesBetweenDevices: "기기 간 설치한 테마 동기화"
|
enableSyncThemesBetweenDevices: "기기 간 설치한 테마 동기화"
|
||||||
|
enablePullToRefresh: "계속해서 갱신"
|
||||||
|
enablePullToRefresh_description: "마우스에서 휠을 누르면서 드래그해요."
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "발신자 이름 표시"
|
showSenderName: "발신자 이름 표시"
|
||||||
sendOnEnter: "엔터로 보내기"
|
sendOnEnter: "엔터로 보내기"
|
||||||
|
@ -1429,6 +1432,7 @@ _preferencesProfile:
|
||||||
profileName: "프로필 이름"
|
profileName: "프로필 이름"
|
||||||
profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요."
|
profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요."
|
||||||
profileNameDescription2: "예: '메인PC', '스마트폰' 등"
|
profileNameDescription2: "예: '메인PC', '스마트폰' 등"
|
||||||
|
manageProfiles: "프로파일 관리"
|
||||||
_preferencesBackup:
|
_preferencesBackup:
|
||||||
autoBackup: "자동 백업"
|
autoBackup: "자동 백업"
|
||||||
restoreFromBackup: "백업으로 복구"
|
restoreFromBackup: "백업으로 복구"
|
||||||
|
@ -1467,6 +1471,7 @@ _delivery:
|
||||||
manuallySuspended: "수동 정지 중"
|
manuallySuspended: "수동 정지 중"
|
||||||
goneSuspended: "서버 삭제를 이유로 정지 중"
|
goneSuspended: "서버 삭제를 이유로 정지 중"
|
||||||
autoSuspendedForNotResponding: "서버 응답 없음을 이유로 정지 중"
|
autoSuspendedForNotResponding: "서버 응답 없음을 이유로 정지 중"
|
||||||
|
softwareSuspended: "전달 정지 중인 소프트웨어이므로 정지 중"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "설명"
|
howToPlay: "설명"
|
||||||
hold: "홀드"
|
hold: "홀드"
|
||||||
|
@ -1598,6 +1603,8 @@ _serverSettings:
|
||||||
openRegistration: "회원 가입을 활성화 하기"
|
openRegistration: "회원 가입을 활성화 하기"
|
||||||
openRegistrationWarning: "회원 가입을 개방하는 것은 리스크가 따릅니다. 서버를 항상 감시할 수 있고, 문제가 발생했을 때 바로 대응할 수 있는 상태에서만 활성화 하는 것을 권장합니다."
|
openRegistrationWarning: "회원 가입을 개방하는 것은 리스크가 따릅니다. 서버를 항상 감시할 수 있고, 문제가 발생했을 때 바로 대응할 수 있는 상태에서만 활성화 하는 것을 권장합니다."
|
||||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
|
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
|
||||||
|
deliverSuspendedSoftware: "전달 정지 중인 소프트웨어"
|
||||||
|
deliverSuspendedSoftwareDescription: "취약성 등의 이유로 서버의 소프트웨어 이름 및 버전 범위를 지정하여 전달을 정지할 수 있어요. 이 버전 정보는 서버가 제공한 것이며 신뢰성은 보장되지 않아요. 버전 지정에는 semver의 범위 지정을 사용할 수 있지만, >= 2024.3.1로 지정하면 2024.3.1-custom.0과 같은 custom.0과 같은 custom 버전이 포함되지 않기 때문에 >= 2024.3.1-0과 같이 prerelease를 지정하는 것이 좋아요."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "다른 계정에서 이 계정으로 이사"
|
moveFrom: "다른 계정에서 이 계정으로 이사"
|
||||||
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
||||||
|
@ -1915,6 +1922,7 @@ _role:
|
||||||
canManageCustomEmojis: "커스텀 이모지 관리"
|
canManageCustomEmojis: "커스텀 이모지 관리"
|
||||||
canManageAvatarDecorations: "아바타 꾸미기 관리"
|
canManageAvatarDecorations: "아바타 꾸미기 관리"
|
||||||
driveCapacity: "드라이브 용량"
|
driveCapacity: "드라이브 용량"
|
||||||
|
maxFileSize: "업로드 가능한 최대 파일 크기"
|
||||||
alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
|
alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
|
||||||
canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용"
|
canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용"
|
||||||
pinMax: "고정할 수 있는 노트 수"
|
pinMax: "고정할 수 있는 노트 수"
|
||||||
|
|
|
@ -784,7 +784,6 @@ thisIsExperimentalFeature: "Dit is een experimentele functie. De functionaliteit
|
||||||
developer: "Ontwikkelaar"
|
developer: "Ontwikkelaar"
|
||||||
makeExplorable: "Gebruikersaccount zichtbaar maken in “Verkennen”"
|
makeExplorable: "Gebruikersaccount zichtbaar maken in “Verkennen”"
|
||||||
makeExplorableDescription: "Als deze optie is uitgeschakeld, is uw gebruikersaccount niet zichtbaar in het gedeelte “Verkennen”."
|
makeExplorableDescription: "Als deze optie is uitgeschakeld, is uw gebruikersaccount niet zichtbaar in het gedeelte “Verkennen”."
|
||||||
showGapBetweenNotesInTimeline: "Een gat tussen noten op de tijdlijn weergeven"
|
|
||||||
duplicate: "Dupliceren"
|
duplicate: "Dupliceren"
|
||||||
left: "Links"
|
left: "Links"
|
||||||
center: "Center"
|
center: "Center"
|
||||||
|
|
|
@ -749,7 +749,6 @@ thisIsExperimentalFeature: "Ta funkcja jest eksperymentalna. Jej funkcjonalnoś
|
||||||
developer: "Programista"
|
developer: "Programista"
|
||||||
makeExplorable: "Pokazuj konto na stronie „Eksploruj”"
|
makeExplorable: "Pokazuj konto na stronie „Eksploruj”"
|
||||||
makeExplorableDescription: "Jeżeli wyłączysz tę opcję, Twoje konto nie będzie wyświetlać się w sekcji „Eksploruj”."
|
makeExplorableDescription: "Jeżeli wyłączysz tę opcję, Twoje konto nie będzie wyświetlać się w sekcji „Eksploruj”."
|
||||||
showGapBetweenNotesInTimeline: "Pokazuj odstęp między wpisami na osi czasu."
|
|
||||||
duplicate: "Duplikuj"
|
duplicate: "Duplikuj"
|
||||||
left: "Lewo"
|
left: "Lewo"
|
||||||
center: "Wyśsrodkuj"
|
center: "Wyśsrodkuj"
|
||||||
|
|
|
@ -784,7 +784,6 @@ thisIsExperimentalFeature: "Este é um recurso experimental. As funções podem
|
||||||
developer: "Programador"
|
developer: "Programador"
|
||||||
makeExplorable: "Deixe a sua conta encontrável em \"Explorar\"."
|
makeExplorable: "Deixe a sua conta encontrável em \"Explorar\"."
|
||||||
makeExplorableDescription: "Se você desativá-lo, outros usuários não poderão encontrar a sua conta na aba Descoberta."
|
makeExplorableDescription: "Se você desativá-lo, outros usuários não poderão encontrar a sua conta na aba Descoberta."
|
||||||
showGapBetweenNotesInTimeline: "Mostrar um espaço entre as notas na linha de tempo"
|
|
||||||
duplicate: "Duplicar"
|
duplicate: "Duplicar"
|
||||||
left: "Esquerda"
|
left: "Esquerda"
|
||||||
center: "Centralizar"
|
center: "Centralizar"
|
||||||
|
@ -1237,7 +1236,6 @@ showAvatarDecorations: "Exibir decorações de avatar"
|
||||||
releaseToRefresh: "Solte para atualizar"
|
releaseToRefresh: "Solte para atualizar"
|
||||||
refreshing: "Atualizando..."
|
refreshing: "Atualizando..."
|
||||||
pullDownToRefresh: "Puxe para baixo para atualizar"
|
pullDownToRefresh: "Puxe para baixo para atualizar"
|
||||||
disableStreamingTimeline: "Desabilitar atualizações em tempo real da linha do tempo"
|
|
||||||
useGroupedNotifications: "Agrupar notificações"
|
useGroupedNotifications: "Agrupar notificações"
|
||||||
signupPendingError: "Houve um problema ao verificar o endereço de email. O link pode ter expirado."
|
signupPendingError: "Houve um problema ao verificar o endereço de email. O link pode ter expirado."
|
||||||
cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada."
|
cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada."
|
||||||
|
|
|
@ -784,7 +784,6 @@ thisIsExperimentalFeature: "Aceasta este o funcție experimentală. Funcționali
|
||||||
developer: "Dezvoltator"
|
developer: "Dezvoltator"
|
||||||
makeExplorable: "Fă-ți contul vizibil în secțiunea„Explorați”"
|
makeExplorable: "Fă-ți contul vizibil în secțiunea„Explorați”"
|
||||||
makeExplorableDescription: "Dacă dezactivezi această opțiune, contul dvs. nu va fi vizibil în secțiunea\"Explorați\"."
|
makeExplorableDescription: "Dacă dezactivezi această opțiune, contul dvs. nu va fi vizibil în secțiunea\"Explorați\"."
|
||||||
showGapBetweenNotesInTimeline: "Afișați un decalaj între postările de pe cronologie"
|
|
||||||
duplicate: "Duplicat"
|
duplicate: "Duplicat"
|
||||||
left: "Stânga"
|
left: "Stânga"
|
||||||
center: "Centru"
|
center: "Centru"
|
||||||
|
|
|
@ -785,7 +785,6 @@ thisIsExperimentalFeature: "Это экспериментальная функц
|
||||||
developer: "Разработчик"
|
developer: "Разработчик"
|
||||||
makeExplorable: "Опубликовать профиль в «Обзоре»."
|
makeExplorable: "Опубликовать профиль в «Обзоре»."
|
||||||
makeExplorableDescription: "Если выключить, ваш профиль не будет показан в разделе «Обзор»."
|
makeExplorableDescription: "Если выключить, ваш профиль не будет показан в разделе «Обзор»."
|
||||||
showGapBetweenNotesInTimeline: "Показывать разделитель между заметками в ленте"
|
|
||||||
duplicate: "Дубликат"
|
duplicate: "Дубликат"
|
||||||
left: "Слева"
|
left: "Слева"
|
||||||
center: "По центру"
|
center: "По центру"
|
||||||
|
@ -1200,7 +1199,6 @@ privacyPolicyUrl: "Ссылка на Политику Конфиденциаль
|
||||||
attach: "Прикрепить"
|
attach: "Прикрепить"
|
||||||
angle: "Угол"
|
angle: "Угол"
|
||||||
flip: "Переворот"
|
flip: "Переворот"
|
||||||
disableStreamingTimeline: "Отключить обновление ленты в режиме реального времени"
|
|
||||||
useGroupedNotifications: "Отображать уведомления сгруппировано"
|
useGroupedNotifications: "Отображать уведомления сгруппировано"
|
||||||
doReaction: "Добавить реакцию"
|
doReaction: "Добавить реакцию"
|
||||||
code: "Код"
|
code: "Код"
|
||||||
|
|
|
@ -682,7 +682,6 @@ experimentalFeatures: "Experimentálne funkcie"
|
||||||
developer: "Vývojár"
|
developer: "Vývojár"
|
||||||
makeExplorable: "Spraviť účet viditeľný v \"Objavovať\""
|
makeExplorable: "Spraviť účet viditeľný v \"Objavovať\""
|
||||||
makeExplorableDescription: "Ak toto vypnete, váš účet sa nezobrazí v sekcii \"Objavovat\"."
|
makeExplorableDescription: "Ak toto vypnete, váš účet sa nezobrazí v sekcii \"Objavovat\"."
|
||||||
showGapBetweenNotesInTimeline: "Zobraziť medzeru medzi príspevkami časovej osi."
|
|
||||||
duplicate: "Duplikovať"
|
duplicate: "Duplikovať"
|
||||||
left: "Naľavo"
|
left: "Naľavo"
|
||||||
center: "Stred"
|
center: "Stred"
|
||||||
|
|
|
@ -778,7 +778,6 @@ thisIsExperimentalFeature: "นี่เป็นฟีเจอร์ทดล
|
||||||
developer: "สำหรับนักพัฒนา"
|
developer: "สำหรับนักพัฒนา"
|
||||||
makeExplorable: "ทำให้บัญชีมองเห็นใน “สำรวจ”"
|
makeExplorable: "ทำให้บัญชีมองเห็นใน “สำรวจ”"
|
||||||
makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน “สำรวจ”"
|
makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน “สำรวจ”"
|
||||||
showGapBetweenNotesInTimeline: "แสดงช่องว่างระหว่างโพสต์บนไทม์ไลน์"
|
|
||||||
duplicate: "ทำซ้ำ"
|
duplicate: "ทำซ้ำ"
|
||||||
left: "ซ้าย"
|
left: "ซ้าย"
|
||||||
center: "กึ่งกลาง"
|
center: "กึ่งกลาง"
|
||||||
|
@ -1227,7 +1226,6 @@ showAvatarDecorations: "แสดงตกแต่งอวตาร"
|
||||||
releaseToRefresh: "ปล่อยเพื่อรีเฟรช"
|
releaseToRefresh: "ปล่อยเพื่อรีเฟรช"
|
||||||
refreshing: "กำลังรีเฟรช..."
|
refreshing: "กำลังรีเฟรช..."
|
||||||
pullDownToRefresh: "ดึงลงเพื่อรีเฟรช"
|
pullDownToRefresh: "ดึงลงเพื่อรีเฟรช"
|
||||||
disableStreamingTimeline: "ปิดใช้งานอัปเดตไทม์ไลน์แบบเรียลไทม์"
|
|
||||||
useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว"
|
useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว"
|
||||||
signupPendingError: "มีปัญหาในการตรวจสอบที่อยู่อีเมลลิงก์อาจหมดอายุแล้ว"
|
signupPendingError: "มีปัญหาในการตรวจสอบที่อยู่อีเมลลิงก์อาจหมดอายุแล้ว"
|
||||||
cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย"
|
cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย"
|
||||||
|
|
|
@ -681,7 +681,6 @@ experimentalFeatures: "Експериментальні функції"
|
||||||
developer: "Розробник"
|
developer: "Розробник"
|
||||||
makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\""
|
makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\""
|
||||||
makeExplorableDescription: "Вимкніть, щоб обліковий запис не показувався у розділі \"Огляд\"."
|
makeExplorableDescription: "Вимкніть, щоб обліковий запис не показувався у розділі \"Огляд\"."
|
||||||
showGapBetweenNotesInTimeline: "Показувати розрив між записами у стрічці новин"
|
|
||||||
duplicate: "Дублікат"
|
duplicate: "Дублікат"
|
||||||
left: "Лівий"
|
left: "Лівий"
|
||||||
center: "Центр"
|
center: "Центр"
|
||||||
|
|
|
@ -783,7 +783,6 @@ thisIsExperimentalFeature: "Tính năng này đang trong quá trình thử nghi
|
||||||
developer: "Nhà phát triển"
|
developer: "Nhà phát triển"
|
||||||
makeExplorable: "Không hiện tôi trong \"Khám phá\""
|
makeExplorable: "Không hiện tôi trong \"Khám phá\""
|
||||||
makeExplorableDescription: "Nếu bạn tắt, tài khoản của bạn sẽ không hiện trong mục \"Khám phá\"."
|
makeExplorableDescription: "Nếu bạn tắt, tài khoản của bạn sẽ không hiện trong mục \"Khám phá\"."
|
||||||
showGapBetweenNotesInTimeline: "Hiện dải phân cách giữa các tút trên bảng tin"
|
|
||||||
duplicate: "Tạo bản sao"
|
duplicate: "Tạo bản sao"
|
||||||
left: "Bên trái"
|
left: "Bên trái"
|
||||||
center: "Giữa"
|
center: "Giữa"
|
||||||
|
|
|
@ -785,7 +785,6 @@ thisIsExperimentalFeature: "这是一项实验性功能。规范可能会变更
|
||||||
developer: "开发者"
|
developer: "开发者"
|
||||||
makeExplorable: "使账号可见。"
|
makeExplorable: "使账号可见。"
|
||||||
makeExplorableDescription: "关闭时,账号不会显示在\"发现\"中。"
|
makeExplorableDescription: "关闭时,账号不会显示在\"发现\"中。"
|
||||||
showGapBetweenNotesInTimeline: "时间线上的帖子分开显示。"
|
|
||||||
duplicate: "复制"
|
duplicate: "复制"
|
||||||
left: "左"
|
left: "左"
|
||||||
center: "中央"
|
center: "中央"
|
||||||
|
@ -1238,7 +1237,6 @@ showAvatarDecorations: "显示头像挂件"
|
||||||
releaseToRefresh: "松开以刷新"
|
releaseToRefresh: "松开以刷新"
|
||||||
refreshing: "刷新中"
|
refreshing: "刷新中"
|
||||||
pullDownToRefresh: "下拉以刷新"
|
pullDownToRefresh: "下拉以刷新"
|
||||||
disableStreamingTimeline: "禁止实时更新时间线"
|
|
||||||
useGroupedNotifications: "分组显示通知"
|
useGroupedNotifications: "分组显示通知"
|
||||||
signupPendingError: "确认电子邮件时出现错误。链接可能已过期。"
|
signupPendingError: "确认电子邮件时出现错误。链接可能已过期。"
|
||||||
cwNotationRequired: "在启用「隐藏内容」时必须输入注释"
|
cwNotationRequired: "在启用「隐藏内容」时必须输入注释"
|
||||||
|
@ -1348,6 +1346,7 @@ readonly: "只读"
|
||||||
goToDeck: "返回至 Deck"
|
goToDeck: "返回至 Deck"
|
||||||
federationJobs: "联合作业"
|
federationJobs: "联合作业"
|
||||||
driveAboutTip: "网盘可以显示以前上传的文件。<br>\n也可以在发布帖子时重复使用文件,或在发布帖子前预先上传文件。<br>\n<b>删除文件时,其将从至今为止所有用到该文件的地方(如帖子、页面、头像、横幅)消失。</b><br>\n也可以新建文件夹来整理文件。"
|
driveAboutTip: "网盘可以显示以前上传的文件。<br>\n也可以在发布帖子时重复使用文件,或在发布帖子前预先上传文件。<br>\n<b>删除文件时,其将从至今为止所有用到该文件的地方(如帖子、页面、头像、横幅)消失。</b><br>\n也可以新建文件夹来整理文件。"
|
||||||
|
scrollToClose: "滑动并关闭"
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "还没有消息"
|
noMessagesYet: "还没有消息"
|
||||||
newMessage: "新消息"
|
newMessage: "新消息"
|
||||||
|
@ -1432,6 +1431,7 @@ _preferencesProfile:
|
||||||
profileName: "配置名"
|
profileName: "配置名"
|
||||||
profileNameDescription: "请指定用于识别此设备的名称"
|
profileNameDescription: "请指定用于识别此设备的名称"
|
||||||
profileNameDescription2: "如「PC」、「手机」等"
|
profileNameDescription2: "如「PC」、「手机」等"
|
||||||
|
manageProfiles: "管理配置文件"
|
||||||
_preferencesBackup:
|
_preferencesBackup:
|
||||||
autoBackup: "自动备份"
|
autoBackup: "自动备份"
|
||||||
restoreFromBackup: "从备份恢复"
|
restoreFromBackup: "从备份恢复"
|
||||||
|
|
|
@ -785,7 +785,6 @@ thisIsExperimentalFeature: "這是一項實驗性功能,其行為會隨需要
|
||||||
developer: "開發者"
|
developer: "開發者"
|
||||||
makeExplorable: "使自己的帳戶更容易被找到"
|
makeExplorable: "使自己的帳戶更容易被找到"
|
||||||
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。"
|
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。"
|
||||||
showGapBetweenNotesInTimeline: "分開顯示時間軸上的貼文"
|
|
||||||
duplicate: "複製"
|
duplicate: "複製"
|
||||||
left: "左"
|
left: "左"
|
||||||
center: "置中"
|
center: "置中"
|
||||||
|
@ -1238,7 +1237,6 @@ showAvatarDecorations: "顯示頭像裝飾"
|
||||||
releaseToRefresh: "放開以更新內容"
|
releaseToRefresh: "放開以更新內容"
|
||||||
refreshing: "載入更新中"
|
refreshing: "載入更新中"
|
||||||
pullDownToRefresh: "往下拉來更新內容"
|
pullDownToRefresh: "往下拉來更新內容"
|
||||||
disableStreamingTimeline: "停用時間軸的即時更新"
|
|
||||||
useGroupedNotifications: "分組顯示通知訊息"
|
useGroupedNotifications: "分組顯示通知訊息"
|
||||||
signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。"
|
signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。"
|
||||||
cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。"
|
cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。"
|
||||||
|
@ -1434,6 +1432,7 @@ _preferencesProfile:
|
||||||
profileName: "設定檔案名稱"
|
profileName: "設定檔案名稱"
|
||||||
profileNameDescription: "設定一個名稱來識別此裝置。"
|
profileNameDescription: "設定一個名稱來識別此裝置。"
|
||||||
profileNameDescription2: "例如:「主要個人電腦」、「智慧型手機」等"
|
profileNameDescription2: "例如:「主要個人電腦」、「智慧型手機」等"
|
||||||
|
manageProfiles: "管理個人檔案"
|
||||||
_preferencesBackup:
|
_preferencesBackup:
|
||||||
autoBackup: "自動備份"
|
autoBackup: "自動備份"
|
||||||
restoreFromBackup: "從備份還原"
|
restoreFromBackup: "從備份還原"
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"watch": "pnpm dev",
|
"watch": "pnpm dev",
|
||||||
"dev": "node scripts/dev.mjs",
|
"dev": "node scripts/dev.mjs",
|
||||||
"lint": "pnpm -r lint",
|
"lint": "pnpm -r lint",
|
||||||
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
|
"cy:open": "pnpm cypress open --config-file=cypress.config.ts",
|
||||||
"cy:run": "pnpm cypress run",
|
"cy:run": "pnpm cypress run",
|
||||||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
"e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
"e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class VisibleUserGeneratedContentsForNonLoggedInVisitors1746330901644 {
|
||||||
|
name = 'VisibleUserGeneratedContentsForNonLoggedInVisitors1746330901644'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "ugcVisibilityForVisitor" character varying(128) NOT NULL DEFAULT 'local'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ugcVisibilityForVisitor"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SingleUserMode1746422049376 {
|
||||||
|
name = 'SingleUserMode1746422049376'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "singleUserMode" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "singleUserMode"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -578,6 +578,20 @@ export class ChatService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteRoom(room: MiChatRoom, deleter?: MiUser) {
|
public async deleteRoom(room: MiChatRoom, deleter?: MiUser) {
|
||||||
|
const memberships = (await this.chatRoomMembershipsRepository.findBy({ roomId: room.id })).map(m => ({
|
||||||
|
userId: m.userId,
|
||||||
|
})).concat({ // ownerはmembershipレコードを作らないため
|
||||||
|
userId: room.ownerId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 未読フラグ削除
|
||||||
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
|
for (const membership of memberships) {
|
||||||
|
redisPipeline.del(`newRoomChatMessageExists:${membership.userId}:${room.id}`);
|
||||||
|
redisPipeline.srem(`newChatMessagesExists:${membership.userId}`, `room:${room.id}`);
|
||||||
|
}
|
||||||
|
await redisPipeline.exec();
|
||||||
|
|
||||||
await this.chatRoomsRepository.delete(room.id);
|
await this.chatRoomsRepository.delete(room.id);
|
||||||
|
|
||||||
if (deleter) {
|
if (deleter) {
|
||||||
|
@ -709,6 +723,12 @@ export class ChatService {
|
||||||
public async leaveRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
|
public async leaveRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
|
||||||
const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId });
|
const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId });
|
||||||
await this.chatRoomMembershipsRepository.delete(membership.id);
|
await this.chatRoomMembershipsRepository.delete(membership.id);
|
||||||
|
|
||||||
|
// 未読フラグを消す (「既読にする」というわけでもないのでreadメソッドは使わないでおく)
|
||||||
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
|
redisPipeline.del(`newRoomChatMessageExists:${userId}:${roomId}`);
|
||||||
|
redisPipeline.srem(`newChatMessagesExists:${userId}`, `room:${roomId}`);
|
||||||
|
await redisPipeline.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -429,6 +429,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
userId: channel.userId,
|
userId: channel.userId,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||||
|
hasPoll: note.hasPoll || undefined,
|
||||||
uri: note.uri ?? undefined,
|
uri: note.uri ?? undefined,
|
||||||
url: note.url ?? undefined,
|
url: note.url ?? undefined,
|
||||||
|
|
||||||
|
@ -593,4 +594,42 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
relations: ['user'],
|
relations: ['user'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async fetchDiffs(noteIds: MiNote['id'][]) {
|
||||||
|
if (noteIds.length === 0) return [];
|
||||||
|
|
||||||
|
const notes = await this.notesRepository.find({
|
||||||
|
where: {
|
||||||
|
id: In(noteIds),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userHost: true,
|
||||||
|
reactions: true,
|
||||||
|
reactionAndUserPairCache: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bufferedReactionsMap = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(noteIds) : null;
|
||||||
|
|
||||||
|
const packings = notes.map(note => {
|
||||||
|
const bufferedReactions = bufferedReactionsMap?.get(note.id);
|
||||||
|
//const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));
|
||||||
|
|
||||||
|
const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.deltas ?? {}));
|
||||||
|
|
||||||
|
const reactionEmojiNames = Object.keys(reactions)
|
||||||
|
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
||||||
|
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
||||||
|
|
||||||
|
return this.customEmojiService.populateEmojis(reactionEmojiNames, note.userHost).then(reactionEmojis => ({
|
||||||
|
id: note.id,
|
||||||
|
reactions,
|
||||||
|
reactionEmojis,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(packings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,3 +106,6 @@ export class MiAntenna {
|
||||||
})
|
})
|
||||||
public excludeNotesInSensitiveChannel: boolean;
|
public excludeNotesInSensitiveChannel: boolean;
|
||||||
}
|
}
|
||||||
|
// Note for future developers: When you added a new column,
|
||||||
|
// You should update ExportAntennaProcessorService and ImportAntennaProcessorService
|
||||||
|
// to export and import antennas correctly.
|
||||||
|
|
|
@ -659,6 +659,12 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public federationHosts: string[];
|
public federationHosts: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
default: 'local',
|
||||||
|
})
|
||||||
|
public ugcVisibilityForVisitor: 'all' | 'local' | 'none';
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 64,
|
length: 64,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -669,6 +675,11 @@ export class MiMeta {
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
public deliverSuspendedSoftware: SoftwareSuspension[];
|
public deliverSuspendedSoftware: SoftwareSuspension[];
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public singleUserMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SoftwareSuspension = {
|
export type SoftwareSuspension = {
|
||||||
|
|
|
@ -256,6 +256,10 @@ export const packedNoteSchema = {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
hasPoll: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
|
||||||
myReaction: {
|
myReaction: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
import { ExportedAntenna } from '@/queue/processors/ImportAntennasProcessorService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type { DBExportAntennasData } from '../types.js';
|
import type { DBExportAntennasData } from '../types.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
@ -86,7 +87,8 @@ export class ExportAntennasProcessorService {
|
||||||
excludeBots: antenna.excludeBots,
|
excludeBots: antenna.excludeBots,
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
}));
|
excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel,
|
||||||
|
} satisfies Required<ExportedAntenna>));
|
||||||
if (antennas.length - 1 !== index) {
|
if (antennas.length - 1 !== index) {
|
||||||
write(', ');
|
write(', ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,17 +11,18 @@ import Logger from '@/logger.js';
|
||||||
import type { AntennasRepository } from '@/models/_.js';
|
import type { AntennasRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { Schema, SchemaType } from '@/misc/json-schema.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import { DBAntennaImportJobData } from '../types.js';
|
import { DBAntennaImportJobData } from '../types.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
const Ajv = _Ajv.default;
|
const Ajv = _Ajv.default;
|
||||||
|
|
||||||
const validate = new Ajv().compile({
|
const exportedAntennaSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||||
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] },
|
src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] },
|
||||||
userListAccts: {
|
userListAccts: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
|
@ -47,9 +48,14 @@ const validate = new Ajv().compile({
|
||||||
excludeBots: { type: 'boolean' },
|
excludeBots: { type: 'boolean' },
|
||||||
withReplies: { type: 'boolean' },
|
withReplies: { type: 'boolean' },
|
||||||
withFile: { type: 'boolean' },
|
withFile: { type: 'boolean' },
|
||||||
|
excludeNotesInSensitiveChannel: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
|
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
|
||||||
});
|
} as const satisfies Schema;
|
||||||
|
|
||||||
|
export type ExportedAntenna = SchemaType<typeof exportedAntennaSchema>;
|
||||||
|
|
||||||
|
const validate = new Ajv().compile<ExportedAntenna>(exportedAntennaSchema);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportAntennasProcessorService {
|
export class ImportAntennasProcessorService {
|
||||||
|
@ -91,6 +97,7 @@ export class ImportAntennasProcessorService {
|
||||||
excludeBots: antenna.excludeBots,
|
excludeBots: antenna.excludeBots,
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
|
excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel,
|
||||||
});
|
});
|
||||||
this.logger.succ('Antenna created: ' + result.id);
|
this.logger.succ('Antenna created: ' + result.id);
|
||||||
this.globalEventService.publishInternalEvent('antennaCreated', result);
|
this.globalEventService.publishInternalEvent('antennaCreated', result);
|
||||||
|
|
|
@ -323,6 +323,7 @@ export * as 'notes/replies' from './endpoints/notes/replies.js';
|
||||||
export * as 'notes/search' from './endpoints/notes/search.js';
|
export * as 'notes/search' from './endpoints/notes/search.js';
|
||||||
export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js';
|
export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js';
|
||||||
export * as 'notes/show' from './endpoints/notes/show.js';
|
export * as 'notes/show' from './endpoints/notes/show.js';
|
||||||
|
export * as 'notes/show-partial-bulk' from './endpoints/notes/show-partial-bulk.js';
|
||||||
export * as 'notes/state' from './endpoints/notes/state.js';
|
export * as 'notes/state' from './endpoints/notes/state.js';
|
||||||
export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js';
|
export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js';
|
||||||
export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js';
|
export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js';
|
||||||
|
|
|
@ -546,6 +546,15 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
singleUserMode: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
ugcVisibilityForVisitor: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['all', 'local', 'none'],
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -691,6 +700,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
federation: instance.federation,
|
federation: instance.federation,
|
||||||
federationHosts: instance.federationHosts,
|
federationHosts: instance.federationHosts,
|
||||||
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
|
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
|
||||||
|
singleUserMode: instance.singleUserMode,
|
||||||
|
ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,6 +196,11 @@ export const paramDef = {
|
||||||
required: ['software', 'versionRange'],
|
required: ['software', 'versionRange'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
singleUserMode: { type: 'boolean' },
|
||||||
|
ugcVisibilityForVisitor: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['all', 'local', 'none'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -690,6 +695,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
|
set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.singleUserMode !== undefined) {
|
||||||
|
set.singleUserMode = ps.singleUserMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.ugcVisibilityForVisitor !== undefined) {
|
||||||
|
set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor;
|
||||||
|
}
|
||||||
|
|
||||||
const before = await this.metaService.fetch(true);
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['notes'],
|
||||||
|
|
||||||
|
requireCredential: false,
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
noteIds: { type: 'array', items: { type: 'string', format: 'misskey:id' }, maxItems: 100, minItems: 1 },
|
||||||
|
},
|
||||||
|
required: ['noteIds'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private noteEntityService: NoteEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
return await this.noteEntityService.fetchDiffs(ps.noteIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,10 +3,12 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -46,6 +48,9 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private serverSettings: MiMeta,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
) {
|
) {
|
||||||
|
@ -59,6 +64,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.signinRequired);
|
throw new ApiError(meta.errors.signinRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
|
||||||
|
throw new ApiError(meta.errors.signinRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
|
||||||
|
throw new ApiError(meta.errors.signinRequired);
|
||||||
|
}
|
||||||
|
|
||||||
return await this.noteEntityService.pack(note, me, {
|
return await this.noteEntityService.pack(note, me, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { In, IsNull } from 'typeorm';
|
import { In, IsNull } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
import type { MiMeta, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
@ -82,6 +82,9 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private serverSettings: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -92,6 +95,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private apiLoggerService: ApiLoggerService,
|
private apiLoggerService: ApiLoggerService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => {
|
super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => {
|
||||||
|
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
}
|
||||||
|
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
const isModerator = await this.roleService.isModerator(me);
|
const isModerator = await this.roleService.isModerator(me);
|
||||||
|
@ -123,6 +130,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
} else {
|
} else {
|
||||||
// Lookup user
|
// Lookup user
|
||||||
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
||||||
|
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && me == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
}
|
||||||
|
|
||||||
user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => {
|
user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => {
|
||||||
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`);
|
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`);
|
||||||
throw new ApiError(meta.errors.failedToResolveRemoteUser);
|
throw new ApiError(meta.errors.failedToResolveRemoteUser);
|
||||||
|
@ -139,6 +150,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.noSuchUser);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && user.host != null && me == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
}
|
||||||
|
|
||||||
if (user.host == null) {
|
if (user.host == null) {
|
||||||
if (me == null && ip != null) {
|
if (me == null && ip != null) {
|
||||||
this.perUserPvChart.commitByVisitor(user, ip);
|
this.perUserPvChart.commitByVisitor(user, ip);
|
||||||
|
|
|
@ -513,7 +513,12 @@ export class ClientServerService {
|
||||||
|
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
if (user != null) {
|
if (
|
||||||
|
user != null && (
|
||||||
|
this.meta.ugcVisibilityForVisitor === 'all' ||
|
||||||
|
(this.meta.ugcVisibilityForVisitor === 'local' && user.host == null)
|
||||||
|
)
|
||||||
|
) {
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
const me = profile.fields
|
const me = profile.fields
|
||||||
? profile.fields
|
? profile.fields
|
||||||
|
@ -577,7 +582,13 @@ export class ClientServerService {
|
||||||
relations: ['user'],
|
relations: ['user'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note && !note.user!.requireSigninToViewContents) {
|
if (
|
||||||
|
note &&
|
||||||
|
!note.user!.requireSigninToViewContents &&
|
||||||
|
(this.meta.ugcVisibilityForVisitor === 'all' ||
|
||||||
|
(this.meta.ugcVisibilityForVisitor === 'local' && note.userHost == null)
|
||||||
|
)
|
||||||
|
) {
|
||||||
const _note = await this.noteEntityService.pack(note);
|
const _note = await this.noteEntityService.pack(note);
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
|
||||||
reply.header('Cache-Control', 'public, max-age=15');
|
reply.header('Cache-Control', 'public, max-age=15');
|
||||||
|
|
|
@ -79,39 +79,6 @@ export async function mainBoot() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = useStream();
|
|
||||||
|
|
||||||
let reloadDialogShowing = false;
|
|
||||||
stream.on('_disconnected_', async () => {
|
|
||||||
if (prefer.s.serverDisconnectedBehavior === 'reload') {
|
|
||||||
window.location.reload();
|
|
||||||
} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
|
|
||||||
if (reloadDialogShowing) return;
|
|
||||||
reloadDialogShowing = true;
|
|
||||||
const { canceled } = await confirm({
|
|
||||||
type: 'warning',
|
|
||||||
title: i18n.ts.disconnectedFromServer,
|
|
||||||
text: i18n.ts.reloadConfirm,
|
|
||||||
});
|
|
||||||
reloadDialogShowing = false;
|
|
||||||
if (!canceled) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('emojiAdded', emojiData => {
|
|
||||||
addCustomEmoji(emojiData.emoji);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('emojiUpdated', emojiData => {
|
|
||||||
updateCustomEmojis(emojiData.emojis);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('emojiDeleted', emojiData => {
|
|
||||||
removeCustomEmojis(emojiData.emojis);
|
|
||||||
});
|
|
||||||
|
|
||||||
launchPlugins();
|
launchPlugins();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -169,8 +136,6 @@ export async function mainBoot() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.on('announcementCreated', onAnnouncementCreated);
|
|
||||||
|
|
||||||
if ($i.isDeleted) {
|
if ($i.isDeleted) {
|
||||||
alert({
|
alert({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -348,6 +313,42 @@ export async function mainBoot() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (store.s.realtimeMode) {
|
||||||
|
const stream = useStream();
|
||||||
|
|
||||||
|
let reloadDialogShowing = false;
|
||||||
|
stream.on('_disconnected_', async () => {
|
||||||
|
if (prefer.s.serverDisconnectedBehavior === 'reload') {
|
||||||
|
window.location.reload();
|
||||||
|
} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
|
||||||
|
if (reloadDialogShowing) return;
|
||||||
|
reloadDialogShowing = true;
|
||||||
|
const { canceled } = await confirm({
|
||||||
|
type: 'warning',
|
||||||
|
title: i18n.ts.disconnectedFromServer,
|
||||||
|
text: i18n.ts.reloadConfirm,
|
||||||
|
});
|
||||||
|
reloadDialogShowing = false;
|
||||||
|
if (!canceled) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('emojiAdded', emojiData => {
|
||||||
|
addCustomEmoji(emojiData.emoji);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('emojiUpdated', emojiData => {
|
||||||
|
updateCustomEmojis(emojiData.emojis);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('emojiDeleted', emojiData => {
|
||||||
|
removeCustomEmojis(emojiData.emojis);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('announcementCreated', onAnnouncementCreated);
|
||||||
|
|
||||||
const main = markRaw(stream.useChannel('main', null, 'System'));
|
const main = markRaw(stream.useChannel('main', null, 'System'));
|
||||||
|
|
||||||
// 自分の情報が更新されたとき
|
// 自分の情報が更新されたとき
|
||||||
|
@ -386,12 +387,7 @@ export async function mainBoot() {
|
||||||
|
|
||||||
// 個人宛てお知らせが発行されたとき
|
// 個人宛てお知らせが発行されたとき
|
||||||
main.on('announcementCreated', onAnnouncementCreated);
|
main.on('announcementCreated', onAnnouncementCreated);
|
||||||
|
}
|
||||||
// トークンが再生成されたとき
|
|
||||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
|
||||||
main.on('myTokenRegenerated', () => {
|
|
||||||
signout();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcut
|
// shortcut
|
||||||
|
|
|
@ -14,13 +14,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
pagination: Paging;
|
pagination: PagingCtx;
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
extractor?: (item: any) => any;
|
extractor?: (item: any) => any;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
|
|
@ -154,6 +154,10 @@ onUnmounted(() => {
|
||||||
&.naked {
|
&.naked {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.scrollable {
|
&.scrollable {
|
||||||
|
|
|
@ -7,8 +7,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
|
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
|
||||||
import MkAd from '@/components/global/MkAd.vue';
|
import MkAd from '@/components/global/MkAd.vue';
|
||||||
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
|
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -19,7 +17,7 @@ import { getDateText } from '@/utility/timeline-date-separate.js';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array as PropType<MisskeyEntity[]>,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
direction: {
|
direction: {
|
||||||
|
|
|
@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, useTemplateRef } from 'vue';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
@ -53,7 +53,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = ref<InstanceType<typeof MkModalWindow>>();
|
const dialog = useTemplateRef('dialog');
|
||||||
|
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
|
|
|
@ -6,11 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!hardMuted && muted === false"
|
v-if="!hardMuted && muted === false"
|
||||||
v-show="!isDeleted"
|
|
||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
|
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
|
||||||
:tabindex="isDeleted ? '-1' : '0'"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||||
|
@ -87,7 +86,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll"/>
|
<MkPoll
|
||||||
|
v-if="appearNote.poll"
|
||||||
|
:noteId="appearNote.id"
|
||||||
|
:multiple="appearNote.poll.multiple"
|
||||||
|
:expiresAt="appearNote.poll.expiresAt"
|
||||||
|
:choices="$appearNote.pollChoices"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:emojiUrls="appearNote.emojis"
|
||||||
|
:class="$style.poll"
|
||||||
|
/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,7 +109,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</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>
|
<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>
|
</div>
|
||||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" style="margin-top: 6px;" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
<MkReactionsViewer
|
||||||
|
v-if="appearNote.reactionAcceptance !== 'likeOnly'"
|
||||||
|
style="margin-top: 6px;"
|
||||||
|
:reactions="$appearNote.reactions"
|
||||||
|
:reactionEmojis="$appearNote.reactionEmojis"
|
||||||
|
:myReaction="$appearNote.myReaction"
|
||||||
|
:noteId="appearNote.id"
|
||||||
|
:maxNumber="16"
|
||||||
|
@mockUpdateMyReaction="emitUpdReaction"
|
||||||
|
>
|
||||||
<template #more>
|
<template #more>
|
||||||
<MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA>
|
<MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
@ -125,11 +142,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && $appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
|
<i v-else-if="$appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number($appearNote.reactionCount) }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
|
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
|
@ -176,7 +193,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, ref, useTemplateRef, watch, provide } from 'vue';
|
import { computed, inject, onMounted, ref, useTemplateRef, watch, provide, shallowRef, reactive } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
|
@ -210,7 +227,7 @@ import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
|
import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
|
||||||
import { useNoteCapture } from '@/use/use-note-capture.js';
|
import { noteEvents, useNoteCapture } from '@/use/use-note-capture.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import { useTooltip } from '@/use/use-tooltip.js';
|
import { useTooltip } from '@/use/use-tooltip.js';
|
||||||
import { claimAchievement } from '@/utility/achievements.js';
|
import { claimAchievement } from '@/utility/achievements.js';
|
||||||
|
@ -223,6 +240,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -245,29 +263,33 @@ const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||||
|
|
||||||
const note = ref(deepClone(props.note));
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
if (noteViewInterruptors.length > 0) {
|
if (noteViewInterruptors.length > 0) {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let result: Misskey.entities.Note | null = deepClone(note.value);
|
let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
try {
|
try {
|
||||||
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
if (result === null) {
|
|
||||||
isDeleted.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
note.value = result as Misskey.entities.Note;
|
note = result as Misskey.entities.Note;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note.value);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
|
const appearNote = getAppearNote(note);
|
||||||
|
const $appearNote = reactive({
|
||||||
|
reactions: appearNote.reactions,
|
||||||
|
reactionCount: appearNote.reactionCount,
|
||||||
|
reactionEmojis: appearNote.reactionEmojis,
|
||||||
|
myReaction: appearNote.myReaction,
|
||||||
|
pollChoices: appearNote.poll?.choices,
|
||||||
|
});
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
const menuButton = useTemplateRef('menuButton');
|
const menuButton = useTemplateRef('menuButton');
|
||||||
|
@ -275,32 +297,30 @@ const renoteButton = useTemplateRef('renoteButton');
|
||||||
const renoteTime = useTemplateRef('renoteTime');
|
const renoteTime = useTemplateRef('renoteTime');
|
||||||
const reactButton = useTemplateRef('reactButton');
|
const reactButton = useTemplateRef('reactButton');
|
||||||
const clipButton = useTemplateRef('clipButton');
|
const clipButton = useTemplateRef('clipButton');
|
||||||
const appearNote = computed(() => getAppearNote(note.value));
|
|
||||||
const galleryEl = useTemplateRef('galleryEl');
|
const galleryEl = useTemplateRef('galleryEl');
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
const parsed = computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
|
||||||
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
|
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.renote?.url !== url && appearNote.renote?.uri !== url) : null);
|
||||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
const isLong = shouldCollapsed(appearNote, urls.value ?? []);
|
||||||
const collapsed = ref(appearNote.value.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const muted = ref(checkMute(appearNote, $i?.mutedWords));
|
||||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords, true));
|
||||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
|
||||||
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
|
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i?.id));
|
||||||
const renoteCollapsed = ref(
|
const renoteCollapsed = ref(
|
||||||
prefer.s.collapseRenotes && isRenote && (
|
prefer.s.collapseRenotes && isRenote && (
|
||||||
($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
|
($i && ($i.id === note.userId || $i.id === appearNote.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
|
||||||
(appearNote.value.myReaction != null)
|
($appearNote.myReaction != null)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
type: 'lookup',
|
type: 'lookup',
|
||||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
url: `https://${host}/notes/${appearNote.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||||
|
@ -357,7 +377,7 @@ const keymap = {
|
||||||
'v|enter': () => {
|
'v|enter': () => {
|
||||||
if (renoteCollapsed.value) {
|
if (renoteCollapsed.value) {
|
||||||
renoteCollapsed.value = false;
|
renoteCollapsed.value = false;
|
||||||
} else if (appearNote.value.cw != null) {
|
} else if (appearNote.cw != null) {
|
||||||
showContent.value = !showContent.value;
|
showContent.value = !showContent.value;
|
||||||
} else if (isLong) {
|
} else if (isLong) {
|
||||||
collapsed.value = !collapsed.value;
|
collapsed.value = !collapsed.value;
|
||||||
|
@ -380,28 +400,28 @@ const keymap = {
|
||||||
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
|
reaction: reaction,
|
||||||
|
}).then(() => {
|
||||||
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
|
userId: $i!.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (props.mock) {
|
if (!props.mock) {
|
||||||
watch(() => props.note, (to) => {
|
|
||||||
note.value = deepClone(to);
|
|
||||||
}, { deep: true });
|
|
||||||
} else {
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: rootEl,
|
|
||||||
note: appearNote,
|
note: appearNote,
|
||||||
pureNote: note,
|
parentNote: note,
|
||||||
isDeletedRef: isDeleted,
|
$note: $appearNote,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.mock) {
|
if (!props.mock) {
|
||||||
useTooltip(renoteButton, async (showing) => {
|
useTooltip(renoteButton, async (showing) => {
|
||||||
const renotes = await misskeyApi('notes/renotes', {
|
const renotes = await misskeyApi('notes/renotes', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 11,
|
limit: 11,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -412,19 +432,19 @@ if (!props.mock) {
|
||||||
const { dispose } = os.popup(MkUsersTooltip, {
|
const { dispose } = os.popup(MkUsersTooltip, {
|
||||||
showing,
|
showing,
|
||||||
users,
|
users,
|
||||||
count: appearNote.value.renoteCount,
|
count: appearNote.renoteCount,
|
||||||
targetElement: renoteButton.value,
|
targetElement: renoteButton.value,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
useTooltip(reactButton, async (showing) => {
|
useTooltip(reactButton, async (showing) => {
|
||||||
const reactions = await misskeyApiGet('notes/reactions', {
|
const reactions = await misskeyApiGet('notes/reactions', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
_cacheKey_: appearNote.value.reactionCount,
|
_cacheKey_: $appearNote.reactionCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = reactions.map(x => x.user);
|
const users = reactions.map(x => x.user);
|
||||||
|
@ -435,7 +455,7 @@ if (!props.mock) {
|
||||||
showing,
|
showing,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
users,
|
users,
|
||||||
count: appearNote.value.reactionCount,
|
count: $appearNote.reactionCount,
|
||||||
targetElement: reactButton.value!,
|
targetElement: reactButton.value!,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -448,7 +468,7 @@ function renote(viaKeyboard = false) {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
|
const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock });
|
||||||
os.popupMenu(menu, renoteButton.value, {
|
os.popupMenu(menu, renoteButton.value, {
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
});
|
});
|
||||||
|
@ -460,8 +480,8 @@ function reply(): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.channel,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
|
@ -470,7 +490,7 @@ function reply(): void {
|
||||||
function react(): void {
|
function react(): void {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
|
@ -478,8 +498,13 @@ function react(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
|
}).then(() => {
|
||||||
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
|
userId: $i!.id,
|
||||||
|
reaction: '❤️',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const el = reactButton.value;
|
const el = reactButton.value;
|
||||||
if (el && prefer.s.animation) {
|
if (el && prefer.s.animation) {
|
||||||
|
@ -492,7 +517,7 @@ function react(): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => {
|
reactionPicker.show(reactButton.value ?? null, note, async (reaction) => {
|
||||||
if (prefer.s.confirmOnReact) {
|
if (prefer.s.confirmOnReact) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
|
@ -506,14 +531,23 @@ function react(): void {
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
emit('reaction', reaction);
|
emit('reaction', reaction);
|
||||||
|
$appearNote.reactions[reaction] = 1;
|
||||||
|
$appearNote.reactionCount++;
|
||||||
|
$appearNote.myReaction = reaction;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
|
reaction: reaction,
|
||||||
|
}).then(() => {
|
||||||
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
|
userId: $i!.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
|
});
|
||||||
|
|
||||||
|
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
|
||||||
claimAchievement('reactWithoutRead');
|
claimAchievement('reactWithoutRead');
|
||||||
}
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -522,8 +556,8 @@ function react(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(targetNote: Misskey.entities.Note): void {
|
function undoReact(): void {
|
||||||
const oldReaction = targetNote.myReaction;
|
const oldReaction = $appearNote.myReaction;
|
||||||
if (!oldReaction) return;
|
if (!oldReaction) return;
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
|
@ -532,15 +566,15 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/delete', {
|
misskeyApi('notes/reactions/delete', {
|
||||||
noteId: targetNote.id,
|
noteId: appearNote.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReact() {
|
function toggleReact() {
|
||||||
if (appearNote.value.myReaction == null) {
|
if ($appearNote.myReaction == null) {
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
undoReact(appearNote.value);
|
undoReact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -556,7 +590,7 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value });
|
||||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -566,7 +600,7 @@ function showMenu(): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value });
|
||||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,7 +609,7 @@ async function clip(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(): void {
|
function showRenoteMenu(): void {
|
||||||
|
@ -590,9 +624,10 @@ function showRenoteMenu(): void {
|
||||||
danger: true,
|
danger: true,
|
||||||
action: () => {
|
action: () => {
|
||||||
misskeyApi('notes/delete', {
|
misskeyApi('notes/delete', {
|
||||||
noteId: note.value.id,
|
noteId: note.id,
|
||||||
|
}).then(() => {
|
||||||
|
globalEvents.emit('noteDeleted', note.id);
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -601,23 +636,23 @@ function showRenoteMenu(): void {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
text: i18n.ts.renoteDetails,
|
text: i18n.ts.renoteDetails,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
to: notePage(note.value),
|
to: notePage(note),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isMyRenote) {
|
if (isMyRenote) {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
os.popupMenu([
|
os.popupMenu([
|
||||||
renoteDetailsMenu,
|
renoteDetailsMenu,
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getUnrenote(),
|
getUnrenote(),
|
||||||
], renoteTime.value);
|
], renoteTime.value);
|
||||||
} else {
|
} else {
|
||||||
os.popupMenu([
|
os.popupMenu([
|
||||||
renoteDetailsMenu,
|
renoteDetailsMenu,
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
|
||||||
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
||||||
], renoteTime.value);
|
], renoteTime.value);
|
||||||
}
|
}
|
||||||
|
@ -641,9 +676,8 @@ function focusAfter() {
|
||||||
|
|
||||||
function readPromo() {
|
function readPromo() {
|
||||||
misskeyApi('promo/read', {
|
misskeyApi('promo/read', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitUpdReaction(emoji: string, delta: number) {
|
function emitUpdReaction(emoji: string, delta: number) {
|
||||||
|
|
|
@ -5,12 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!muted"
|
v-if="!muted && !isDeleted"
|
||||||
v-show="!isDeleted"
|
|
||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="$style.root"
|
:class="$style.root"
|
||||||
:tabindex="isDeleted ? '-1' : '0'"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div v-if="appearNote.reply && appearNote.reply.replyId">
|
<div v-if="appearNote.reply && appearNote.reply.replyId">
|
||||||
<div v-if="!conversationLoaded" style="padding: 16px">
|
<div v-if="!conversationLoaded" style="padding: 16px">
|
||||||
|
@ -110,7 +109,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
<MkPoll
|
||||||
|
v-if="appearNote.poll"
|
||||||
|
:noteId="appearNote.id"
|
||||||
|
:multiple="appearNote.poll.multiple"
|
||||||
|
:expiresAt="appearNote.poll.expiresAt"
|
||||||
|
:choices="$appearNote.pollChoices"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:emojiUrls="appearNote.emojis"
|
||||||
|
:class="$style.poll"
|
||||||
|
/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,7 +132,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" style="margin-top: 6px;" :note="appearNote"/>
|
<MkReactionsViewer
|
||||||
|
v-if="appearNote.reactionAcceptance !== 'likeOnly'"
|
||||||
|
style="margin-top: 6px;"
|
||||||
|
:reactions="$appearNote.reactions"
|
||||||
|
:reactionEmojis="$appearNote.reactionEmojis"
|
||||||
|
:myReaction="$appearNote.myReaction"
|
||||||
|
:noteId="appearNote.id"
|
||||||
|
:maxNumber="16"
|
||||||
|
@mockUpdateMyReaction="emitUpdReaction"
|
||||||
|
/>
|
||||||
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
||||||
<i class="ti ti-arrow-back-up"></i>
|
<i class="ti ti-arrow-back-up"></i>
|
||||||
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
||||||
|
@ -143,11 +160,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && $appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
|
<i v-else-if="$appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number($appearNote.reactionCount) }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
|
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
|
@ -182,9 +199,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
|
<div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
|
||||||
<div :class="$style.reactionTabs">
|
<div :class="$style.reactionTabs">
|
||||||
<button v-for="reaction in Object.keys(appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
|
<button v-for="reaction in Object.keys($appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
|
||||||
<MkReactionIcon :reaction="reaction"/>
|
<MkReactionIcon :reaction="reaction"/>
|
||||||
<span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
|
<span style="margin-left: 4px;">{{ $appearNote.reactions[reaction] }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
|
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
|
||||||
|
@ -199,7 +216,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
<div v-else-if="muted" class="_panel" :class="$style.muted" @click="muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||||
|
@ -211,13 +228,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue';
|
import { computed, inject, onMounted, provide, reactive, ref, useTemplateRef } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
|
||||||
import type { Keymap } from '@/utility/hotkey.js';
|
import type { Keymap } from '@/utility/hotkey.js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
|
@ -242,7 +258,7 @@ import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
|
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
|
||||||
import { useNoteCapture } from '@/use/use-note-capture.js';
|
import { noteEvents, useNoteCapture } from '@/use/use-note-capture.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import { useTooltip } from '@/use/use-tooltip.js';
|
import { useTooltip } from '@/use/use-tooltip.js';
|
||||||
import { claimAchievement } from '@/utility/achievements.js';
|
import { claimAchievement } from '@/utility/achievements.js';
|
||||||
|
@ -257,6 +273,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
import { globalEvents, useGlobalEvent } from '@/events.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
@ -267,29 +284,33 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
|
|
||||||
const note = ref(deepClone(props.note));
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
if (noteViewInterruptors.length > 0) {
|
if (noteViewInterruptors.length > 0) {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let result: Misskey.entities.Note | null = deepClone(note.value);
|
let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
try {
|
try {
|
||||||
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
if (result === null) {
|
|
||||||
isDeleted.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
note.value = result as Misskey.entities.Note;
|
note = result as Misskey.entities.Note;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note.value);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
|
const appearNote = getAppearNote(note);
|
||||||
|
const $appearNote = reactive({
|
||||||
|
reactions: appearNote.reactions,
|
||||||
|
reactionCount: appearNote.reactionCount,
|
||||||
|
reactionEmojis: appearNote.reactionEmojis,
|
||||||
|
myReaction: appearNote.myReaction,
|
||||||
|
pollChoices: appearNote.poll?.choices,
|
||||||
|
});
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
const menuButton = useTemplateRef('menuButton');
|
const menuButton = useTemplateRef('menuButton');
|
||||||
|
@ -297,24 +318,29 @@ const renoteButton = useTemplateRef('renoteButton');
|
||||||
const renoteTime = useTemplateRef('renoteTime');
|
const renoteTime = useTemplateRef('renoteTime');
|
||||||
const reactButton = useTemplateRef('reactButton');
|
const reactButton = useTemplateRef('reactButton');
|
||||||
const clipButton = useTemplateRef('clipButton');
|
const clipButton = useTemplateRef('clipButton');
|
||||||
const appearNote = computed(() => getAppearNote(note.value));
|
|
||||||
const galleryEl = useTemplateRef('galleryEl');
|
const galleryEl = useTemplateRef('galleryEl');
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
|
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
const parsed = appearNote.text ? mfm.parse(appearNote.text) : null;
|
||||||
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
|
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.renote?.url !== url && appearNote.renote?.uri !== url) : null;
|
||||||
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const conversation = ref<Misskey.entities.Note[]>([]);
|
const conversation = ref<Misskey.entities.Note[]>([]);
|
||||||
const replies = ref<Misskey.entities.Note[]>([]);
|
const replies = ref<Misskey.entities.Note[]>([]);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i?.id);
|
||||||
|
|
||||||
|
useGlobalEvent('noteDeleted', (noteId) => {
|
||||||
|
if (noteId === note.id || noteId === appearNote.id) {
|
||||||
|
isDeleted.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
type: 'lookup',
|
type: 'lookup',
|
||||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
url: `https://${host}/notes/${appearNote.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
|
@ -328,7 +354,7 @@ const keymap = {
|
||||||
},
|
},
|
||||||
'o': () => galleryEl.value?.openGallery(),
|
'o': () => galleryEl.value?.openGallery(),
|
||||||
'v|enter': () => {
|
'v|enter': () => {
|
||||||
if (appearNote.value.cw != null) {
|
if (appearNote.cw != null) {
|
||||||
showContent.value = !showContent.value;
|
showContent.value = !showContent.value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -341,41 +367,45 @@ const keymap = {
|
||||||
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
|
}).then(() => {
|
||||||
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
|
userId: $i!.id,
|
||||||
|
reaction: reaction,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const tab = ref(props.initialTab);
|
const tab = ref(props.initialTab);
|
||||||
const reactionTabType = ref<string | null>(null);
|
const reactionTabType = ref<string | null>(null);
|
||||||
|
|
||||||
const renotesPagination = computed<Paging>(() => ({
|
const renotesPagination = computed(() => ({
|
||||||
endpoint: 'notes/renotes',
|
endpoint: 'notes/renotes',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: {
|
params: {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const reactionsPagination = computed<Paging>(() => ({
|
const reactionsPagination = computed(() => ({
|
||||||
endpoint: 'notes/reactions',
|
endpoint: 'notes/reactions',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: {
|
params: {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
type: reactionTabType.value,
|
type: reactionTabType.value,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: rootEl,
|
|
||||||
note: appearNote,
|
note: appearNote,
|
||||||
pureNote: note,
|
parentNote: note,
|
||||||
isDeletedRef: isDeleted,
|
$note: $appearNote,
|
||||||
});
|
});
|
||||||
|
|
||||||
useTooltip(renoteButton, async (showing) => {
|
useTooltip(renoteButton, async (showing) => {
|
||||||
const renotes = await misskeyApi('notes/renotes', {
|
const renotes = await misskeyApi('notes/renotes', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 11,
|
limit: 11,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -386,19 +416,19 @@ useTooltip(renoteButton, async (showing) => {
|
||||||
const { dispose } = os.popup(MkUsersTooltip, {
|
const { dispose } = os.popup(MkUsersTooltip, {
|
||||||
showing,
|
showing,
|
||||||
users,
|
users,
|
||||||
count: appearNote.value.renoteCount,
|
count: appearNote.renoteCount,
|
||||||
targetElement: renoteButton.value,
|
targetElement: renoteButton.value,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
useTooltip(reactButton, async (showing) => {
|
useTooltip(reactButton, async (showing) => {
|
||||||
const reactions = await misskeyApiGet('notes/reactions', {
|
const reactions = await misskeyApiGet('notes/reactions', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
_cacheKey_: appearNote.value.reactionCount,
|
_cacheKey_: $appearNote.reactionCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = reactions.map(x => x.user);
|
const users = reactions.map(x => x.user);
|
||||||
|
@ -409,7 +439,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
showing,
|
showing,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
users,
|
users,
|
||||||
count: appearNote.value.reactionCount,
|
count: $appearNote.reactionCount,
|
||||||
targetElement: reactButton.value!,
|
targetElement: reactButton.value!,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -421,7 +451,7 @@ function renote() {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
const { menu } = getRenoteMenu({ note: note, renoteButton });
|
||||||
os.popupMenu(menu, renoteButton.value);
|
os.popupMenu(menu, renoteButton.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,8 +459,8 @@ function reply(): void {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.channel,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
|
@ -439,12 +469,17 @@ function reply(): void {
|
||||||
function react(): void {
|
function react(): void {
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
|
}).then(() => {
|
||||||
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
|
userId: $i!.id,
|
||||||
|
reaction: '❤️',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const el = reactButton.value;
|
const el = reactButton.value;
|
||||||
if (el && prefer.s.animation) {
|
if (el && prefer.s.animation) {
|
||||||
|
@ -457,7 +492,7 @@ function react(): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => {
|
reactionPicker.show(reactButton.value ?? null, note, async (reaction) => {
|
||||||
if (prefer.s.confirmOnReact) {
|
if (prefer.s.confirmOnReact) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
|
@ -470,10 +505,15 @@ function react(): void {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
|
reaction: reaction,
|
||||||
|
}).then(() => {
|
||||||
|
noteEvents.emit(`reacted:${appearNote.id}`, {
|
||||||
|
userId: $i!.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
|
});
|
||||||
|
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
|
||||||
claimAchievement('reactWithoutRead');
|
claimAchievement('reactWithoutRead');
|
||||||
}
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -491,10 +531,10 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReact() {
|
function toggleReact() {
|
||||||
if (appearNote.value.myReaction == null) {
|
if (appearNote.myReaction == null) {
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
undoReact(appearNote.value);
|
undoReact(appearNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,18 +546,18 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation });
|
||||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(): void {
|
function showMenu(): void {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation });
|
||||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip(): Promise<void> {
|
async function clip(): Promise<void> {
|
||||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(): void {
|
function showRenoteMenu(): void {
|
||||||
|
@ -529,9 +569,10 @@ function showRenoteMenu(): void {
|
||||||
danger: true,
|
danger: true,
|
||||||
action: () => {
|
action: () => {
|
||||||
misskeyApi('notes/delete', {
|
misskeyApi('notes/delete', {
|
||||||
noteId: note.value.id,
|
noteId: note.id,
|
||||||
|
}).then(() => {
|
||||||
|
globalEvents.emit('noteDeleted', note.id);
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
|
||||||
},
|
},
|
||||||
}], renoteTime.value);
|
}], renoteTime.value);
|
||||||
}
|
}
|
||||||
|
@ -549,7 +590,7 @@ const repliesLoaded = ref(false);
|
||||||
function loadReplies() {
|
function loadReplies() {
|
||||||
repliesLoaded.value = true;
|
repliesLoaded.value = true;
|
||||||
misskeyApi('notes/children', {
|
misskeyApi('notes/children', {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.id,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
replies.value = res;
|
replies.value = res;
|
||||||
|
@ -560,9 +601,9 @@ const conversationLoaded = ref(false);
|
||||||
|
|
||||||
function loadConversation() {
|
function loadConversation() {
|
||||||
conversationLoaded.value = true;
|
conversationLoaded.value = true;
|
||||||
if (appearNote.value.replyId == null) return;
|
if (appearNote.replyId == null) return;
|
||||||
misskeyApi('notes/conversation', {
|
misskeyApi('notes/conversation', {
|
||||||
noteId: appearNote.value.replyId,
|
noteId: appearNote.replyId,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
conversation.value = res.reverse();
|
conversation.value = res.reverse();
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,13 +4,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
|
<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad" :pullToRefresh="pullToRefresh">
|
||||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
||||||
|
|
||||||
<template #default="{ items: notes }">
|
<template #default="{ items: notes }">
|
||||||
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: pagination.reversed }]">
|
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]">
|
||||||
<template v-for="(note, i) in notes" :key="note.id">
|
<template v-for="(note, i) in notes" :key="note.id">
|
||||||
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
|
<div v-if="i > 0 && isSeparatorNeeded(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
|
||||||
|
<div :class="$style.date">
|
||||||
|
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
|
||||||
|
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||||
|
<span>{{ getSeparatorInfo(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||||
|
</div>
|
||||||
|
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
|
||||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||||
<div :class="$style.ad">
|
<div :class="$style.ad">
|
||||||
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
||||||
|
@ -25,30 +33,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useTemplateRef } from 'vue';
|
import { useTemplateRef } from 'vue';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import MkNote from '@/components/MkNote.vue';
|
import MkNote from '@/components/MkNote.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { globalEvents, useGlobalEvent } from '@/events.js';
|
||||||
|
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
pagination: Paging;
|
pagination: PagingCtx;
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
disableAutoLoad?: boolean;
|
disableAutoLoad?: boolean;
|
||||||
}>();
|
pullToRefresh?: boolean;
|
||||||
|
}>(), {
|
||||||
|
pullToRefresh: true,
|
||||||
|
});
|
||||||
|
|
||||||
const pagingComponent = useTemplateRef('pagingComponent');
|
const pagingComponent = useTemplateRef('pagingComponent');
|
||||||
|
|
||||||
|
useGlobalEvent('noteDeleted', (noteId) => {
|
||||||
|
pagingComponent.value?.paginator.removeItem(noteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
return pagingComponent.value?.paginator.reload();
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
pagingComponent,
|
reload,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.reverse {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
|
|
||||||
|
@ -77,6 +93,18 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: flex;
|
||||||
|
font-size: 85%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
opacity: 0.75;
|
||||||
|
padding: 8px 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
}
|
||||||
|
|
||||||
.ad:empty {
|
.ad:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
|
@ -1,142 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reload()">
|
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
|
||||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotifications"/></template>
|
|
||||||
|
|
||||||
<template #default="{ items: notifications }">
|
|
||||||
<component
|
|
||||||
:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]"
|
|
||||||
:enterActiveClass="$style.transition_x_enterActive"
|
|
||||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
|
||||||
:enterFromClass="$style.transition_x_enterFrom"
|
|
||||||
:leaveToClass="$style.transition_x_leaveTo"
|
|
||||||
:moveClass=" $style.transition_x_move"
|
|
||||||
tag="div"
|
|
||||||
>
|
|
||||||
<template v-for="(notification, i) in notifications" :key="notification.id">
|
|
||||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true" :data-scroll-anchor="notification.id"/>
|
|
||||||
<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true" :data-scroll-anchor="notification.id"/>
|
|
||||||
</template>
|
|
||||||
</component>
|
|
||||||
</template>
|
|
||||||
</MkPagination>
|
|
||||||
</component>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue';
|
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import type { notificationTypes } from '@@/js/const.js';
|
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
|
||||||
import XNotification from '@/components/MkNotification.vue';
|
|
||||||
import MkNote from '@/components/MkNote.vue';
|
|
||||||
import { useStream } from '@/stream.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
|
||||||
import { prefer } from '@/preferences.js';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
excludeTypes?: typeof notificationTypes[number][];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const pagingComponent = useTemplateRef('pagingComponent');
|
|
||||||
|
|
||||||
const pagination = computed(() => prefer.r.useGroupedNotifications.value ? {
|
|
||||||
endpoint: 'i/notifications-grouped' as const,
|
|
||||||
limit: 20,
|
|
||||||
params: computed(() => ({
|
|
||||||
excludeTypes: props.excludeTypes ?? undefined,
|
|
||||||
})),
|
|
||||||
} : {
|
|
||||||
endpoint: 'i/notifications' as const,
|
|
||||||
limit: 20,
|
|
||||||
params: computed(() => ({
|
|
||||||
excludeTypes: props.excludeTypes ?? undefined,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
function onNotification(notification) {
|
|
||||||
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
|
|
||||||
if (isMuted || window.document.visibilityState === 'visible') {
|
|
||||||
useStream().send('readNotification');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMuted) {
|
|
||||||
pagingComponent.value?.prepend(notification);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload() {
|
|
||||||
return new Promise<void>((res) => {
|
|
||||||
pagingComponent.value?.reload().then(() => {
|
|
||||||
res();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let connection: Misskey.ChannelConnection<Misskey.Channels['main']>;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
connection = useStream().useChannel('main');
|
|
||||||
connection.on('notification', onNotification);
|
|
||||||
connection.on('notificationFlushed', reload);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (connection) connection.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
reload,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.transition_x_move {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition_x_enterActive {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
|
|
||||||
&.item,
|
|
||||||
.item {
|
|
||||||
/* Skip Note Rendering有効時、TransitionGroupで通知を追加するときに一瞬がくっとなる問題を抑制する */
|
|
||||||
content-visibility: visible !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition_x_leaveActive {
|
|
||||||
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition_x_enterFrom {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(max(-64px, -100%));
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (interpolate-size: allow-keywords) {
|
|
||||||
.transition_x_enterFrom {
|
|
||||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition_x_leaveTo {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notifications {
|
|
||||||
container-type: inline-size;
|
|
||||||
background: var(--MI_THEME-panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -4,483 +4,74 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<component :is="prefer.s.enablePullToRefresh && pullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => paginator.reload()">
|
||||||
<Transition
|
<Transition
|
||||||
:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
|
:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
|
||||||
:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
|
:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
|
||||||
:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
|
:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
|
||||||
:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
|
:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
|
||||||
|
:css="prefer.s.animation"
|
||||||
mode="out-in"
|
mode="out-in"
|
||||||
>
|
>
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="paginator.fetching.value"/>
|
||||||
|
|
||||||
<MkError v-else-if="error" @retry="init()"/>
|
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
||||||
|
|
||||||
<div v-else-if="empty" key="_empty_">
|
<div v-else-if="paginator.items.value.length === 0" key="_empty_">
|
||||||
<slot name="empty"><MkResult type="empty"/></slot>
|
<slot name="empty"><MkResult type="empty"/></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else ref="rootEl" class="_gaps">
|
<div v-else ref="rootEl" class="_gaps">
|
||||||
<div v-show="pagination.reversed && more" key="_more_">
|
<div v-show="pagination.reversed && paginator.canFetchOlder.value" key="_more_">
|
||||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
|
<MkButton v-if="!paginator.fetchingOlder.value" v-appear="(prefer.s.enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="paginator.fetchingOlder.value" primary rounded @click="paginator.fetchNewer">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else/>
|
<MkLoading v-else/>
|
||||||
</div>
|
</div>
|
||||||
<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
|
<slot :items="paginator.items.value" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
|
||||||
<div v-show="!pagination.reversed && more" key="_more_">
|
<div v-show="!pagination.reversed && paginator.canFetchOlder.value" key="_more_">
|
||||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
|
<MkButton v-if="!paginator.fetchingOlder.value" v-appear="(prefer.s.enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="paginator.fetchingOlder.value" primary rounded @click="paginator.fetchOlder">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else/>
|
<MkLoading v-else/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch } from 'vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import * as Misskey from 'misskey-js';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
|
|
||||||
import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible } from '@@/js/scroll.js';
|
|
||||||
import type { ComputedRef } from 'vue';
|
|
||||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
import { usePagination } from '@/use/use-pagination.js';
|
||||||
const SECOND_FETCH_LIMIT = 30;
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
const TOLERANCE = 16;
|
|
||||||
const APPEAR_MINIMUM_INTERVAL = 600;
|
|
||||||
|
|
||||||
export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> = {
|
|
||||||
endpoint: E;
|
|
||||||
limit: number;
|
|
||||||
params?: Misskey.Endpoints[E]['req'] | ComputedRef<Misskey.Endpoints[E]['req']>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 検索APIのような、ページング不可なエンドポイントを利用する場合
|
|
||||||
* (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
|
|
||||||
*/
|
|
||||||
noPaging?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* items 配列の中身を逆順にする(新しい方が最後)
|
|
||||||
*/
|
|
||||||
reversed?: boolean;
|
|
||||||
|
|
||||||
offsetMode?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MisskeyEntityMap = Map<string, MisskeyEntity>;
|
|
||||||
|
|
||||||
function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
|
|
||||||
return entities.map(en => [en.id, en]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap {
|
|
||||||
return new Map([...map, ...arrayToEntries(entities)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
pagination: Paging;
|
pagination: PagingCtx;
|
||||||
disableAutoLoad?: boolean;
|
disableAutoLoad?: boolean;
|
||||||
displayLimit?: number;
|
displayLimit?: number;
|
||||||
|
pullToRefresh?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
displayLimit: 20,
|
displayLimit: 20,
|
||||||
|
pullToRefresh: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const paginator = usePagination({
|
||||||
(ev: 'queue', count: number): void;
|
ctx: props.pagination,
|
||||||
(ev: 'status', error: boolean): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
|
||||||
|
|
||||||
// 遡り中かどうか
|
|
||||||
const backed = ref(false);
|
|
||||||
|
|
||||||
const scrollRemove = ref<(() => void) | null>(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表示するアイテムのソース
|
|
||||||
* 最新が0番目
|
|
||||||
*/
|
|
||||||
const items = ref<MisskeyEntityMap>(new Map());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* タブが非アクティブなどの場合に更新を貯めておく
|
|
||||||
* 最新が0番目
|
|
||||||
*/
|
|
||||||
const queue = ref<MisskeyEntityMap>(new Map());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初期化中かどうか(trueならMkLoadingで全て隠す)
|
|
||||||
*/
|
|
||||||
const fetching = ref(true);
|
|
||||||
|
|
||||||
const moreFetching = ref(false);
|
|
||||||
const more = ref(false);
|
|
||||||
const preventAppearFetchMore = ref(false);
|
|
||||||
const preventAppearFetchMoreTimer = ref<number | null>(null);
|
|
||||||
const isBackTop = ref(false);
|
|
||||||
const empty = computed(() => items.value.size === 0);
|
|
||||||
const error = ref(false);
|
|
||||||
const {
|
|
||||||
enableInfiniteScroll,
|
|
||||||
} = prefer.r;
|
|
||||||
|
|
||||||
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body);
|
|
||||||
|
|
||||||
const visibility = useDocumentVisibility();
|
|
||||||
|
|
||||||
let isPausingUpdate = false;
|
|
||||||
let timerForSetPause: number | null = null;
|
|
||||||
const BACKGROUND_PAUSE_WAIT_SEC = 10;
|
|
||||||
|
|
||||||
// 先頭が表示されているかどうかを検出
|
|
||||||
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
|
|
||||||
const scrollObserver = ref<IntersectionObserver>();
|
|
||||||
|
|
||||||
watch([() => props.pagination.reversed, scrollableElement], () => {
|
|
||||||
if (scrollObserver.value) scrollObserver.value.disconnect();
|
|
||||||
|
|
||||||
scrollObserver.value = new IntersectionObserver(entries => {
|
|
||||||
backed.value = entries[0].isIntersecting;
|
|
||||||
}, {
|
|
||||||
root: scrollableElement.value,
|
|
||||||
rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
|
|
||||||
threshold: 0.01,
|
|
||||||
});
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
watch(rootEl, () => {
|
|
||||||
scrollObserver.value?.disconnect();
|
|
||||||
nextTick(() => {
|
|
||||||
if (rootEl.value) scrollObserver.value?.observe(rootEl.value);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch([backed, rootEl], () => {
|
function appearFetchMoreAhead() {
|
||||||
if (!backed.value) {
|
paginator.fetchNewer();
|
||||||
if (!rootEl.value) return;
|
|
||||||
|
|
||||||
scrollRemove.value = props.pagination.reversed
|
|
||||||
? onScrollBottom(rootEl.value, executeQueue, TOLERANCE)
|
|
||||||
: onScrollTop(rootEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE);
|
|
||||||
} else {
|
|
||||||
if (scrollRemove.value) scrollRemove.value();
|
|
||||||
scrollRemove.value = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
|
|
||||||
watch(() => [props.pagination.endpoint, props.pagination.params], init, { deep: true });
|
|
||||||
|
|
||||||
watch(queue, (a, b) => {
|
|
||||||
if (a.size === 0 && b.size === 0) return;
|
|
||||||
emit('queue', queue.value.size);
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
watch(error, (n, o) => {
|
|
||||||
if (n === o) return;
|
|
||||||
emit('status', n);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
|
||||||
items.value = new Map();
|
|
||||||
queue.value = new Map();
|
|
||||||
fetching.value = true;
|
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
|
||||||
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
|
||||||
...params,
|
|
||||||
limit: props.pagination.limit ?? 10,
|
|
||||||
allowPartial: true,
|
|
||||||
}).then(res => {
|
|
||||||
for (let i = 0; i < res.length; i++) {
|
|
||||||
const item = res[i];
|
|
||||||
if (i === 3) item._shouldInsertAd_ = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.length === 0 || props.pagination.noPaging) {
|
function appearFetchMore() {
|
||||||
concatItems(res);
|
paginator.fetchOlder();
|
||||||
more.value = false;
|
|
||||||
} else {
|
|
||||||
if (props.pagination.reversed) moreFetching.value = true;
|
|
||||||
concatItems(res);
|
|
||||||
more.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
error.value = false;
|
|
||||||
fetching.value = false;
|
|
||||||
}, err => {
|
|
||||||
error.value = true;
|
|
||||||
fetching.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = (): Promise<void> => {
|
|
||||||
return init();
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMore = async (): Promise<void> => {
|
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
|
||||||
moreFetching.value = true;
|
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
|
||||||
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
|
||||||
...params,
|
|
||||||
limit: SECOND_FETCH_LIMIT,
|
|
||||||
...(props.pagination.offsetMode ? {
|
|
||||||
offset: items.value.size,
|
|
||||||
} : {
|
|
||||||
untilId: Array.from(items.value.keys()).at(-1),
|
|
||||||
}),
|
|
||||||
}).then(res => {
|
|
||||||
for (let i = 0; i < res.length; i++) {
|
|
||||||
const item = res[i];
|
|
||||||
if (i === 10) item._shouldInsertAd_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reverseConcat = _res => {
|
|
||||||
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
|
|
||||||
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
|
|
||||||
|
|
||||||
items.value = concatMapWithArray(items.value, _res);
|
|
||||||
|
|
||||||
return nextTick(() => {
|
|
||||||
if (scrollableElement.value) {
|
|
||||||
scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
|
|
||||||
} else {
|
|
||||||
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextTick();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (res.length === 0) {
|
|
||||||
if (props.pagination.reversed) {
|
|
||||||
reverseConcat(res).then(() => {
|
|
||||||
more.value = false;
|
|
||||||
moreFetching.value = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
items.value = concatMapWithArray(items.value, res);
|
|
||||||
more.value = false;
|
|
||||||
moreFetching.value = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (props.pagination.reversed) {
|
|
||||||
reverseConcat(res).then(() => {
|
|
||||||
more.value = true;
|
|
||||||
moreFetching.value = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
items.value = concatMapWithArray(items.value, res);
|
|
||||||
more.value = true;
|
|
||||||
moreFetching.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, err => {
|
|
||||||
moreFetching.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMoreAhead = async (): Promise<void> => {
|
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
|
||||||
moreFetching.value = true;
|
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
|
||||||
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
|
||||||
...params,
|
|
||||||
limit: SECOND_FETCH_LIMIT,
|
|
||||||
...(props.pagination.offsetMode ? {
|
|
||||||
offset: items.value.size,
|
|
||||||
} : {
|
|
||||||
sinceId: Array.from(items.value.keys()).at(-1),
|
|
||||||
}),
|
|
||||||
}).then(res => {
|
|
||||||
if (res.length === 0) {
|
|
||||||
items.value = concatMapWithArray(items.value, res);
|
|
||||||
more.value = false;
|
|
||||||
} else {
|
|
||||||
items.value = concatMapWithArray(items.value, res);
|
|
||||||
more.value = true;
|
|
||||||
}
|
|
||||||
moreFetching.value = false;
|
|
||||||
}, err => {
|
|
||||||
moreFetching.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appear(IntersectionObserver)によってfetchMoreが呼ばれる場合、
|
|
||||||
* APPEAR_MINIMUM_INTERVALミリ秒以内に2回fetchMoreが呼ばれるのを防ぐ
|
|
||||||
*/
|
|
||||||
const fetchMoreApperTimeoutFn = (): void => {
|
|
||||||
preventAppearFetchMore.value = false;
|
|
||||||
preventAppearFetchMoreTimer.value = null;
|
|
||||||
};
|
|
||||||
const fetchMoreAppearTimeout = (): void => {
|
|
||||||
preventAppearFetchMore.value = true;
|
|
||||||
preventAppearFetchMoreTimer.value = window.setTimeout(fetchMoreApperTimeoutFn, APPEAR_MINIMUM_INTERVAL);
|
|
||||||
};
|
|
||||||
|
|
||||||
const appearFetchMore = async (): Promise<void> => {
|
|
||||||
if (preventAppearFetchMore.value) return;
|
|
||||||
await fetchMore();
|
|
||||||
fetchMoreAppearTimeout();
|
|
||||||
};
|
|
||||||
|
|
||||||
const appearFetchMoreAhead = async (): Promise<void> => {
|
|
||||||
if (preventAppearFetchMore.value) return;
|
|
||||||
await fetchMoreAhead();
|
|
||||||
fetchMoreAppearTimeout();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isHead = (): boolean => isBackTop.value || (props.pagination.reversed ? isTailVisible : isHeadVisible)(rootEl.value!, TOLERANCE);
|
|
||||||
|
|
||||||
watch(visibility, () => {
|
|
||||||
if (visibility.value === 'hidden') {
|
|
||||||
timerForSetPause = window.setTimeout(() => {
|
|
||||||
isPausingUpdate = true;
|
|
||||||
timerForSetPause = null;
|
|
||||||
},
|
|
||||||
BACKGROUND_PAUSE_WAIT_SEC * 1000);
|
|
||||||
} else { // 'visible'
|
|
||||||
if (timerForSetPause) {
|
|
||||||
window.clearTimeout(timerForSetPause);
|
|
||||||
timerForSetPause = null;
|
|
||||||
} else {
|
|
||||||
isPausingUpdate = false;
|
|
||||||
if (isHead()) {
|
|
||||||
executeQueue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 最新のものとして1つだけアイテムを追加する
|
|
||||||
* ストリーミングから降ってきたアイテムはこれで追加する
|
|
||||||
* @param item アイテム
|
|
||||||
*/
|
|
||||||
function prepend(item: MisskeyEntity): void {
|
|
||||||
if (items.value.size === 0) {
|
|
||||||
items.value.set(item.id, item);
|
|
||||||
fetching.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_DEV_) console.log(isHead(), isPausingUpdate);
|
|
||||||
|
|
||||||
if (isHead() && !isPausingUpdate) unshiftItems([item]);
|
|
||||||
else prependQueue(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新着アイテムをitemsの先頭に追加し、displayLimitを適用する
|
|
||||||
* @param newItems 新しいアイテムの配列
|
|
||||||
*/
|
|
||||||
function unshiftItems(newItems: MisskeyEntity[]) {
|
|
||||||
const length = newItems.length + items.value.size;
|
|
||||||
items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit));
|
|
||||||
|
|
||||||
if (length >= props.displayLimit) more.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 古いアイテムをitemsの末尾に追加し、displayLimitを適用する
|
|
||||||
* @param oldItems 古いアイテムの配列
|
|
||||||
*/
|
|
||||||
function concatItems(oldItems: MisskeyEntity[]) {
|
|
||||||
const length = oldItems.length + items.value.size;
|
|
||||||
items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, props.displayLimit));
|
|
||||||
|
|
||||||
if (length >= props.displayLimit) more.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeQueue() {
|
|
||||||
unshiftItems(Array.from(queue.value.values()));
|
|
||||||
queue.value = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
function prependQueue(newItem: MisskeyEntity) {
|
|
||||||
queue.value = new Map([[newItem.id, newItem], ...queue.value].slice(0, props.displayLimit) as [string, MisskeyEntity][]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* アイテムを末尾に追加する(使うの?)
|
|
||||||
*/
|
|
||||||
const appendItem = (item: MisskeyEntity): void => {
|
|
||||||
items.value.set(item.id, item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeItem = (id: string) => {
|
|
||||||
items.value.delete(id);
|
|
||||||
queue.value.delete(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => {
|
|
||||||
const item = items.value.get(id);
|
|
||||||
if (item) items.value.set(id, replacer(item));
|
|
||||||
|
|
||||||
const queueItem = queue.value.get(id);
|
|
||||||
if (queueItem) queue.value.set(id, replacer(queueItem));
|
|
||||||
};
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
isBackTop.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
onDeactivated(() => {
|
|
||||||
isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
function toBottom() {
|
|
||||||
scrollToBottom(rootEl.value!);
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
init().then(() => {
|
|
||||||
if (props.pagination.reversed) {
|
|
||||||
nextTick(() => {
|
|
||||||
window.setTimeout(toBottom, 800);
|
|
||||||
|
|
||||||
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
|
||||||
// more = trueを遅らせる
|
|
||||||
window.setTimeout(() => {
|
|
||||||
moreFetching.value = false;
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (timerForSetPause) {
|
|
||||||
window.clearTimeout(timerForSetPause);
|
|
||||||
timerForSetPause = null;
|
|
||||||
}
|
|
||||||
if (preventAppearFetchMoreTimer.value) {
|
|
||||||
window.clearTimeout(preventAppearFetchMoreTimer.value);
|
|
||||||
preventAppearFetchMoreTimer.value = null;
|
|
||||||
}
|
|
||||||
scrollObserver.value?.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
items,
|
paginator: paginator,
|
||||||
queue,
|
|
||||||
backed: backed.value,
|
|
||||||
more,
|
|
||||||
reload,
|
|
||||||
prepend,
|
|
||||||
append: appendItem,
|
|
||||||
removeItem,
|
|
||||||
updateItem,
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ [$style.done]: closed || isVoted }">
|
<div :class="{ [$style.done]: closed || isVoted }">
|
||||||
<ul :class="$style.choices">
|
<ul :class="$style.choices">
|
||||||
<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
|
<li v-for="(choice, i) in choices" :key="i" :class="$style.choice" @click="vote(i)">
|
||||||
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||||
<span :class="$style.fg">
|
<span :class="$style.fg">
|
||||||
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
|
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
|
||||||
|
@ -40,7 +40,9 @@ import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
poll: NonNullable<Misskey.entities.Note['poll']>;
|
multiple: NonNullable<Misskey.entities.Note['poll']>['multiple'];
|
||||||
|
expiresAt: NonNullable<Misskey.entities.Note['poll']>['expiresAt'];
|
||||||
|
choices: NonNullable<Misskey.entities.Note['poll']>['choices'];
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
emojiUrls?: Record<string, string>;
|
emojiUrls?: Record<string, string>;
|
||||||
author?: Misskey.entities.UserLite;
|
author?: Misskey.entities.UserLite;
|
||||||
|
@ -48,9 +50,9 @@ const props = defineProps<{
|
||||||
|
|
||||||
const remaining = ref(-1);
|
const remaining = ref(-1);
|
||||||
|
|
||||||
const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
|
const total = computed(() => sum(props.choices.map(x => x.votes)));
|
||||||
const closed = computed(() => remaining.value === 0);
|
const closed = computed(() => remaining.value === 0);
|
||||||
const isVoted = computed(() => !props.poll.multiple && props.poll.choices.some(c => c.isVoted));
|
const isVoted = computed(() => !props.multiple && props.choices.some(c => c.isVoted));
|
||||||
const timer = computed(() => i18n.tsx._poll[
|
const timer = computed(() => i18n.tsx._poll[
|
||||||
remaining.value >= 86400 ? 'remainingDays' :
|
remaining.value >= 86400 ? 'remainingDays' :
|
||||||
remaining.value >= 3600 ? 'remainingHours' :
|
remaining.value >= 3600 ? 'remainingHours' :
|
||||||
|
@ -70,9 +72,9 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 期限付きアンケート
|
// 期限付きアンケート
|
||||||
if (props.poll.expiresAt) {
|
if (props.expiresAt) {
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
remaining.value = Math.floor(Math.max(new Date(props.poll.expiresAt!).getTime() - Date.now(), 0) / 1000);
|
remaining.value = Math.floor(Math.max(new Date(props.expiresAt!).getTime() - Date.now(), 0) / 1000);
|
||||||
if (remaining.value === 0) {
|
if (remaining.value === 0) {
|
||||||
showResult.value = true;
|
showResult.value = true;
|
||||||
}
|
}
|
||||||
|
@ -91,7 +93,7 @@ const vote = async (id) => {
|
||||||
|
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
|
text: i18n.tsx.voteConfirm({ choice: props.choices[id].text }),
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
|
@ -99,7 +101,7 @@ const vote = async (id) => {
|
||||||
noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
choice: id,
|
choice: id,
|
||||||
});
|
});
|
||||||
if (!showResult.value) showResult.value = !props.poll.multiple;
|
if (!showResult.value) showResult.value = !props.multiple;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,7 @@ import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { getPluginHandlers } from '@/plugin.js';
|
import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -883,12 +884,15 @@ async function post(ev?: MouseEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
posting.value = true;
|
posting.value = true;
|
||||||
misskeyApi('notes/create', postData, token).then(() => {
|
misskeyApi('notes/create', postData, token).then((res) => {
|
||||||
if (props.freezeAfterPosted) {
|
if (props.freezeAfterPosted) {
|
||||||
posted.value = true;
|
posted.value = true;
|
||||||
} else {
|
} else {
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalEvents.emit('notePosted', res.createdNote);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
deleteDraft();
|
deleteDraft();
|
||||||
emit('posted');
|
emit('posted');
|
||||||
|
|
|
@ -48,7 +48,8 @@ function toggle(): void {
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
|
@ -102,7 +103,8 @@ function toggle(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
position: absolute;
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -126,7 +128,7 @@ function toggle(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-left: 28px;
|
margin-left: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -5,14 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, h, ref, watch } from 'vue';
|
import { defineComponent, h, ref, watch } from 'vue';
|
||||||
import type { VNode } from 'vue';
|
|
||||||
import MkRadio from './MkRadio.vue';
|
import MkRadio from './MkRadio.vue';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
vertical: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const value = ref(props.modelValue);
|
const value = ref(props.modelValue);
|
||||||
|
@ -34,7 +38,10 @@ export default defineComponent({
|
||||||
options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if'));
|
options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if'));
|
||||||
|
|
||||||
return () => h('div', {
|
return () => h('div', {
|
||||||
class: 'novjtcto',
|
class: [
|
||||||
|
'novjtcto',
|
||||||
|
...(props.vertical ? ['vertical'] : []),
|
||||||
|
],
|
||||||
}, [
|
}, [
|
||||||
...(label ? [h('div', {
|
...(label ? [h('div', {
|
||||||
class: 'label',
|
class: 'label',
|
||||||
|
@ -71,7 +78,7 @@ export default defineComponent({
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,5 +91,11 @@ export default defineComponent({
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
> .body {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<slot name="label"></slot>
|
<slot name="label"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div v-adaptive-border class="body">
|
<div v-adaptive-border class="body">
|
||||||
|
<slot name="prefix"></slot>
|
||||||
<div ref="containerEl" class="container">
|
<div ref="containerEl" class="container">
|
||||||
<div class="track">
|
<div class="track">
|
||||||
<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
|
<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
|
||||||
|
@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@touchstart="onMousedown"
|
@touchstart="onMousedown"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="caption">
|
<div class="caption">
|
||||||
<slot name="caption"></slot>
|
<slot name="caption"></slot>
|
||||||
|
@ -224,12 +226,17 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
$thumbWidth: 20px;
|
$thumbWidth: 20px;
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border: solid 1px var(--MI_THEME-panel);
|
border: solid 1px var(--MI_THEME-panel);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
> .container {
|
> .container {
|
||||||
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: $thumbHeight;
|
height: $thumbHeight;
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="buttonEl"
|
ref="buttonEl"
|
||||||
v-ripple="canToggle"
|
v-ripple="canToggle"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
|
:class="[$style.root, { [$style.reacted]: myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
@contextmenu.prevent.stop="menu"
|
@contextmenu.prevent.stop="menu"
|
||||||
>
|
>
|
||||||
<MkReactionIcon style="pointer-events: none;" :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
<MkReactionIcon style="pointer-events: none;" :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||||
<span :class="$style.count">{{ count }}</span>
|
<span :class="$style.count">{{ count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -29,19 +29,21 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
import { useTooltip } from '@/use/use-tooltip.js';
|
import { useTooltip } from '@/use/use-tooltip.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||||
import { claimAchievement } from '@/utility/achievements.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as sound from '@/utility/sound.js';
|
import * as sound from '@/utility/sound.js';
|
||||||
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
|
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
|
||||||
import { customEmojisMap } from '@/custom-emojis.js';
|
import { customEmojisMap } from '@/custom-emojis.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
import { noteEvents } from '@/use/use-note-capture.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
noteId: Misskey.entities.Note['id'];
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
reactionEmojis: Misskey.entities.Note['reactionEmojis'];
|
||||||
|
myReaction: Misskey.entities.Note['myReaction'];
|
||||||
count: number;
|
count: number;
|
||||||
isInitial: boolean;
|
isInitial: boolean;
|
||||||
note: Misskey.entities.Note;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const mock = inject(DI.mock, false);
|
const mock = inject(DI.mock, false);
|
||||||
|
@ -56,14 +58,16 @@ const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./,
|
||||||
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
|
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
|
||||||
|
|
||||||
const canToggle = computed(() => {
|
const canToggle = computed(() => {
|
||||||
return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
|
// TODO
|
||||||
|
//return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
|
||||||
|
return !props.reaction.match(/@\w/) && $i && emoji.value;
|
||||||
});
|
});
|
||||||
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||||
|
|
||||||
async function toggleReaction() {
|
async function toggleReaction() {
|
||||||
if (!canToggle.value) return;
|
if (!canToggle.value) return;
|
||||||
|
|
||||||
const oldReaction = props.note.myReaction;
|
const oldReaction = props.myReaction;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -81,12 +85,23 @@ async function toggleReaction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/delete', {
|
misskeyApi('notes/reactions/delete', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
noteEvents.emit(`unreacted:${props.noteId}`, {
|
||||||
|
userId: $i!.id,
|
||||||
|
reaction: props.reaction,
|
||||||
|
emoji: emoji.value,
|
||||||
|
});
|
||||||
if (oldReaction !== props.reaction) {
|
if (oldReaction !== props.reaction) {
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
|
}).then(() => {
|
||||||
|
noteEvents.emit(`reacted:${props.noteId}`, {
|
||||||
|
userId: $i!.id,
|
||||||
|
reaction: props.reaction,
|
||||||
|
emoji: emoji.value,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -108,12 +123,19 @@ async function toggleReaction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
|
}).then(() => {
|
||||||
|
noteEvents.emit(`reacted:${props.noteId}`, {
|
||||||
|
userId: $i!.id,
|
||||||
|
reaction: props.reaction,
|
||||||
|
emoji: emoji.value,
|
||||||
});
|
});
|
||||||
if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
|
});
|
||||||
claimAchievement('reactWithoutRead');
|
// TODO: 上位コンポーネントでやる
|
||||||
}
|
//if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
|
||||||
|
// claimAchievement('reactWithoutRead');
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +179,7 @@ onMounted(() => {
|
||||||
if (!mock) {
|
if (!mock) {
|
||||||
useTooltip(buttonEl, async (showing) => {
|
useTooltip(buttonEl, async (showing) => {
|
||||||
const reactions = await misskeyApiGet('notes/reactions', {
|
const reactions = await misskeyApiGet('notes/reactions', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
type: props.reaction,
|
type: props.reaction,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
_cacheKey_: props.count,
|
_cacheKey_: props.count,
|
||||||
|
|
|
@ -13,7 +13,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:moveClass="$style.transition_x_move"
|
:moveClass="$style.transition_x_move"
|
||||||
tag="div" :class="$style.root"
|
tag="div" :class="$style.root"
|
||||||
>
|
>
|
||||||
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
|
<XReaction
|
||||||
|
v-for="[reaction, count] in _reactions"
|
||||||
|
:key="reaction"
|
||||||
|
:reaction="reaction"
|
||||||
|
:reactionEmojis="props.reactionEmojis"
|
||||||
|
:count="count"
|
||||||
|
:isInitial="initialReactions.has(reaction)"
|
||||||
|
:noteId="props.noteId"
|
||||||
|
:myReaction="props.myReaction"
|
||||||
|
@reactionToggled="onMockToggleReaction"
|
||||||
|
/>
|
||||||
<slot v-if="hasMoreReactions" name="more"/>
|
<slot v-if="hasMoreReactions" name="more"/>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
@ -27,7 +37,10 @@ import { prefer } from '@/preferences.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
noteId: Misskey.entities.Note['id'];
|
||||||
|
reactions: Misskey.entities.Note['reactions'];
|
||||||
|
reactionEmojis: Misskey.entities.Note['reactionEmojis'];
|
||||||
|
myReaction: Misskey.entities.Note['myReaction'];
|
||||||
maxNumber?: number;
|
maxNumber?: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
maxNumber: Infinity,
|
maxNumber: Infinity,
|
||||||
|
@ -39,33 +52,33 @@ const emit = defineEmits<{
|
||||||
(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
|
(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const initialReactions = new Set(Object.keys(props.note.reactions));
|
const initialReactions = new Set(Object.keys(props.reactions));
|
||||||
|
|
||||||
const reactions = ref<[string, number][]>([]);
|
const _reactions = ref<[string, number][]>([]);
|
||||||
const hasMoreReactions = ref(false);
|
const hasMoreReactions = ref(false);
|
||||||
|
|
||||||
if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
|
if (props.myReaction && !Object.keys(_reactions.value).includes(props.myReaction)) {
|
||||||
reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
|
_reactions.value[props.myReaction] = props.reactions[props.myReaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMockToggleReaction(emoji: string, count: number) {
|
function onMockToggleReaction(emoji: string, count: number) {
|
||||||
if (!mock) return;
|
if (!mock) return;
|
||||||
|
|
||||||
const i = reactions.value.findIndex((item) => item[0] === emoji);
|
const i = _reactions.value.findIndex((item) => item[0] === emoji);
|
||||||
if (i < 0) return;
|
if (i < 0) return;
|
||||||
|
|
||||||
emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1]));
|
emit('mockUpdateMyReaction', emoji, (count - _reactions.value[i][1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
||||||
let newReactions: [string, number][] = [];
|
let newReactions: [string, number][] = [];
|
||||||
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
||||||
|
|
||||||
for (let i = 0; i < reactions.value.length; i++) {
|
for (let i = 0; i < _reactions.value.length; i++) {
|
||||||
const reaction = reactions.value[i][0];
|
const reaction = _reactions.value[i][0];
|
||||||
if (reaction in newSource && newSource[reaction] !== 0) {
|
if (reaction in newSource && newSource[reaction] !== 0) {
|
||||||
reactions.value[i][1] = newSource[reaction];
|
_reactions.value[i][1] = newSource[reaction];
|
||||||
newReactions.push(reactions.value[i]);
|
newReactions.push(_reactions.value[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,11 +92,11 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
|
||||||
|
|
||||||
newReactions = newReactions.slice(0, props.maxNumber);
|
newReactions = newReactions.slice(0, props.maxNumber);
|
||||||
|
|
||||||
if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
|
if (props.myReaction && !newReactions.map(([x]) => x).includes(props.myReaction)) {
|
||||||
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
|
newReactions.push([props.myReaction, newSource[props.myReaction]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reactions.value = newReactions;
|
_reactions.value = newReactions;
|
||||||
}, { immediate: true, deep: true });
|
}, { immediate: true, deep: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, useTemplateRef } from 'vue';
|
||||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
@ -81,7 +81,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void
|
(ev: 'closed'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
const windowEl = useTemplateRef('windowEl');
|
||||||
|
|
||||||
const name = computed(() => props.emoji.name);
|
const name = computed(() => props.emoji.name);
|
||||||
const host = computed(() => props.emoji.host);
|
const host = computed(() => props.emoji.host);
|
||||||
|
|
|
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, toRefs } from 'vue';
|
import { computed, ref, toRefs, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -74,7 +74,7 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const { initialRoleIds, infoMessage, title, publicOnly } = toRefs(props);
|
const { initialRoleIds, infoMessage, title, publicOnly } = toRefs(props);
|
||||||
|
|
||||||
const windowEl = ref<InstanceType<typeof MkModalWindow>>();
|
const windowEl = useTemplateRef('windowEl');
|
||||||
const roles = ref<Misskey.entities.Role[]>([]);
|
const roles = ref<Misskey.entities.Role[]>([]);
|
||||||
const selectedRoleIds = ref<string[]>(initialRoleIds.value ?? []);
|
const selectedRoleIds = ref<string[]>(initialRoleIds.value ?? []);
|
||||||
const fetching = ref(false);
|
const fetching = ref(false);
|
||||||
|
|
|
@ -0,0 +1,356 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.root" class="_gaps_m">
|
||||||
|
<MkInput v-model="q_name" data-cy-server-name>
|
||||||
|
<template #label>{{ i18n.ts.instanceName }}</template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
|
<MkFolder :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._serverSetupWizard.howWillYouUseMisskey }}</template>
|
||||||
|
<template #icon><i class="ti ti-settings-question"></i></template>
|
||||||
|
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkRadios v-model="q_use" :vertical="true">
|
||||||
|
<option value="single">
|
||||||
|
<div><i class="ti ti-user"></i> <b>{{ i18n.ts._serverSetupWizard._use.single }}</b></div>
|
||||||
|
<div>{{ i18n.ts._serverSetupWizard._use.single_description }}</div>
|
||||||
|
</option>
|
||||||
|
<option value="group">
|
||||||
|
<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._serverSetupWizard._use.group }}</b></div>
|
||||||
|
<div>{{ i18n.ts._serverSetupWizard._use.group_description }}</div>
|
||||||
|
</option>
|
||||||
|
<option value="open">
|
||||||
|
<div><i class="ti ti-world"></i> <b>{{ i18n.ts._serverSetupWizard._use.open }}</b></div>
|
||||||
|
<div>{{ i18n.ts._serverSetupWizard._use.open_description }}</div>
|
||||||
|
</option>
|
||||||
|
</MkRadios>
|
||||||
|
|
||||||
|
<MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo>
|
||||||
|
<MkInfo v-if="q_use === 'open'" warn><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.openServerAdvice }}</MkInfo>
|
||||||
|
<MkInfo v-if="q_use === 'open'" warn><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.openServerAntiSpamAdvice }}</MkInfo>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="q_use !== 'single'" :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._serverSetupWizard.howManyUsersDoYouExpect }}</template>
|
||||||
|
<template #icon><i class="ti ti-users"></i></template>
|
||||||
|
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkRadios v-model="q_scale" :vertical="true">
|
||||||
|
<option value="small"><i class="ti ti-user"></i> {{ i18n.ts._serverSetupWizard._scale.small }}</option>
|
||||||
|
<option value="medium"><i class="ti ti-users"></i> {{ i18n.ts._serverSetupWizard._scale.medium }}</option>
|
||||||
|
<option value="large"><i class="ti ti-users-group"></i> {{ i18n.ts._serverSetupWizard._scale.large }}</option>
|
||||||
|
</MkRadios>
|
||||||
|
|
||||||
|
<MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse }}</template>
|
||||||
|
<template #icon><i class="ti ti-planet"></i></template>
|
||||||
|
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}</div>
|
||||||
|
|
||||||
|
<MkRadios v-model="q_federation" :vertical="true">
|
||||||
|
<option value="yes">{{ i18n.ts.yes }}</option>
|
||||||
|
<option value="no">{{ i18n.ts.no }}</option>
|
||||||
|
</MkRadios>
|
||||||
|
|
||||||
|
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="q_use === 'open' || q_federation === 'yes'" :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._serverSetupWizard.adminInfo }}</template>
|
||||||
|
<template #icon><i class="ti ti-mail"></i></template>
|
||||||
|
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<div>{{ i18n.ts._serverSetupWizard.adminInfo_description }}</div>
|
||||||
|
|
||||||
|
<MkInfo warn>{{ i18n.ts._serverSetupWizard.adminInfo_mustBeFilled }}</MkInfo>
|
||||||
|
|
||||||
|
<MkInput v-model="q_adminName">
|
||||||
|
<template #label>{{ i18n.ts.maintainerName }}</template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
|
<MkInput v-model="q_adminEmail" type="email">
|
||||||
|
<template #label>{{ i18n.ts.maintainerEmail }}</template>
|
||||||
|
</MkInput>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder :defaultOpen="true" :maxHeight="300">
|
||||||
|
<template #label>{{ i18n.ts._serverSetupWizard.followingSettingsAreRecommended }}</template>
|
||||||
|
<template #icon><i class="ti ti-adjustments-alt"></i></template>
|
||||||
|
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._serverSettings.singleUserMode }}:</b></div>
|
||||||
|
<div>{{ serverSettings.singleUserMode ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._serverSettings.openRegistration }}:</b></div>
|
||||||
|
<div>{{ !serverSettings.disableRegistration ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts.emailRequiredForSignup }}:</b></div>
|
||||||
|
<div>{{ serverSettings.emailRequiredForSignup ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>Log IP:</b></div>
|
||||||
|
<div>{{ serverSettings.enableIpLogging ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts.federation }}:</b></div>
|
||||||
|
<div>{{ serverSettings.federation === 'none' ? i18n.ts.no : i18n.ts.all }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>FTT:</b></div>
|
||||||
|
<div>{{ serverSettings.enableFanoutTimeline ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>FTT/{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}:</b></div>
|
||||||
|
<div>{{ serverSettings.enableFanoutTimelineDbFallback ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>RBT:</b></div>
|
||||||
|
<div>{{ serverSettings.enableReactionsBuffering ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.rateLimitFactor }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.rateLimitFactor }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.driveCapacity }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.driveCapacityMb }} MB</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.userListMax }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.userListLimit }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.antennaMax }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.antennaLimit }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.webhookMax }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.webhookLimit }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportFollowing }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportMuting }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.canImportMuting ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportBlocking }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportUserLists }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><b>{{ i18n.ts._role.baseRole }}/{{ i18n.ts._role._options.canImportAntennas }}:</b></div>
|
||||||
|
<div>{{ defaultPolicies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<MkButton gradate large rounded data-cy-server-setup-wizard-apply style="margin: 0 auto;" @click="applySettings">
|
||||||
|
<i class="ti ti-check"></i> {{ i18n.ts._serverSetupWizard.applyTheseSettings }}
|
||||||
|
</MkButton>
|
||||||
|
</template>
|
||||||
|
</MkFolder>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { ROLE_POLICIES } from '@@/js/const.js';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'finished'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
token?: string;
|
||||||
|
}>(), {
|
||||||
|
});
|
||||||
|
|
||||||
|
const q_name = ref('');
|
||||||
|
const q_use = ref('single');
|
||||||
|
const q_scale = ref('small');
|
||||||
|
const q_federation = ref('yes');
|
||||||
|
const q_adminName = ref('');
|
||||||
|
const q_adminEmail = ref('');
|
||||||
|
|
||||||
|
const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => {
|
||||||
|
let enableReactionsBuffering;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
enableReactionsBuffering = false;
|
||||||
|
} else {
|
||||||
|
enableReactionsBuffering = q_scale.value !== 'small';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
singleUserMode: q_use.value === 'single',
|
||||||
|
disableRegistration: q_use.value !== 'open',
|
||||||
|
emailRequiredForSignup: q_use.value === 'open',
|
||||||
|
enableIpLogging: q_use.value === 'open',
|
||||||
|
federation: q_federation.value === 'yes' ? 'all' : 'none',
|
||||||
|
enableFanoutTimeline: true,
|
||||||
|
enableFanoutTimelineDbFallback: q_use.value === 'single',
|
||||||
|
enableReactionsBuffering,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultPolicies = computed<Partial<Record<typeof ROLE_POLICIES[number], any>>>(() => {
|
||||||
|
let driveCapacityMb;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
driveCapacityMb = 8192;
|
||||||
|
} else if (q_use.value === 'group') {
|
||||||
|
driveCapacityMb = 1000;
|
||||||
|
} else if (q_use.value === 'open') {
|
||||||
|
driveCapacityMb = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rateLimitFactor;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
rateLimitFactor = 0.3;
|
||||||
|
} else if (q_use.value === 'group') {
|
||||||
|
rateLimitFactor = 0.7;
|
||||||
|
} else if (q_use.value === 'open') {
|
||||||
|
if (q_scale.value === 'small') {
|
||||||
|
rateLimitFactor = 1;
|
||||||
|
} else if (q_scale.value === 'medium') {
|
||||||
|
rateLimitFactor = 1.25;
|
||||||
|
} else if (q_scale.value === 'large') {
|
||||||
|
rateLimitFactor = 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let userListLimit;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
userListLimit = 100;
|
||||||
|
} else if (q_use.value === 'group') {
|
||||||
|
userListLimit = 5;
|
||||||
|
} else if (q_use.value === 'open') {
|
||||||
|
userListLimit = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
let antennaLimit;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
antennaLimit = 100;
|
||||||
|
} else if (q_use.value === 'group') {
|
||||||
|
antennaLimit = 5;
|
||||||
|
} else if (q_use.value === 'open') {
|
||||||
|
antennaLimit = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let webhookLimit;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
webhookLimit = 100;
|
||||||
|
} else if (q_use.value === 'group') {
|
||||||
|
webhookLimit = 0;
|
||||||
|
} else if (q_use.value === 'open') {
|
||||||
|
webhookLimit = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let canImportFollowing;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
canImportFollowing = true;
|
||||||
|
} else {
|
||||||
|
canImportFollowing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let canImportMuting;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
canImportMuting = true;
|
||||||
|
} else {
|
||||||
|
canImportMuting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let canImportBlocking;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
canImportBlocking = true;
|
||||||
|
} else {
|
||||||
|
canImportBlocking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let canImportUserLists;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
canImportUserLists = true;
|
||||||
|
} else {
|
||||||
|
canImportUserLists = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let canImportAntennas;
|
||||||
|
if (q_use.value === 'single') {
|
||||||
|
canImportAntennas = true;
|
||||||
|
} else {
|
||||||
|
canImportAntennas = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rateLimitFactor,
|
||||||
|
driveCapacityMb,
|
||||||
|
userListLimit,
|
||||||
|
antennaLimit,
|
||||||
|
webhookLimit,
|
||||||
|
canImportFollowing,
|
||||||
|
canImportMuting,
|
||||||
|
canImportBlocking,
|
||||||
|
canImportUserLists,
|
||||||
|
canImportAntennas,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function applySettings() {
|
||||||
|
const _close = os.waiting();
|
||||||
|
Promise.all([
|
||||||
|
misskeyApi('admin/update-meta', {
|
||||||
|
...serverSettings.value,
|
||||||
|
name: q_name.value === '' ? undefined : q_name.value,
|
||||||
|
maintainerName: q_adminName.value === '' ? undefined : q_adminName.value,
|
||||||
|
maintainerEmail: q_adminEmail.value === '' ? undefined : q_adminEmail.value,
|
||||||
|
}, props.token),
|
||||||
|
misskeyApi('admin/roles/update-default-policies', {
|
||||||
|
policies: defaultPolicies.value,
|
||||||
|
}, props.token),
|
||||||
|
]).then(() => {
|
||||||
|
emit('finished');
|
||||||
|
}).catch((err) => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
title: err.code,
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
_close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,531 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reloadTimeline()">
|
||||||
|
<MkLoading v-if="paginator.fetching.value"/>
|
||||||
|
|
||||||
|
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
||||||
|
|
||||||
|
<div v-else-if="paginator.items.value.length === 0" key="_empty_">
|
||||||
|
<slot name="empty"><MkResult type="empty" :text="i18n.ts.noNotes"/></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else ref="rootEl">
|
||||||
|
<div v-if="paginator.queuedAheadItemsCount.value > 0" :class="$style.new">
|
||||||
|
<div :class="$style.newBg1"></div>
|
||||||
|
<div :class="$style.newBg2"></div>
|
||||||
|
<button class="_button" :class="$style.newButton" @click="releaseQueue()"><i class="ti ti-circle-arrow-up"></i> {{ i18n.ts.newNote }}</button>
|
||||||
|
</div>
|
||||||
|
<component
|
||||||
|
:is="prefer.s.animation ? TransitionGroup : 'div'"
|
||||||
|
:class="$style.notes"
|
||||||
|
:enterActiveClass="$style.transition_x_enterActive"
|
||||||
|
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||||
|
:enterFromClass="$style.transition_x_enterFrom"
|
||||||
|
:leaveToClass="$style.transition_x_leaveTo"
|
||||||
|
:moveClass="$style.transition_x_move"
|
||||||
|
tag="div"
|
||||||
|
>
|
||||||
|
<template v-for="(note, i) in paginator.items.value" :key="note.id">
|
||||||
|
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
|
||||||
|
<div :class="$style.date">
|
||||||
|
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
|
||||||
|
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||||
|
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||||
|
</div>
|
||||||
|
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="note._shouldInsertAd_" :data-scroll-anchor="note.id">
|
||||||
|
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||||
|
<div :class="$style.ad">
|
||||||
|
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MkNote v-else :class="$style.note" :note="note" :withHardMute="true" :data-scroll-anchor="note.id"/>
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
<button v-show="paginator.canFetchOlder.value" key="_more_" v-appear="prefer.s.enableInfiniteScroll ? paginator.fetchOlder : null" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder">
|
||||||
|
<div v-if="!paginator.fetchingOlder.value">{{ i18n.ts.loadMore }}</div>
|
||||||
|
<MkLoading v-else :inline="true"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
|
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
|
||||||
|
import type { BasicTimelineType } from '@/timelines.js';
|
||||||
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
|
import { usePagination } from '@/use/use-pagination.js';
|
||||||
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
|
import { useStream } from '@/stream.js';
|
||||||
|
import * as sound from '@/utility/sound.js';
|
||||||
|
import { $i } from '@/i.js';
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
import { prefer } from '@/preferences.js';
|
||||||
|
import { store } from '@/store.js';
|
||||||
|
import MkNote from '@/components/MkNote.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { globalEvents, useGlobalEvent } from '@/events.js';
|
||||||
|
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
||||||
|
list?: string;
|
||||||
|
antenna?: string;
|
||||||
|
channel?: string;
|
||||||
|
role?: string;
|
||||||
|
sound?: boolean;
|
||||||
|
withRenotes?: boolean;
|
||||||
|
withReplies?: boolean;
|
||||||
|
withSensitive?: boolean;
|
||||||
|
onlyFiles?: boolean;
|
||||||
|
}>(), {
|
||||||
|
withRenotes: true,
|
||||||
|
withReplies: false,
|
||||||
|
withSensitive: true,
|
||||||
|
onlyFiles: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
provide('inTimeline', true);
|
||||||
|
provide('tl_withSensitive', computed(() => props.withSensitive));
|
||||||
|
provide('inChannel', computed(() => props.src === 'channel'));
|
||||||
|
|
||||||
|
function isTop() {
|
||||||
|
if (scrollContainer == null) return true;
|
||||||
|
if (rootEl.value == null) return true;
|
||||||
|
const scrollTop = scrollContainer.scrollTop;
|
||||||
|
const tlTop = rootEl.value.offsetTop - scrollContainer.offsetTop;
|
||||||
|
return scrollTop <= tlTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scrollContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
|
function onScrollContainerScroll() {
|
||||||
|
if (isTop()) {
|
||||||
|
paginator.releaseQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootEl = useTemplateRef('rootEl');
|
||||||
|
watch(rootEl, (el) => {
|
||||||
|
if (el && scrollContainer == null) {
|
||||||
|
scrollContainer = getScrollContainer(el);
|
||||||
|
if (scrollContainer == null) return;
|
||||||
|
scrollContainer.addEventListener('scroll', onScrollContainerScroll, { passive: true }); // ほんとはscrollendにしたいけどiosが非対応
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.removeEventListener('scroll', onScrollContainerScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type TimelineQueryType = {
|
||||||
|
antennaId?: string,
|
||||||
|
withRenotes?: boolean,
|
||||||
|
withReplies?: boolean,
|
||||||
|
withFiles?: boolean,
|
||||||
|
visibility?: string,
|
||||||
|
listId?: string,
|
||||||
|
channelId?: string,
|
||||||
|
roleId?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
let adInsertionCounter = 0;
|
||||||
|
|
||||||
|
const MIN_POLLING_INTERVAL = 1000 * 10;
|
||||||
|
const POLLING_INTERVAL =
|
||||||
|
prefer.s.pollingInterval === 1 ? MIN_POLLING_INTERVAL * 1.5 * 1.5 :
|
||||||
|
prefer.s.pollingInterval === 2 ? MIN_POLLING_INTERVAL * 1.5 :
|
||||||
|
prefer.s.pollingInterval === 3 ? MIN_POLLING_INTERVAL :
|
||||||
|
MIN_POLLING_INTERVAL;
|
||||||
|
|
||||||
|
if (!store.s.realtimeMode) {
|
||||||
|
// TODO: 先頭のノートの作成日時が1日以上前であれば流速が遅いTLと見做してインターバルを通常より延ばす
|
||||||
|
useInterval(async () => {
|
||||||
|
paginator.fetchNewer({
|
||||||
|
toQueue: !isTop(),
|
||||||
|
});
|
||||||
|
}, POLLING_INTERVAL, {
|
||||||
|
immediate: false,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
useGlobalEvent('notePosted', (note) => {
|
||||||
|
paginator.fetchNewer({
|
||||||
|
toQueue: !isTop(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useGlobalEvent('noteDeleted', (noteId) => {
|
||||||
|
paginator.removeItem(noteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
function releaseQueue() {
|
||||||
|
paginator.releaseQueue();
|
||||||
|
scrollToTop(rootEl.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepend(note: Misskey.entities.Note) {
|
||||||
|
adInsertionCounter++;
|
||||||
|
|
||||||
|
if (instance.notesPerOneAd > 0 && adInsertionCounter % instance.notesPerOneAd === 0) {
|
||||||
|
note._shouldInsertAd_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTop()) {
|
||||||
|
paginator.prepend(note);
|
||||||
|
} else {
|
||||||
|
paginator.enqueue(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.sound) {
|
||||||
|
sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection: Misskey.ChannelConnection | null = null;
|
||||||
|
let connection2: Misskey.ChannelConnection | null = null;
|
||||||
|
let paginationQuery: PagingCtx;
|
||||||
|
|
||||||
|
const stream = store.s.realtimeMode ? useStream() : null;
|
||||||
|
|
||||||
|
function connectChannel() {
|
||||||
|
if (props.src === 'antenna') {
|
||||||
|
if (props.antenna == null) return;
|
||||||
|
connection = stream.useChannel('antenna', {
|
||||||
|
antennaId: props.antenna,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'home') {
|
||||||
|
connection = stream.useChannel('homeTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
connection2 = stream.useChannel('main');
|
||||||
|
} else if (props.src === 'local') {
|
||||||
|
connection = stream.useChannel('localTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'social') {
|
||||||
|
connection = stream.useChannel('hybridTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'global') {
|
||||||
|
connection = stream.useChannel('globalTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'mentions') {
|
||||||
|
connection = stream.useChannel('main');
|
||||||
|
connection.on('mention', prepend);
|
||||||
|
} else if (props.src === 'directs') {
|
||||||
|
const onNote = note => {
|
||||||
|
if (note.visibility === 'specified') {
|
||||||
|
prepend(note);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
connection = stream.useChannel('main');
|
||||||
|
connection.on('mention', onNote);
|
||||||
|
} else if (props.src === 'list') {
|
||||||
|
if (props.list == null) return;
|
||||||
|
connection = stream.useChannel('userList', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
listId: props.list,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'channel') {
|
||||||
|
if (props.channel == null) return;
|
||||||
|
connection = stream.useChannel('channel', {
|
||||||
|
channelId: props.channel,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'role') {
|
||||||
|
if (props.role == null) return;
|
||||||
|
connection = stream.useChannel('roleTimeline', {
|
||||||
|
roleId: props.role,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectChannel() {
|
||||||
|
if (connection) connection.dispose();
|
||||||
|
if (connection2) connection2.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePaginationQuery() {
|
||||||
|
let endpoint: keyof Misskey.Endpoints | null;
|
||||||
|
let query: TimelineQueryType | null;
|
||||||
|
|
||||||
|
if (props.src === 'antenna') {
|
||||||
|
endpoint = 'antennas/notes';
|
||||||
|
query = {
|
||||||
|
antennaId: props.antenna,
|
||||||
|
};
|
||||||
|
} else if (props.src === 'home') {
|
||||||
|
endpoint = 'notes/timeline';
|
||||||
|
query = {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
};
|
||||||
|
} else if (props.src === 'local') {
|
||||||
|
endpoint = 'notes/local-timeline';
|
||||||
|
query = {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
};
|
||||||
|
} else if (props.src === 'social') {
|
||||||
|
endpoint = 'notes/hybrid-timeline';
|
||||||
|
query = {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
};
|
||||||
|
} else if (props.src === 'global') {
|
||||||
|
endpoint = 'notes/global-timeline';
|
||||||
|
query = {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
};
|
||||||
|
} else if (props.src === 'mentions') {
|
||||||
|
endpoint = 'notes/mentions';
|
||||||
|
query = null;
|
||||||
|
} else if (props.src === 'directs') {
|
||||||
|
endpoint = 'notes/mentions';
|
||||||
|
query = {
|
||||||
|
visibility: 'specified',
|
||||||
|
};
|
||||||
|
} else if (props.src === 'list') {
|
||||||
|
endpoint = 'notes/user-list-timeline';
|
||||||
|
query = {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
listId: props.list,
|
||||||
|
};
|
||||||
|
} else if (props.src === 'channel') {
|
||||||
|
endpoint = 'channels/timeline';
|
||||||
|
query = {
|
||||||
|
channelId: props.channel,
|
||||||
|
};
|
||||||
|
} else if (props.src === 'role') {
|
||||||
|
endpoint = 'roles/notes';
|
||||||
|
query = {
|
||||||
|
roleId: props.role,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error('Unrecognized timeline type: ' + props.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationQuery = {
|
||||||
|
endpoint: endpoint,
|
||||||
|
limit: 10,
|
||||||
|
params: query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshEndpointAndChannel() {
|
||||||
|
if (store.s.realtimeMode) {
|
||||||
|
disconnectChannel();
|
||||||
|
connectChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePaginationQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる
|
||||||
|
// IDが切り替わったら切り替え先のTLを表示させたい
|
||||||
|
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
|
||||||
|
|
||||||
|
// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK
|
||||||
|
watch(() => props.withSensitive, reloadTimeline);
|
||||||
|
|
||||||
|
// 初回表示用
|
||||||
|
refreshEndpointAndChannel();
|
||||||
|
|
||||||
|
const paginator = usePagination({
|
||||||
|
ctx: paginationQuery,
|
||||||
|
useShallowRef: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
disconnectChannel();
|
||||||
|
});
|
||||||
|
|
||||||
|
function reloadTimeline() {
|
||||||
|
return new Promise<void>((res) => {
|
||||||
|
adInsertionCounter = 0;
|
||||||
|
|
||||||
|
paginator.reload().then(() => {
|
||||||
|
res();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reloadTimeline,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.transition_x_move {
|
||||||
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_x_enterActive {
|
||||||
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
|
||||||
|
&.note,
|
||||||
|
.note {
|
||||||
|
/* Skip Note Rendering有効時、TransitionGroupでnoteを追加するときに一瞬がくっとなる問題を抑制する */
|
||||||
|
content-visibility: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_x_leaveActive {
|
||||||
|
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_x_enterFrom {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(max(-64px, -100%));
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (interpolate-size: allow-keywords) {
|
||||||
|
.transition_x_leaveTo {
|
||||||
|
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_x_leaveTo {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes {
|
||||||
|
container-type: inline-size;
|
||||||
|
background: var(--MI_THEME-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new {
|
||||||
|
--gapFill: 0.5px; // 上位ヘッダーの高さにフォントの関係などで少数が含まれると、レンダリングエンジンによっては隙間が表示されてしまうため、隙間を隠すために少しずらす
|
||||||
|
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--MI-stickyTop, 0px) - var(--gapFill));
|
||||||
|
z-index: 1000;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: calc(10px + var(--gapFill)) 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 疑似progressive blur */
|
||||||
|
.newBg1, .newBg2 {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newBg1 {
|
||||||
|
height: 100%;
|
||||||
|
-webkit-backdrop-filter: var(--MI-blur, blur(2px));
|
||||||
|
backdrop-filter: var(--MI-blur, blur(2px));
|
||||||
|
mask-image: linear-gradient( /* 疑似Easing Linear Gradients */
|
||||||
|
to top,
|
||||||
|
rgb(0 0 0 / 0%) 0%,
|
||||||
|
rgb(0 0 0 / 4.9%) 7.75%,
|
||||||
|
rgb(0 0 0 / 10.4%) 11.25%,
|
||||||
|
rgb(0 0 0 / 45%) 23.55%,
|
||||||
|
rgb(0 0 0 / 55%) 26.45%,
|
||||||
|
rgb(0 0 0 / 89.6%) 38.75%,
|
||||||
|
rgb(0 0 0 / 95.1%) 42.25%,
|
||||||
|
rgb(0 0 0 / 100%) 50%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.newBg2 {
|
||||||
|
height: 75%;
|
||||||
|
-webkit-backdrop-filter: var(--MI-blur, blur(4px));
|
||||||
|
backdrop-filter: var(--MI-blur, blur(4px));
|
||||||
|
mask-image: linear-gradient( /* 疑似Easing Linear Gradients */
|
||||||
|
to top,
|
||||||
|
rgb(0 0 0 / 0%) 0%,
|
||||||
|
rgb(0 0 0 / 4.9%) 15.5%,
|
||||||
|
rgb(0 0 0 / 10.4%) 22.5%,
|
||||||
|
rgb(0 0 0 / 45%) 47.1%,
|
||||||
|
rgb(0 0 0 / 55%) 52.9%,
|
||||||
|
rgb(0 0 0 / 89.6%) 77.5%,
|
||||||
|
rgb(0 0 0 / 95.1%) 91.9%,
|
||||||
|
rgb(0 0 0 / 100%) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.newButton {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
width: max-content;
|
||||||
|
margin: auto;
|
||||||
|
background: var(--MI_THEME-accent);
|
||||||
|
color: var(--MI_THEME-fgOnAccent);
|
||||||
|
font-size: 90%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: flex;
|
||||||
|
font-size: 85%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
opacity: 0.75;
|
||||||
|
padding: 8px 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ad {
|
||||||
|
padding: 8px;
|
||||||
|
background-size: auto auto;
|
||||||
|
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
|
||||||
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--MI_THEME-panel);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,199 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reload()">
|
||||||
|
<MkLoading v-if="paginator.fetching.value"/>
|
||||||
|
|
||||||
|
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
|
||||||
|
|
||||||
|
<div v-else-if="paginator.items.value.length === 0" key="_empty_">
|
||||||
|
<slot name="empty"><MkResult type="empty" :text="i18n.ts.noNotifications"/></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else ref="rootEl">
|
||||||
|
<component
|
||||||
|
:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]"
|
||||||
|
:enterActiveClass="$style.transition_x_enterActive"
|
||||||
|
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||||
|
:enterFromClass="$style.transition_x_enterFrom"
|
||||||
|
:leaveToClass="$style.transition_x_leaveTo"
|
||||||
|
:moveClass="$style.transition_x_move"
|
||||||
|
tag="div"
|
||||||
|
>
|
||||||
|
<div v-for="(notification, i) in paginator.items.value" :key="notification.id" :data-scroll-anchor="notification.id" :class="$style.item">
|
||||||
|
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, notification.createdAt)" :class="$style.date">
|
||||||
|
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt).prevText }}</span>
|
||||||
|
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||||
|
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||||
|
</div>
|
||||||
|
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.content" :note="notification.note" :withHardMute="true"/>
|
||||||
|
<XNotification v-else :class="$style.content" :notification="notification" :withTime="true" :full="true"/>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
<button v-show="paginator.canFetchOlder.value" key="_more_" v-appear="prefer.s.enableInfiniteScroll ? paginator.fetchOlder : null" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder">
|
||||||
|
<div v-if="!paginator.fetchingOlder.value">{{ i18n.ts.loadMore }}</div>
|
||||||
|
<MkLoading v-else/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
|
import type { notificationTypes } from '@@/js/const.js';
|
||||||
|
import XNotification from '@/components/MkNotification.vue';
|
||||||
|
import MkNote from '@/components/MkNote.vue';
|
||||||
|
import { useStream } from '@/stream.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
|
import { prefer } from '@/preferences.js';
|
||||||
|
import { store } from '@/store.js';
|
||||||
|
import { usePagination } from '@/use/use-pagination.js';
|
||||||
|
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
excludeTypes?: typeof notificationTypes[number][];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rootEl = useTemplateRef('rootEl');
|
||||||
|
|
||||||
|
const paginator = usePagination({
|
||||||
|
ctx: prefer.s.useGroupedNotifications ? {
|
||||||
|
endpoint: 'i/notifications-grouped' as const,
|
||||||
|
limit: 20,
|
||||||
|
params: computed(() => ({
|
||||||
|
excludeTypes: props.excludeTypes ?? undefined,
|
||||||
|
})),
|
||||||
|
} : {
|
||||||
|
endpoint: 'i/notifications' as const,
|
||||||
|
limit: 20,
|
||||||
|
params: computed(() => ({
|
||||||
|
excludeTypes: props.excludeTypes ?? undefined,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MIN_POLLING_INTERVAL = 1000 * 10;
|
||||||
|
const POLLING_INTERVAL =
|
||||||
|
prefer.s.pollingInterval === 1 ? MIN_POLLING_INTERVAL * 1.5 * 1.5 :
|
||||||
|
prefer.s.pollingInterval === 2 ? MIN_POLLING_INTERVAL * 1.5 :
|
||||||
|
prefer.s.pollingInterval === 3 ? MIN_POLLING_INTERVAL :
|
||||||
|
MIN_POLLING_INTERVAL;
|
||||||
|
|
||||||
|
if (!store.s.realtimeMode) {
|
||||||
|
useInterval(async () => {
|
||||||
|
paginator.fetchNewer({
|
||||||
|
toQueue: false,
|
||||||
|
});
|
||||||
|
}, POLLING_INTERVAL, {
|
||||||
|
immediate: false,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNotification(notification) {
|
||||||
|
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
|
||||||
|
if (isMuted || window.document.visibilityState === 'visible') {
|
||||||
|
if (store.s.realtimeMode) {
|
||||||
|
useStream().send('readNotification');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMuted) {
|
||||||
|
paginator.prepend(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
return paginator.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection: Misskey.ChannelConnection<Misskey.Channels['main']> | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (store.s.realtimeMode) {
|
||||||
|
connection = useStream().useChannel('main');
|
||||||
|
connection.on('notification', onNotification);
|
||||||
|
connection.on('notificationFlushed', reload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (connection) connection.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reload,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.transition_x_move {
|
||||||
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_x_enterActive {
|
||||||
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
|
||||||
|
&.content,
|
||||||
|
.content {
|
||||||
|
/* Skip Note Rendering有効時、TransitionGroupで通知を追加するときに一瞬がくっとなる問題を抑制する */
|
||||||
|
content-visibility: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_x_leaveActive {
|
||||||
|
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_x_enterFrom {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(max(-64px, -100%));
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (interpolate-size: allow-keywords) {
|
||||||
|
.transition_x_enterFrom {
|
||||||
|
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_x_leaveTo {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
container-type: inline-size;
|
||||||
|
background: var(--MI_THEME-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: flex;
|
||||||
|
font-size: 85%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
opacity: 0.75;
|
||||||
|
padding: 8px 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--MI_THEME-panel);
|
||||||
|
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -18,8 +18,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</details>
|
</details>
|
||||||
<details v-if="note.poll">
|
<details v-if="note.poll">
|
||||||
<summary>{{ i18n.ts.poll }}</summary>
|
<summary>{{ i18n.ts.poll }}</summary>
|
||||||
<MkPoll :noteId="note.id" :poll="note.poll" :author="note.user" :emojiUrls="note.emojis"/>
|
<MkPoll
|
||||||
|
:noteId="note.id"
|
||||||
|
:multiple="note.poll.multiple"
|
||||||
|
:expiresAt="note.poll.expiresAt"
|
||||||
|
:choices="note.poll.choices"
|
||||||
|
:author="note.user"
|
||||||
|
:emojiUrls="note.emojis"
|
||||||
|
/>
|
||||||
</details>
|
</details>
|
||||||
|
<MkA v-if="note.hasPoll && note.poll == null" :to="`/notes/${note.id}`">({{ i18n.ts.poll }})</MkA>
|
||||||
<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
|
<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
|
||||||
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
|
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,372 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reloadTimeline()">
|
|
||||||
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)">
|
|
||||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
|
||||||
|
|
||||||
<template #default="{ items: notes }">
|
|
||||||
<component
|
|
||||||
:is="prefer.s.animation ? TransitionGroup : 'div'"
|
|
||||||
:class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: paginationQuery.reversed }]"
|
|
||||||
:enterActiveClass="$style.transition_x_enterActive"
|
|
||||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
|
||||||
:enterFromClass="$style.transition_x_enterFrom"
|
|
||||||
:leaveToClass="$style.transition_x_leaveTo"
|
|
||||||
:moveClass="$style.transition_x_move"
|
|
||||||
tag="div"
|
|
||||||
>
|
|
||||||
<template v-for="(note, i) in notes" :key="note.id">
|
|
||||||
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
|
|
||||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
|
||||||
<div :class="$style.ad">
|
|
||||||
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MkNote v-else :class="$style.note" :note="note" :withHardMute="true" :data-scroll-anchor="note.id"/>
|
|
||||||
</template>
|
|
||||||
</component>
|
|
||||||
</template>
|
|
||||||
</MkPagination>
|
|
||||||
</component>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup } from 'vue';
|
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import type { BasicTimelineType } from '@/timelines.js';
|
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
|
||||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
|
||||||
import { useStream } from '@/stream.js';
|
|
||||||
import * as sound from '@/utility/sound.js';
|
|
||||||
import { $i } from '@/i.js';
|
|
||||||
import { instance } from '@/instance.js';
|
|
||||||
import { prefer } from '@/preferences.js';
|
|
||||||
import MkNote from '@/components/MkNote.vue';
|
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
|
||||||
list?: string;
|
|
||||||
antenna?: string;
|
|
||||||
channel?: string;
|
|
||||||
role?: string;
|
|
||||||
sound?: boolean;
|
|
||||||
withRenotes?: boolean;
|
|
||||||
withReplies?: boolean;
|
|
||||||
withSensitive?: boolean;
|
|
||||||
onlyFiles?: boolean;
|
|
||||||
}>(), {
|
|
||||||
withRenotes: true,
|
|
||||||
withReplies: false,
|
|
||||||
withSensitive: true,
|
|
||||||
onlyFiles: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'note'): void;
|
|
||||||
(ev: 'queue', count: number): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
provide('inTimeline', true);
|
|
||||||
provide('tl_withSensitive', computed(() => props.withSensitive));
|
|
||||||
provide('inChannel', computed(() => props.src === 'channel'));
|
|
||||||
|
|
||||||
type TimelineQueryType = {
|
|
||||||
antennaId?: string,
|
|
||||||
withRenotes?: boolean,
|
|
||||||
withReplies?: boolean,
|
|
||||||
withFiles?: boolean,
|
|
||||||
visibility?: string,
|
|
||||||
listId?: string,
|
|
||||||
channelId?: string,
|
|
||||||
roleId?: string
|
|
||||||
};
|
|
||||||
|
|
||||||
const pagingComponent = useTemplateRef('pagingComponent');
|
|
||||||
|
|
||||||
let tlNotesCount = 0;
|
|
||||||
|
|
||||||
function prepend(note) {
|
|
||||||
if (pagingComponent.value == null) return;
|
|
||||||
|
|
||||||
tlNotesCount++;
|
|
||||||
|
|
||||||
if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) {
|
|
||||||
note._shouldInsertAd_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pagingComponent.value.prepend(note);
|
|
||||||
|
|
||||||
emit('note');
|
|
||||||
|
|
||||||
if (props.sound) {
|
|
||||||
sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let connection: Misskey.ChannelConnection | null = null;
|
|
||||||
let connection2: Misskey.ChannelConnection | null = null;
|
|
||||||
let paginationQuery: Paging | null = null;
|
|
||||||
const noGap = !prefer.s.showGapBetweenNotesInTimeline;
|
|
||||||
|
|
||||||
const stream = useStream();
|
|
||||||
|
|
||||||
function connectChannel() {
|
|
||||||
if (props.src === 'antenna') {
|
|
||||||
if (props.antenna == null) return;
|
|
||||||
connection = stream.useChannel('antenna', {
|
|
||||||
antennaId: props.antenna,
|
|
||||||
});
|
|
||||||
} else if (props.src === 'home') {
|
|
||||||
connection = stream.useChannel('homeTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection2 = stream.useChannel('main');
|
|
||||||
} else if (props.src === 'local') {
|
|
||||||
connection = stream.useChannel('localTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
} else if (props.src === 'social') {
|
|
||||||
connection = stream.useChannel('hybridTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
} else if (props.src === 'global') {
|
|
||||||
connection = stream.useChannel('globalTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
} else if (props.src === 'mentions') {
|
|
||||||
connection = stream.useChannel('main');
|
|
||||||
connection.on('mention', prepend);
|
|
||||||
} else if (props.src === 'directs') {
|
|
||||||
const onNote = note => {
|
|
||||||
if (note.visibility === 'specified') {
|
|
||||||
prepend(note);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
connection = stream.useChannel('main');
|
|
||||||
connection.on('mention', onNote);
|
|
||||||
} else if (props.src === 'list') {
|
|
||||||
if (props.list == null) return;
|
|
||||||
connection = stream.useChannel('userList', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
listId: props.list,
|
|
||||||
});
|
|
||||||
} else if (props.src === 'channel') {
|
|
||||||
if (props.channel == null) return;
|
|
||||||
connection = stream.useChannel('channel', {
|
|
||||||
channelId: props.channel,
|
|
||||||
});
|
|
||||||
} else if (props.src === 'role') {
|
|
||||||
if (props.role == null) return;
|
|
||||||
connection = stream.useChannel('roleTimeline', {
|
|
||||||
roleId: props.role,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectChannel() {
|
|
||||||
if (connection) connection.dispose();
|
|
||||||
if (connection2) connection2.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePaginationQuery() {
|
|
||||||
let endpoint: keyof Misskey.Endpoints | null;
|
|
||||||
let query: TimelineQueryType | null;
|
|
||||||
|
|
||||||
if (props.src === 'antenna') {
|
|
||||||
endpoint = 'antennas/notes';
|
|
||||||
query = {
|
|
||||||
antennaId: props.antenna,
|
|
||||||
};
|
|
||||||
} else if (props.src === 'home') {
|
|
||||||
endpoint = 'notes/timeline';
|
|
||||||
query = {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
};
|
|
||||||
} else if (props.src === 'local') {
|
|
||||||
endpoint = 'notes/local-timeline';
|
|
||||||
query = {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
};
|
|
||||||
} else if (props.src === 'social') {
|
|
||||||
endpoint = 'notes/hybrid-timeline';
|
|
||||||
query = {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
};
|
|
||||||
} else if (props.src === 'global') {
|
|
||||||
endpoint = 'notes/global-timeline';
|
|
||||||
query = {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
};
|
|
||||||
} else if (props.src === 'mentions') {
|
|
||||||
endpoint = 'notes/mentions';
|
|
||||||
query = null;
|
|
||||||
} else if (props.src === 'directs') {
|
|
||||||
endpoint = 'notes/mentions';
|
|
||||||
query = {
|
|
||||||
visibility: 'specified',
|
|
||||||
};
|
|
||||||
} else if (props.src === 'list') {
|
|
||||||
endpoint = 'notes/user-list-timeline';
|
|
||||||
query = {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
listId: props.list,
|
|
||||||
};
|
|
||||||
} else if (props.src === 'channel') {
|
|
||||||
endpoint = 'channels/timeline';
|
|
||||||
query = {
|
|
||||||
channelId: props.channel,
|
|
||||||
};
|
|
||||||
} else if (props.src === 'role') {
|
|
||||||
endpoint = 'roles/notes';
|
|
||||||
query = {
|
|
||||||
roleId: props.role,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
endpoint = null;
|
|
||||||
query = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoint && query) {
|
|
||||||
paginationQuery = {
|
|
||||||
endpoint: endpoint,
|
|
||||||
limit: 10,
|
|
||||||
params: query,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
paginationQuery = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshEndpointAndChannel() {
|
|
||||||
if (!prefer.s.disableStreamingTimeline) {
|
|
||||||
disconnectChannel();
|
|
||||||
connectChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePaginationQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる
|
|
||||||
// IDが切り替わったら切り替え先のTLを表示させたい
|
|
||||||
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
|
|
||||||
|
|
||||||
// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK
|
|
||||||
watch(() => props.withSensitive, reloadTimeline);
|
|
||||||
|
|
||||||
// 初回表示用
|
|
||||||
refreshEndpointAndChannel();
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
disconnectChannel();
|
|
||||||
});
|
|
||||||
|
|
||||||
function reloadTimeline() {
|
|
||||||
return new Promise<void>((res) => {
|
|
||||||
if (pagingComponent.value == null) return;
|
|
||||||
|
|
||||||
tlNotesCount = 0;
|
|
||||||
|
|
||||||
pagingComponent.value.reload().then(() => {
|
|
||||||
res();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
reloadTimeline,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.transition_x_move {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition_x_enterActive {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
|
|
||||||
&.note,
|
|
||||||
.note {
|
|
||||||
/* Skip Note Rendering有効時、TransitionGroupでnoteを追加するときに一瞬がくっとなる問題を抑制する */
|
|
||||||
content-visibility: visible !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition_x_leaveActive {
|
|
||||||
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition_x_enterFrom {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(max(-64px, -100%));
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (interpolate-size: allow-keywords) {
|
|
||||||
.transition_x_leaveTo {
|
|
||||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition_x_leaveTo {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reverse {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root {
|
|
||||||
container-type: inline-size;
|
|
||||||
|
|
||||||
&.noGap {
|
|
||||||
background: var(--MI_THEME-panel);
|
|
||||||
|
|
||||||
.note {
|
|
||||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ad {
|
|
||||||
padding: 8px;
|
|
||||||
background-size: auto auto;
|
|
||||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
|
|
||||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.noGap) {
|
|
||||||
background: var(--MI_THEME-bg);
|
|
||||||
|
|
||||||
.note {
|
|
||||||
background: var(--MI_THEME-panel);
|
|
||||||
border-radius: var(--MI-radius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ad:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -76,8 +76,6 @@ const onceReacted = ref<boolean>(false);
|
||||||
function addReaction(emoji) {
|
function addReaction(emoji) {
|
||||||
onceReacted.value = true;
|
onceReacted.value = true;
|
||||||
emit('reacted');
|
emit('reacted');
|
||||||
exampleNote.reactions[emoji] = 1;
|
|
||||||
exampleNote.myReaction = emoji;
|
|
||||||
doNotification(emoji);
|
doNotification(emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -74,7 +74,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void
|
(ev: 'closed'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
|
const dialog = useTemplateRef('dialog');
|
||||||
const title = ref(props.announcement ? props.announcement.title : '');
|
const title = ref(props.announcement ? props.announcement.title : '');
|
||||||
const text = ref(props.announcement ? props.announcement.text : '');
|
const text = ref(props.announcement ? props.announcement.text : '');
|
||||||
const icon = ref(props.announcement ? props.announcement.icon : 'info');
|
const icon = ref(props.announcement ? props.announcement.icon : 'info');
|
||||||
|
|
|
@ -16,13 +16,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import MkUserInfo from '@/components/MkUserInfo.vue';
|
import MkUserInfo from '@/components/MkUserInfo.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
pagination: Paging;
|
pagination: PagingCtx;
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
extractor?: (item: any) => any;
|
extractor?: (item: any) => any;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
|
|
@ -39,15 +39,15 @@ import { i18n } from '@/i18n.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import XUser from '@/components/MkUserSetupDialog.User.vue';
|
import XUser from '@/components/MkUserSetupDialog.User.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
|
|
||||||
const pinnedUsers: Paging = {
|
const pinnedUsers: PagingCtx = {
|
||||||
endpoint: 'pinned-users',
|
endpoint: 'pinned-users',
|
||||||
noPaging: true,
|
noPaging: true,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
const popularUsers: Paging = {
|
const popularUsers: PagingCtx = {
|
||||||
endpoint: 'users',
|
endpoint: 'users',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
noPaging: true,
|
noPaging: true,
|
||||||
|
|
|
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="instance.policies.ltlAvailable" :class="[$style.tl, $style.panel]">
|
<div v-if="instance.policies.ltlAvailable" :class="[$style.tl, $style.panel]">
|
||||||
<div :class="$style.tlHeader">{{ i18n.ts.letsLookAtTimeline }}</div>
|
<div :class="$style.tlHeader">{{ i18n.ts.letsLookAtTimeline }}</div>
|
||||||
<div :class="$style.tlBody">
|
<div :class="$style.tlBody">
|
||||||
<MkTimeline src="local"/>
|
<MkStreamingNotesTimeline src="local"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.panel">
|
<div :class="$style.panel">
|
||||||
|
@ -58,7 +58,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import XSigninDialog from '@/components/MkSigninDialog.vue';
|
import XSigninDialog from '@/components/MkSigninDialog.vue';
|
||||||
import XSignupDialog from '@/components/MkSignupDialog.vue';
|
import XSignupDialog from '@/components/MkSignupDialog.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkTimeline from '@/components/MkTimeline.vue';
|
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { instanceName } from '@@/js/config.js';
|
import { instanceName } from '@@/js/config.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
|
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, toRefs, watch } from 'vue';
|
import { computed, onMounted, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||||
import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
|
import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
|
||||||
import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
|
import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
|
||||||
import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
|
import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
|
||||||
|
@ -130,7 +130,7 @@ const bus = new GridEventEmitter();
|
||||||
*/
|
*/
|
||||||
const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries)));
|
const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries)));
|
||||||
|
|
||||||
const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
|
const rootEl = useTemplateRef('rootEl');
|
||||||
/**
|
/**
|
||||||
* グリッドの最も上位にある状態。
|
* グリッドの最も上位にある状態。
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -31,10 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||||
import { GridEventEmitter } from '@/components/grid/grid.js';
|
|
||||||
import type { Size } from '@/components/grid/grid.js';
|
import type { Size } from '@/components/grid/grid.js';
|
||||||
import type { GridColumn } from '@/components/grid/column.js';
|
import type { GridColumn } from '@/components/grid/column.js';
|
||||||
|
import { GridEventEmitter } from '@/components/grid/grid.js';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'operation:beginWidthChange', sender: GridColumn): void;
|
(ev: 'operation:beginWidthChange', sender: GridColumn): void;
|
||||||
|
@ -50,8 +50,8 @@ const props = defineProps<{
|
||||||
|
|
||||||
const { column, bus } = toRefs(props);
|
const { column, bus } = toRefs(props);
|
||||||
|
|
||||||
const rootEl = ref<InstanceType<typeof HTMLTableCellElement>>();
|
const rootEl = useTemplateRef('rootEl');
|
||||||
const contentEl = ref<InstanceType<typeof HTMLDivElement>>();
|
const contentEl = useTemplateRef('contentEl');
|
||||||
|
|
||||||
const resizing = ref<boolean>(false);
|
const resizing = ref<boolean>(false);
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,24 @@
|
||||||
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
export const globalEvents = new EventEmitter<{
|
type Events = {
|
||||||
themeChanging: () => void;
|
themeChanging: () => void;
|
||||||
themeChanged: () => void;
|
themeChanged: () => void;
|
||||||
clientNotification: (notification: Misskey.entities.Notification) => void;
|
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||||
}>();
|
notePosted: (note: Misskey.entities.Note) => void;
|
||||||
|
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const globalEvents = new EventEmitter<Events>();
|
||||||
|
|
||||||
|
export function useGlobalEvent<T extends keyof Events>(
|
||||||
|
event: T,
|
||||||
|
callback: Events[T],
|
||||||
|
): void {
|
||||||
|
globalEvents.on(event, callback);
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
globalEvents.off(event, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { $i } from '@/i.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { get, set } from '@/utility/idb-proxy.js';
|
import { get, set } from '@/utility/idb-proxy.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import { useStream } from '@/stream.js';
|
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import { deepMerge } from '@/utility/merge.js';
|
import { deepMerge } from '@/utility/merge.js';
|
||||||
|
|
||||||
|
@ -129,25 +128,6 @@ export class Pizzax<T extends StateDef> {
|
||||||
if (where === 'deviceAccount' && !($i && userId !== $i.id)) return;
|
if (where === 'deviceAccount' && !($i && userId !== $i.id)) return;
|
||||||
this.r[key].value = this.s[key] = value;
|
this.r[key].value = this.s[key] = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($i) {
|
|
||||||
const connection = useStream().useChannel('main');
|
|
||||||
|
|
||||||
// streamingのuser storage updateイベントを監視して更新
|
|
||||||
connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
|
|
||||||
if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.s[key] === value) return;
|
|
||||||
|
|
||||||
this.r[key].value = this.s[key] = value;
|
|
||||||
|
|
||||||
this.addIdbSetJob(async () => {
|
|
||||||
const cache = await get(this.registryCacheKeyName);
|
|
||||||
if (cache[key] !== value) {
|
|
||||||
cache[key] = value;
|
|
||||||
await set(this.registryCacheKeyName, cache);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private load(): Promise<void> {
|
private load(): Promise<void> {
|
||||||
|
|
|
@ -547,18 +547,24 @@ export function success(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function waiting(text?: string | null): Promise<void> {
|
export function waiting(text?: string | null): () => void {
|
||||||
return new Promise(resolve => {
|
window.document.body.setAttribute('inert', 'true');
|
||||||
|
|
||||||
const showing = ref(true);
|
const showing = ref(true);
|
||||||
const { dispose } = popup(MkWaitingDialog, {
|
const { dispose } = popup(MkWaitingDialog, {
|
||||||
success: false,
|
success: false,
|
||||||
showing: showing,
|
showing: showing,
|
||||||
text,
|
text,
|
||||||
}, {
|
}, {
|
||||||
done: () => resolve(),
|
closed: () => {
|
||||||
closed: () => dispose(),
|
window.document.body.removeAttribute('inert');
|
||||||
});
|
dispose();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
showing.value = false;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> {
|
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> {
|
||||||
|
|
|
@ -55,7 +55,7 @@ import { computed, ref } from 'vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -81,7 +81,7 @@ const pagination = {
|
||||||
state.value === 'notResponding' ? { notResponding: true } :
|
state.value === 'notResponding' ? { notResponding: true } :
|
||||||
{}),
|
{}),
|
||||||
})),
|
})),
|
||||||
} as Paging;
|
} as PagingCtx;
|
||||||
|
|
||||||
function getStatus(instance) {
|
function getStatus(instance) {
|
||||||
if (instance.isSuspended) return 'Suspended';
|
if (instance.isSuspended) return 'Suspended';
|
||||||
|
|
|
@ -87,7 +87,7 @@ const pagination = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolved(reportId) {
|
function resolved(reportId) {
|
||||||
reports.value?.removeItem(reportId);
|
reports.value?.paginator.removeItem(reportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTutorial() {
|
function closeTutorial() {
|
||||||
|
|
|
@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, useTemplateRef } from 'vue';
|
import { computed, ref, useTemplateRef } from 'vue';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -73,7 +73,7 @@ const pagingComponent = useTemplateRef('pagingComponent');
|
||||||
const type = ref('all');
|
const type = ref('all');
|
||||||
const sort = ref('+createdAt');
|
const sort = ref('+createdAt');
|
||||||
|
|
||||||
const pagination: Paging = {
|
const pagination: PagingCtx = {
|
||||||
endpoint: 'admin/invite/list' as const,
|
endpoint: 'admin/invite/list' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
|
@ -100,12 +100,12 @@ async function createWithOptions() {
|
||||||
text: tickets.map(x => x.code).join('\n'),
|
text: tickets.map(x => x.code).join('\n'),
|
||||||
});
|
});
|
||||||
|
|
||||||
tickets.forEach(ticket => pagingComponent.value?.prepend(ticket));
|
tickets.forEach(ticket => pagingComponent.value?.paginator.prepend(ticket));
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleted(id: string) {
|
function deleted(id: string) {
|
||||||
if (pagingComponent.value) {
|
if (pagingComponent.value) {
|
||||||
pagingComponent.value.items.delete(id);
|
pagingComponent.value.paginator.removeItem(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
|
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
|
||||||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
<template #label>{{ i18n.ts.emailRequiredForSignup }} ({{ i18n.ts.recommended }})</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
|
<MkSelect v-model="ugcVisibilityForVisitor" @update:modelValue="onChange_ugcVisibilityForVisitor">
|
||||||
|
<template #label>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</template>
|
||||||
|
<option value="all">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all }}</option>
|
||||||
|
<option value="local">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly }} ({{ i18n.ts.recommended }})</option>
|
||||||
|
<option value="none">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none }}</option>
|
||||||
|
<template #caption>
|
||||||
|
<div>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</div>
|
||||||
|
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</div>
|
||||||
|
</template>
|
||||||
|
</MkSelect>
|
||||||
|
|
||||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||||
|
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
|
@ -137,9 +148,11 @@ import { definePage } from '@/page.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
|
||||||
const enableRegistration = ref<boolean>(false);
|
const enableRegistration = ref<boolean>(false);
|
||||||
const emailRequiredForSignup = ref<boolean>(false);
|
const emailRequiredForSignup = ref<boolean>(false);
|
||||||
|
const ugcVisibilityForVisitor = ref<string>('all');
|
||||||
const sensitiveWords = ref<string>('');
|
const sensitiveWords = ref<string>('');
|
||||||
const prohibitedWords = ref<string>('');
|
const prohibitedWords = ref<string>('');
|
||||||
const prohibitedWordsForNameOfUser = ref<string>('');
|
const prohibitedWordsForNameOfUser = ref<string>('');
|
||||||
|
@ -153,6 +166,7 @@ async function init() {
|
||||||
const meta = await misskeyApi('admin/meta');
|
const meta = await misskeyApi('admin/meta');
|
||||||
enableRegistration.value = !meta.disableRegistration;
|
enableRegistration.value = !meta.disableRegistration;
|
||||||
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
||||||
|
ugcVisibilityForVisitor.value = meta.ugcVisibilityForVisitor;
|
||||||
sensitiveWords.value = meta.sensitiveWords.join('\n');
|
sensitiveWords.value = meta.sensitiveWords.join('\n');
|
||||||
prohibitedWords.value = meta.prohibitedWords.join('\n');
|
prohibitedWords.value = meta.prohibitedWords.join('\n');
|
||||||
prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
|
prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
|
||||||
|
@ -189,6 +203,14 @@ function onChange_emailRequiredForSignup(value: boolean) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChange_ugcVisibilityForVisitor(value: string) {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
ugcVisibilityForVisitor: value,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function save_preservedUsernames() {
|
function save_preservedUsernames() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||||
|
|
|
@ -121,7 +121,7 @@ async function addUser() {
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
paginationComponent.value?.reload();
|
paginationComponent.value?.paginator.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, useTemplateRef } from 'vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
@ -71,7 +71,7 @@ const paginationPast = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
const paginationEl = useTemplateRef('paginationEl');
|
||||||
|
|
||||||
const tab = ref('current');
|
const tab = ref('current');
|
||||||
|
|
||||||
|
@ -86,10 +86,10 @@ async function read(target) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!paginationEl.value) return;
|
if (!paginationEl.value) return;
|
||||||
paginationEl.value.updateItem(target.id, a => {
|
paginationEl.value.paginator.updateItem(target.id, a => ({
|
||||||
a.isRead = true;
|
...a,
|
||||||
return a;
|
isRead: true,
|
||||||
});
|
}));
|
||||||
misskeyApi('i/read-announcement', { announcementId: target.id });
|
misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||||
updateCurrentAccountPartial({
|
updateCurrentAccountPartial({
|
||||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
||||||
|
|
|
@ -6,27 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||||
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||||
<div ref="rootEl">
|
|
||||||
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
|
||||||
<div :class="$style.tl">
|
<div :class="$style.tl">
|
||||||
<MkTimeline
|
<MkStreamingNotesTimeline
|
||||||
ref="tlEl" :key="antennaId"
|
ref="tlEl" :key="antennaId"
|
||||||
src="antenna"
|
src="antenna"
|
||||||
:antenna="antennaId"
|
:antenna="antennaId"
|
||||||
:sound="true"
|
:sound="true"
|
||||||
@queue="queueUpdated"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</PageWithHeader>
|
</PageWithHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch, ref, useTemplateRef } from 'vue';
|
import { computed, watch, ref, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { scrollInContainer } from '@@/js/scroll.js';
|
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
|
||||||
import MkTimeline from '@/components/MkTimeline.vue';
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
@ -40,18 +35,8 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const antenna = ref<Misskey.entities.Antenna | null>(null);
|
const antenna = ref<Misskey.entities.Antenna | null>(null);
|
||||||
const queue = ref(0);
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
|
||||||
const tlEl = useTemplateRef('tlEl');
|
const tlEl = useTemplateRef('tlEl');
|
||||||
|
|
||||||
function queueUpdated(q) {
|
|
||||||
queue.value = q;
|
|
||||||
}
|
|
||||||
|
|
||||||
function top() {
|
|
||||||
scrollInContainer(rootEl.value, { top: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function timetravel() {
|
async function timetravel() {
|
||||||
const { canceled, result: date } = await os.inputDate({
|
const { canceled, result: date } = await os.inputDate({
|
||||||
title: i18n.ts.date,
|
title: i18n.ts.date,
|
||||||
|
@ -94,25 +79,6 @@ definePage(() => ({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.new {
|
|
||||||
position: sticky;
|
|
||||||
top: calc(var(--MI-stickyTop, 0px) + 16px);
|
|
||||||
z-index: 1000;
|
|
||||||
width: 100%;
|
|
||||||
margin: calc(-0.675em - 8px) 0;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: calc(-0.675em - 8px - var(--MI-margin));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.newButton {
|
|
||||||
display: block;
|
|
||||||
margin: var(--MI-margin) auto 0 auto;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tl {
|
.tl {
|
||||||
background: var(--MI_THEME-bg);
|
background: var(--MI_THEME-bg);
|
||||||
border-radius: var(--MI-radius);
|
border-radius: var(--MI-radius);
|
||||||
|
|
|
@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch, ref } from 'vue';
|
import { computed, watch, ref, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -86,7 +86,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void
|
(ev: 'closed'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
const windowEl = useTemplateRef('windowEl');
|
||||||
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
|
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
|
||||||
const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
|
const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
|
||||||
const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
|
const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
|
||||||
|
|
|
@ -37,10 +37,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
||||||
<MkPostForm v-if="$i && prefer.r.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
<MkPostForm v-if="$i && prefer.r.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
||||||
|
|
||||||
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
|
<MkStreamingNotesTimeline :key="channelId" src="channel" :channel="channelId"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'featured'">
|
<div v-else-if="tab === 'featured'">
|
||||||
<MkNotes :pagination="featuredPagination"/>
|
<MkNotesTimeline :pagination="featuredPagination"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'search'">
|
<div v-else-if="tab === 'search'">
|
||||||
<div v-if="notesSearchAvailable" class="_gaps">
|
<div v-if="notesSearchAvailable" class="_gaps">
|
||||||
|
@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
|
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
|
<MkNotesTimeline v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
|
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
|
||||||
|
@ -73,9 +73,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { computed, watch, ref } from 'vue';
|
import { computed, watch, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { url } from '@@/js/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
import MkTimeline from '@/components/MkTimeline.vue';
|
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
|
||||||
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -83,7 +84,7 @@ import { $i, iAmModerator } from '@/i.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import { deviceKind } from '@/utility/device-kind.js';
|
import { deviceKind } from '@/utility/device-kind.js';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
|
||||||
import { favoritedChannelsCache } from '@/cache.js';
|
import { favoritedChannelsCache } from '@/cache.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
@ -118,6 +119,14 @@ const featuredPagination = computed(() => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
if (channel.value == null) return;
|
||||||
|
miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, Date.now());
|
||||||
|
}, 3000, {
|
||||||
|
immediate: true,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => props.channelId, async () => {
|
watch(() => props.channelId, async () => {
|
||||||
channel.value = await misskeyApi('channels/show', {
|
channel.value = await misskeyApi('channels/show', {
|
||||||
channelId: props.channelId,
|
channelId: props.channelId,
|
||||||
|
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkNotes :pagination="pagination" :detail="true"/>
|
<MkNotesTimeline :pagination="pagination" :detail="true"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageWithHeader>
|
</PageWithHeader>
|
||||||
|
@ -34,7 +34,7 @@ import { computed, watch, provide, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { url } from '@@/js/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
|
@ -115,7 +115,7 @@ const selectAll = () => {
|
||||||
if (selectedEmojis.value.length > 0) {
|
if (selectedEmojis.value.length > 0) {
|
||||||
selectedEmojis.value = [];
|
selectedEmojis.value = [];
|
||||||
} else {
|
} else {
|
||||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
|
selectedEmojis.value = emojisPaginationComponent.value?.paginator.items.value.map(item => item.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ const add = async (ev: MouseEvent) => {
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
if (result.created) {
|
if (result.created) {
|
||||||
emojisPaginationComponent.value?.prepend(result.created);
|
emojisPaginationComponent.value?.paginator.prepend(result.created);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -145,12 +145,12 @@ const edit = (emoji) => {
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
if (result.updated) {
|
if (result.updated) {
|
||||||
emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
|
emojisPaginationComponent.value?.paginator.updateItem(result.updated.id, (oldEmoji) => ({
|
||||||
...oldEmoji,
|
...oldEmoji,
|
||||||
...result.updated,
|
...result.updated,
|
||||||
}));
|
}));
|
||||||
} else if (result.deleted) {
|
} else if (result.deleted) {
|
||||||
emojisPaginationComponent.value?.removeItem(emoji.id);
|
emojisPaginationComponent.value?.paginator.removeItem(emoji.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -242,7 +242,7 @@ const setCategoryBulk = async () => {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
category: result,
|
category: result,
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value?.reload();
|
emojisPaginationComponent.value?.paginator.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setLicenseBulk = async () => {
|
const setLicenseBulk = async () => {
|
||||||
|
@ -254,7 +254,7 @@ const setLicenseBulk = async () => {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
license: result,
|
license: result,
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value?.reload();
|
emojisPaginationComponent.value?.paginator.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTagBulk = async () => {
|
const addTagBulk = async () => {
|
||||||
|
@ -266,7 +266,7 @@ const addTagBulk = async () => {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
aliases: result.split(' '),
|
aliases: result.split(' '),
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value?.reload();
|
emojisPaginationComponent.value?.paginator.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTagBulk = async () => {
|
const removeTagBulk = async () => {
|
||||||
|
@ -278,7 +278,7 @@ const removeTagBulk = async () => {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
aliases: result.split(' '),
|
aliases: result.split(' '),
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value?.reload();
|
emojisPaginationComponent.value?.paginator.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTagBulk = async () => {
|
const setTagBulk = async () => {
|
||||||
|
@ -290,7 +290,7 @@ const setTagBulk = async () => {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
aliases: result.split(' '),
|
aliases: result.split(' '),
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value?.reload();
|
emojisPaginationComponent.value?.paginator.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const delBulk = async () => {
|
const delBulk = async () => {
|
||||||
|
@ -302,7 +302,7 @@ const delBulk = async () => {
|
||||||
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value?.reload();
|
emojisPaginationComponent.value?.paginator.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerActions = computed(() => [{
|
const headerActions = computed(() => [{
|
||||||
|
|
|
@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
|
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
|
||||||
<MkNotes ref="tlComponent" :pagination="pagination"/>
|
<MkNotesTimeline ref="tlComponent" :pagination="pagination"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fileId: string;
|
fileId: string;
|
||||||
|
@ -23,7 +23,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const realFileId = computed(() => props.fileId);
|
const realFileId = computed(() => props.fileId);
|
||||||
|
|
||||||
const pagination = ref<Paging>({
|
const pagination = ref<PagingCtx>({
|
||||||
endpoint: 'drive/files/attached-notes',
|
endpoint: 'drive/files/attached-notes',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: {
|
params: {
|
||||||
|
|
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch, ref } from 'vue';
|
import { computed, watch, ref, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -103,7 +103,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void
|
(ev: 'closed'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
const windowEl = useTemplateRef('windowEl');
|
||||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||||
const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
|
const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
|
||||||
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||||
|
|
|
@ -9,14 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="notes">{{ i18n.ts.notes }}</option>
|
<option value="notes">{{ i18n.ts.notes }}</option>
|
||||||
<option value="polls">{{ i18n.ts.poll }}</option>
|
<option value="polls">{{ i18n.ts.poll }}</option>
|
||||||
</MkTab>
|
</MkTab>
|
||||||
<MkNotes v-if="tab === 'notes'" :pagination="paginationForNotes"/>
|
<MkNotesTimeline v-if="tab === 'notes'" :pagination="paginationForNotes"/>
|
||||||
<MkNotes v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
|
<MkNotesTimeline v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
import MkTab from '@/components/MkTab.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { useTemplateRef, computed, ref } from 'vue';
|
import { useTemplateRef, computed, ref } from 'vue';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { userPage, acct } from '@/filters/user.js';
|
import { userPage, acct } from '@/filters/user.js';
|
||||||
|
@ -47,7 +47,7 @@ import { $i } from '@/i.js';
|
||||||
|
|
||||||
const paginationComponent = useTemplateRef('paginationComponent');
|
const paginationComponent = useTemplateRef('paginationComponent');
|
||||||
|
|
||||||
const pagination = computed<Paging>(() => tab.value === 'list' ? {
|
const pagination = computed<PagingCtx>(() => tab.value === 'list' ? {
|
||||||
endpoint: 'following/requests/list',
|
endpoint: 'following/requests/list',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
} : {
|
} : {
|
||||||
|
@ -57,19 +57,19 @@ const pagination = computed<Paging>(() => tab.value === 'list' ? {
|
||||||
|
|
||||||
function accept(user: Misskey.entities.UserLite) {
|
function accept(user: Misskey.entities.UserLite) {
|
||||||
os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => {
|
os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => {
|
||||||
paginationComponent.value?.reload();
|
paginationComponent.value?.paginator.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reject(user: Misskey.entities.UserLite) {
|
function reject(user: Misskey.entities.UserLite) {
|
||||||
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
|
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
|
||||||
paginationComponent.value?.reload();
|
paginationComponent.value?.paginator.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel(user: Misskey.entities.UserLite) {
|
function cancel(user: Misskey.entities.UserLite) {
|
||||||
os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
|
os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
|
||||||
paginationComponent.value?.reload();
|
paginationComponent.value?.paginator.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { ChartSrc } from '@/components/MkChart.vue';
|
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import MkChart from '@/components/MkChart.vue';
|
import MkChart from '@/components/MkChart.vue';
|
||||||
import MkObjectView from '@/components/MkObjectView.vue';
|
import MkObjectView from '@/components/MkObjectView.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
|
@ -180,7 +180,7 @@ const usersPagination = {
|
||||||
hostname: props.host,
|
hostname: props.host,
|
||||||
},
|
},
|
||||||
offsetMode: true,
|
offsetMode: true,
|
||||||
} satisfies Paging;
|
} satisfies PagingCtx;
|
||||||
|
|
||||||
if (iAmModerator) {
|
if (iAmModerator) {
|
||||||
watch(moderationNote, async () => {
|
watch(moderationNote, async () => {
|
||||||
|
|
|
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, useTemplateRef } from 'vue';
|
import { computed, ref, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -45,7 +45,7 @@ const currentInviteLimit = ref<null | number>(null);
|
||||||
const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number;
|
const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number;
|
||||||
const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number;
|
const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number;
|
||||||
|
|
||||||
const pagination: Paging = {
|
const pagination: PagingCtx = {
|
||||||
endpoint: 'invite/list' as const,
|
endpoint: 'invite/list' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
@ -68,13 +68,13 @@ async function create() {
|
||||||
text: ticket.code,
|
text: ticket.code,
|
||||||
});
|
});
|
||||||
|
|
||||||
pagingComponent.value?.prepend(ticket);
|
pagingComponent.value?.paginator.prepend(ticket);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleted(id: string) {
|
function deleted(id: string) {
|
||||||
if (pagingComponent.value) {
|
if (pagingComponent.value) {
|
||||||
pagingComponent.value.items.delete(id);
|
pagingComponent.value.paginator.removeItem(id);
|
||||||
}
|
}
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,15 +73,15 @@ async function create() {
|
||||||
|
|
||||||
clipsCache.delete();
|
clipsCache.delete();
|
||||||
|
|
||||||
pagingComponent.value?.reload();
|
pagingComponent.value?.paginator.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClipCreated() {
|
function onClipCreated() {
|
||||||
pagingComponent.value?.reload();
|
pagingComponent.value?.paginator.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClipDeleted() {
|
function onClipDeleted() {
|
||||||
pagingComponent.value?.reload();
|
pagingComponent.value?.paginator.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = computed(() => []);
|
const headerActions = computed(() => []);
|
||||||
|
|
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -80,7 +80,7 @@ const props = defineProps<{
|
||||||
listId: string;
|
listId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
const paginationEl = useTemplateRef('paginationEl');
|
||||||
const list = ref<Misskey.entities.UserList | null>(null);
|
const list = ref<Misskey.entities.UserList | null>(null);
|
||||||
const isPublic = ref(false);
|
const isPublic = ref(false);
|
||||||
const name = ref('');
|
const name = ref('');
|
||||||
|
@ -109,7 +109,7 @@ function addUser() {
|
||||||
listId: list.value.id,
|
listId: list.value.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
paginationEl.value?.reload();
|
paginationEl.value?.paginator.reload();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ async function removeUser(item, ev) {
|
||||||
listId: list.value.id,
|
listId: list.value.id,
|
||||||
userId: item.userId,
|
userId: item.userId,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
paginationEl.value?.removeItem(item.id);
|
paginationEl.value?.paginator.removeItem(item.id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
@ -147,7 +147,7 @@ async function showMembershipMenu(item, ev) {
|
||||||
userId: item.userId,
|
userId: item.userId,
|
||||||
withReplies,
|
withReplies,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
paginationEl.value!.updateItem(item.id, (old) => ({
|
paginationEl.value!.paginator.updateItem(item.id, (old) => ({
|
||||||
...old,
|
...old,
|
||||||
withReplies,
|
withReplies,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -6,11 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||||
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||||
<div>
|
|
||||||
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
|
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
|
||||||
<div v-if="note">
|
<div v-if="note">
|
||||||
<div v-if="showNext" class="_margin">
|
<div v-if="showNext" class="_margin">
|
||||||
<MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
|
<MkNotesTimeline :pullToRefresh="false" class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="_margin">
|
<div class="_margin">
|
||||||
|
@ -35,14 +34,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showPrev" class="_margin">
|
<div v-if="showPrev" class="_margin">
|
||||||
<MkNotes class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/>
|
<MkNotesTimeline :pullToRefresh="false" class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkError v-else-if="error" @retry="fetchNote()"/>
|
<MkError v-else-if="error" @retry="fetchNote()"/>
|
||||||
<MkLoading v-else/>
|
<MkLoading v-else/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</PageWithHeader>
|
</PageWithHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -50,9 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { computed, watch, ref } from 'vue';
|
import { computed, watch, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { PagingCtx } from '@/use/use-pagination.js';
|
||||||
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
|
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
|
||||||
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
|
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -80,26 +78,27 @@ const showPrev = ref<'user' | 'channel' | false>(false);
|
||||||
const showNext = ref<'user' | 'channel' | false>(false);
|
const showNext = ref<'user' | 'channel' | false>(false);
|
||||||
const error = ref();
|
const error = ref();
|
||||||
|
|
||||||
const prevUserPagination: Paging = {
|
const prevUserPagination: PagingCtx = {
|
||||||
endpoint: 'users/notes',
|
endpoint: 'users/notes',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
baseId: props.noteId,
|
||||||
|
direction: 'older',
|
||||||
params: computed(() => note.value ? ({
|
params: computed(() => note.value ? ({
|
||||||
userId: note.value.userId,
|
userId: note.value.userId,
|
||||||
untilId: note.value.id,
|
|
||||||
}) : undefined),
|
}) : undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextUserPagination: Paging = {
|
const nextUserPagination: PagingCtx = {
|
||||||
reversed: true,
|
|
||||||
endpoint: 'users/notes',
|
endpoint: 'users/notes',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
baseId: props.noteId,
|
||||||
|
direction: 'newer',
|
||||||
params: computed(() => note.value ? ({
|
params: computed(() => note.value ? ({
|
||||||
userId: note.value.userId,
|
userId: note.value.userId,
|
||||||
sinceId: note.value.id,
|
|
||||||
}) : undefined),
|
}) : undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevChannelPagination: Paging = {
|
const prevChannelPagination: PagingCtx = {
|
||||||
endpoint: 'channels/timeline',
|
endpoint: 'channels/timeline',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: computed(() => note.value ? ({
|
params: computed(() => note.value ? ({
|
||||||
|
@ -108,7 +107,7 @@ const prevChannelPagination: Paging = {
|
||||||
}) : undefined),
|
}) : undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextChannelPagination: Paging = {
|
const nextChannelPagination: PagingCtx = {
|
||||||
reversed: true,
|
reversed: true,
|
||||||
endpoint: 'channels/timeline',
|
endpoint: 'channels/timeline',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue