Merge branch 'develop' of https://github.com/transfem-org/Sharkey into develop

This commit is contained in:
Mar0xy 2023-09-29 00:57:53 +02:00
commit 4b87753564
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
35 changed files with 277 additions and 86 deletions

View file

@ -8,6 +8,8 @@ on:
paths: paths:
- packages/** - packages/**
pull_request: pull_request:
branches-ignore:
- weblate
jobs: jobs:
pnpm_install: pnpm_install:

View file

@ -12,6 +12,19 @@
--> -->
## next
### General
- Enhance: タイムラインからRenoteを除外するオプションを追加
- Enhance: ユーザーページのート一覧でRenoteを除外できるように
### Client
- Enhance: モデレーションログ機能の強化
- Enhance: Plugin:register_post_form_actionを用いてCWを取得・変更できるように
### Server
- Enhance: MasterプロセスのPIDを書き出せるように
## 2023.9.1 ## 2023.9.1
### General ### General

View file

@ -6,9 +6,10 @@
**🌎 **[Sharkey](https://joinsharkey.org/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **[Sharkey](https://joinsharkey.org/)** is an open source, decentralized social media platform that's free forever! 🚀**
--- ---
<a href="https://translate.joinsharkey.org/engage/sharkey/">
<img src="https://translate.joinsharkey.org/widget/sharkey/github/287x66-black.png" alt="Translation status" />
</a>
Work In Progress
--- ---
</div> </div>

View file

@ -161,7 +161,7 @@ youCanCleanRemoteFilesCache: "Klicke auf den 🗑️-Knopf der Dateiverwaltungsa
cacheRemoteSensitiveFiles: "Sensitive Dateien von fremden Instanzen im Cache speichern" cacheRemoteSensitiveFiles: "Sensitive Dateien von fremden Instanzen im Cache speichern"
cacheRemoteSensitiveFilesDescription: "Ist diese Einstellung deaktiviert, so werden sensitive Dateien fremder Instanzen direkt von dort ohne Zwischenspeicherung geladen." cacheRemoteSensitiveFilesDescription: "Ist diese Einstellung deaktiviert, so werden sensitive Dateien fremder Instanzen direkt von dort ohne Zwischenspeicherung geladen."
flagAsBot: "Als Bot markieren" flagAsBot: "Als Bot markieren"
flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Misskeys interne Systeme dieses Benutzerkonto als Bot behandeln." flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Sharkeys interne Systeme dieses Benutzerkonto als Bot behandeln."
flagAsCat: "Als Katze markieren" flagAsCat: "Als Katze markieren"
flagAsCatDescription: "Aktiviere diese Option, um dieses Benutzerkonto als Katze zu markieren." flagAsCatDescription: "Aktiviere diese Option, um dieses Benutzerkonto als Katze zu markieren."
flagShowTimelineReplies: "Antworten in der Chronik anzeigen" flagShowTimelineReplies: "Antworten in der Chronik anzeigen"
@ -221,7 +221,7 @@ noUsers: "Keine Benutzer gefunden"
editProfile: "Profil bearbeiten" editProfile: "Profil bearbeiten"
noteDeleteConfirm: "Möchtest du diese Notiz wirklich löschen?" noteDeleteConfirm: "Möchtest du diese Notiz wirklich löschen?"
pinLimitExceeded: "Du kannst nicht noch mehr Notizen anheften." pinLimitExceeded: "Du kannst nicht noch mehr Notizen anheften."
intro: "Misskey ist installiert! Lass uns nun ein Administratorkonto einrichten." intro: "Sharkey ist installiert! Lass uns nun ein Administratorkonto einrichten."
done: "Fertig" done: "Fertig"
processing: "In Bearbeitung …" processing: "In Bearbeitung …"
preview: "Vorschau" preview: "Vorschau"
@ -555,7 +555,7 @@ sort: "Sortieren"
ascendingOrder: "Aufsteigende Reihenfolge" ascendingOrder: "Aufsteigende Reihenfolge"
descendingOrder: "Absteigende Reihenfolge" descendingOrder: "Absteigende Reihenfolge"
scratchpad: "Testumgebung" scratchpad: "Testumgebung"
scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Misskey überprüfen." scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Sharkey überprüfen."
output: "Ausgabe" output: "Ausgabe"
script: "Skript" script: "Skript"
disablePagesScript: "AiScript auf Seiten deaktivieren" disablePagesScript: "AiScript auf Seiten deaktivieren"
@ -687,7 +687,7 @@ unclip: "Aus Clip entfernen"
confirmToUnclipAlreadyClippedNote: "Diese Notiz ist bereits im \"{name}\" Clip enthalten. Möchtest du sie aus diesem Clip entfernen?" confirmToUnclipAlreadyClippedNote: "Diese Notiz ist bereits im \"{name}\" Clip enthalten. Möchtest du sie aus diesem Clip entfernen?"
public: "Öffentlich" public: "Öffentlich"
private: "Privat" private: "Privat"
i18nInfo: "Misskey wird durch freiwillige Helfer in viele verschiedene Sprachen übersetzt. Auf {link} kannst du mithelfen." i18nInfo: "Sharkey wird durch freiwillige Helfer in viele verschiedene Sprachen übersetzt. Auf {link} kannst du mithelfen."
manageAccessTokens: "Zugriffstokens verwalten" manageAccessTokens: "Zugriffstokens verwalten"
accountInfo: "Benutzerkonto-Informationen" accountInfo: "Benutzerkonto-Informationen"
notesCount: "Anzahl der Notizen" notesCount: "Anzahl der Notizen"
@ -741,7 +741,7 @@ onlineUsersCount: "{n} Benutzer sind online"
nUsers: "{n} Benutzer" nUsers: "{n} Benutzer"
nNotes: "{n} Notizen" nNotes: "{n} Notizen"
sendErrorReports: "Fehlerberichte senden" sendErrorReports: "Fehlerberichte senden"
sendErrorReportsDescription: "Ist diese Option aktiviert, so werden beim Auftreten von Fehlern detaillierte Fehlerinformationen an Misskey weitergegeben, was zur Verbesserung der Qualität von Misskey beiträgt.\nEnthalten in diesen Informationen sind u.a. die Version deines Betriebssystems, welchen Browser du verwendest und ein Verlauf deiner Aktivitäten innerhalb Misskey." sendErrorReportsDescription: "Ist diese Option aktiviert, so werden beim Auftreten von Fehlern detaillierte Fehlerinformationen an Sharkey weitergegeben, was zur Verbesserung der Qualität von Sharkey beiträgt.\nEnthalten in diesen Informationen sind u.a. die Version deines Betriebssystems, welchen Browser du verwendest und ein Verlauf deiner Aktivitäten innerhalb Sharkey."
myTheme: "Mein Farbschema" myTheme: "Mein Farbschema"
backgroundColor: "Hintergrundfarbe" backgroundColor: "Hintergrundfarbe"
accentColor: "Akzentfarbe" accentColor: "Akzentfarbe"
@ -835,7 +835,7 @@ hashtags: "Hashtags"
troubleshooting: "Problembehandlung" troubleshooting: "Problembehandlung"
useBlurEffect: "Weichzeichnungseffekt in der Benutzeroberfläche verwenden" useBlurEffect: "Weichzeichnungseffekt in der Benutzeroberfläche verwenden"
learnMore: "Mehr erfahren" learnMore: "Mehr erfahren"
misskeyUpdated: "Misskey wurde aktualisiert!" misskeyUpdated: "Sharkey wurde aktualisiert!"
whatIsNew: "Änderungen anzeigen" whatIsNew: "Änderungen anzeigen"
translate: "Übersetzen" translate: "Übersetzen"
translatedFrom: "Aus {x} übersetzt" translatedFrom: "Aus {x} übersetzt"
@ -964,8 +964,8 @@ numberOfLikes: "\"Gefällt mir\"-Anzahl"
show: "Anzeigen" show: "Anzeigen"
neverShow: "Nicht wieder anzeigen" neverShow: "Nicht wieder anzeigen"
remindMeLater: "Vielleicht später" remindMeLater: "Vielleicht später"
didYouLikeMisskey: "Gefällt dir Misskey?" didYouLikeMisskey: "Gefällt dir Sharkey?"
pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" pleaseDonate: "Sharkey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!"
roles: "Rollen" roles: "Rollen"
role: "Rolle" role: "Rolle"
noRole: "Rolle nicht gefunden" noRole: "Rolle nicht gefunden"
@ -1075,7 +1075,7 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Diese Rollen müssen öffe
cancelReactionConfirm: "Möchtest du deine Reaktion wirklich löschen?" cancelReactionConfirm: "Möchtest du deine Reaktion wirklich löschen?"
changeReactionConfirm: "Möchtest du deine Reaktion wirklich ändern?" changeReactionConfirm: "Möchtest du deine Reaktion wirklich ändern?"
later: "Später" later: "Später"
goToMisskey: "Zu Misskey" goToMisskey: "Zu Sharkey"
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher" additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
installed: "Installiert" installed: "Installiert"
branding: "Branding" branding: "Branding"
@ -1141,7 +1141,7 @@ _initialAccountSetting:
pushNotificationDescription: "Durch die Aktivierung von Push-Benachrichtigungen kannst du von {name} Benachrichtigungen direkt auf dein Gerät erhalten." pushNotificationDescription: "Durch die Aktivierung von Push-Benachrichtigungen kannst du von {name} Benachrichtigungen direkt auf dein Gerät erhalten."
initialAccountSettingCompleted: "Kontoeinrichtung abgeschlossen!" initialAccountSettingCompleted: "Kontoeinrichtung abgeschlossen!"
haveFun: "Viel Spaß mit {name}!" haveFun: "Viel Spaß mit {name}!"
ifYouNeedLearnMore: "Besuche {link}, falls du mehr über {name} (Misskey) lernen möchtest." ifYouNeedLearnMore: "Besuche {link}, falls du mehr über {name} (Sharkey) lernen möchtest."
skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?" skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?"
laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?" laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?"
_serverRules: _serverRules:
@ -1163,7 +1163,7 @@ _accountMigration:
moveTo: "Dieses Konto zu einem neuen migrieren" moveTo: "Dieses Konto zu einem neuen migrieren"
moveToLabel: "Umzugsziel:" moveToLabel: "Umzugsziel:"
moveCannotBeUndone: "Die Migration eines Benutzerkontos ist unwiderruflich." moveCannotBeUndone: "Die Migration eines Benutzerkontos ist unwiderruflich."
moveAccountDescription: "Hierdurch wird dein Konto zu einem anderen migriert.\n ・Follower von diesem Konto werden automatisch auf das neue Konto migriert\n ・Dieses Konto wird allen Nutzern, denen es derzeit folgt, nicht mehr folgen\n ・Mit diesem Konto können keine neuen Notizen usw. erstellt werden\n\nWährend die Migration der Follower automatisch erfolgt, muss die Migration der Konten, denen du folgst, manuell vorbereitet werden. Exportiere hierzu die Liste der gefolgten Nutzer über das Einstellungsmenu, und importiere diese Liste im neuen Konto. Das gleiche Verfahren gilt für erstellte Listen und stummgeschaltete oder blockierte Nutzer.\n\n(Diese Erklärung gilt für Misskey v13.12.0 oder später. Die Funktionsweise andere ActivityPub-Software, beispielsweise Mastodon, kann hiervon abweichen.)" moveAccountDescription: "Hierdurch wird dein Konto zu einem anderen migriert.\n ・Follower von diesem Konto werden automatisch auf das neue Konto migriert\n ・Dieses Konto wird allen Nutzern, denen es derzeit folgt, nicht mehr folgen\n ・Mit diesem Konto können keine neuen Notizen usw. erstellt werden\n\nWährend die Migration der Follower automatisch erfolgt, muss die Migration der Konten, denen du folgst, manuell vorbereitet werden. Exportiere hierzu die Liste der gefolgten Nutzer über das Einstellungsmenu, und importiere diese Liste im neuen Konto. Das gleiche Verfahren gilt für erstellte Listen und stummgeschaltete oder blockierte Nutzer.\n\n(Diese Erklärung gilt für Sharkey v13.12.0 oder später. Die Funktionsweise andere ActivityPub-Software, beispielsweise Mastodon, kann hiervon abweichen.)"
moveAccountHowTo: "Um ein Konto zu migrieren, erstelle zuerst auf dem Umzugsziel einen Alias für dieses Konto.\nGib dann das Umzugsziel in folgendem Format ein: @username@server.example.com" moveAccountHowTo: "Um ein Konto zu migrieren, erstelle zuerst auf dem Umzugsziel einen Alias für dieses Konto.\nGib dann das Umzugsziel in folgendem Format ein: @username@server.example.com"
startMigration: "Migrieren" startMigration: "Migrieren"
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden." migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden."
@ -1174,9 +1174,9 @@ _achievements:
earnedAt: "Freigeschaltet am" earnedAt: "Freigeschaltet am"
_types: _types:
_notes1: _notes1:
title: "Hallo Misskey!" title: "Hallo Sharkey!"
description: "Sende deine erste Notiz" description: "Sende deine erste Notiz"
flavor: "Hab eine schöne Zeit mit Misskey!" flavor: "Hab eine schöne Zeit mit Sharkey!"
_notes10: _notes10:
title: "Ein paar Notizen" title: "Ein paar Notizen"
description: "10 Notizen gesendet" description: "10 Notizen gesendet"
@ -1272,7 +1272,7 @@ _achievements:
_login1000: _login1000:
title: "Meister der Notizen Ⅲ" title: "Meister der Notizen Ⅲ"
description: "An 1000 Tagen eingeloggt" description: "An 1000 Tagen eingeloggt"
flavor: "Danke, dass du Misskey nutzt!" flavor: "Danke, dass du Sharkey nutzt!"
_noteClipped1: _noteClipped1:
title: "Muss... clippen..." title: "Muss... clippen..."
description: "Die erste Notiz geclippt" description: "Die erste Notiz geclippt"
@ -1332,18 +1332,18 @@ _achievements:
title: "Fan von Errungenschaften" title: "Fan von Errungenschaften"
description: "Schau dir die Liste deiner Errungenschaften für mindestens 3 Minuten an" description: "Schau dir die Liste deiner Errungenschaften für mindestens 3 Minuten an"
_iLoveMisskey: _iLoveMisskey:
title: "I Love Misskey" title: "I Love Sharkey"
description: "Sende \"I ❤ #Misskey\"" description: "Sende \"I ❤ #Sharkey\""
flavor: "Danke, dass du Misskey verwendest! - vom Entwicklerteam" flavor: "Danke, dass du Sharkey verwendest! - vom Entwicklerteam"
_foundTreasure: _foundTreasure:
title: "Schatzsuche" title: "Schatzsuche"
description: "Du hast einen verborgenen Schatz gefunden" description: "Du hast einen verborgenen Schatz gefunden"
_client30min: _client30min:
title: "Kurze Pause" title: "Kurze Pause"
description: "Habe Misskey für mindestens 30 Minuten geöffnet" description: "Habe Sharkey für mindestens 30 Minuten geöffnet"
_client60min: _client60min:
title: "Munter mit Misskey" title: "Munter mit Sharkey"
description: "Habe Misskey für mindestens 60 Minuten geöffnet" description: "Habe Sharkey für mindestens 60 Minuten geöffnet"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "Ups" title: "Ups"
description: "Lösche eine Notiz innerhalb von 1 Minute nachdem sie gesendet wurde" description: "Lösche eine Notiz innerhalb von 1 Minute nachdem sie gesendet wurde"
@ -1711,7 +1711,7 @@ _time:
hour: "Stunde(n)" hour: "Stunde(n)"
day: "Tag(en)" day: "Tag(en)"
_timelineTutorial: _timelineTutorial:
title: "Wie du Misskey verwendest" title: "Wie du Sharkey verwendest"
step1_1: "Dieser Bildschirm ist die \"Chronik\". Hier werden alle \"Notizen\" von {name} angezeigt." step1_1: "Dieser Bildschirm ist die \"Chronik\". Hier werden alle \"Notizen\" von {name} angezeigt."
step1_2: "Es gibt einige verschiedene Chroniken. Beispielsweise werden in der \"Startseite\" alle Notizen von Nutzern, denen du folgst, angezeigt, und in der \"Lokalen Chronik\" werden Notizen aller Nutzer auf {name} angezeigt." step1_2: "Es gibt einige verschiedene Chroniken. Beispielsweise werden in der \"Startseite\" alle Notizen von Nutzern, denen du folgst, angezeigt, und in der \"Lokalen Chronik\" werden Notizen aller Nutzer auf {name} angezeigt."
step2_1: "Lass uns als nächstes versuchen, eine Notiz zu schreiben. Dies kannst du tun, indem du auf den Knopf mit dem Stift-Icon drückst." step2_1: "Lass uns als nächstes versuchen, eine Notiz zu schreiben. Dies kannst du tun, indem du auf den Knopf mit dem Stift-Icon drückst."

View file

@ -221,7 +221,7 @@ noUsers: "There are no users"
editProfile: "Edit profile" editProfile: "Edit profile"
noteDeleteConfirm: "Are you sure you want to delete this note?" noteDeleteConfirm: "Are you sure you want to delete this note?"
pinLimitExceeded: "You cannot pin any more notes" pinLimitExceeded: "You cannot pin any more notes"
intro: "Installation of Misskey has been finished! Please create an admin user." intro: "Installation of Sharkey has been finished! Please create an admin user."
done: "Done" done: "Done"
processing: "Processing..." processing: "Processing..."
preview: "Preview" preview: "Preview"
@ -741,7 +741,7 @@ onlineUsersCount: "{n} users are online"
nUsers: "{n} Users" nUsers: "{n} Users"
nNotes: "{n} Notes" nNotes: "{n} Notes"
sendErrorReports: "Send error reports" sendErrorReports: "Send error reports"
sendErrorReportsDescription: "When turned on, detailed error information will be shared with Misskey when a problem occurs, helping to improve the quality of Misskey.\nThis will include information such the version of your OS, what browser you're using, your activity in Misskey, etc." sendErrorReportsDescription: "When turned on, detailed error information will be shared with Sharkey when a problem occurs, helping to improve the quality of Sharkey.\nThis will include information such the version of your OS, what browser you're using, your activity in Sharkey, etc."
myTheme: "My theme" myTheme: "My theme"
backgroundColor: "Background color" backgroundColor: "Background color"
accentColor: "Accent color" accentColor: "Accent color"
@ -835,7 +835,7 @@ hashtags: "Hashtags"
troubleshooting: "Troubleshooting" troubleshooting: "Troubleshooting"
useBlurEffect: "Use blur effects in the UI" useBlurEffect: "Use blur effects in the UI"
learnMore: "Learn more" learnMore: "Learn more"
misskeyUpdated: "Misskey has been updated!" misskeyUpdated: "Sharkey has been updated!"
whatIsNew: "Show changes" whatIsNew: "Show changes"
translate: "Translate" translate: "Translate"
translatedFrom: "Translated from {x}" translatedFrom: "Translated from {x}"
@ -964,8 +964,8 @@ numberOfLikes: "Likes"
show: "Show" show: "Show"
neverShow: "Don't show again" neverShow: "Don't show again"
remindMeLater: "Maybe later" remindMeLater: "Maybe later"
didYouLikeMisskey: "Have you taken a liking to Misskey?" didYouLikeMisskey: "Have you taken a liking to Sharkey?"
pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!" pleaseDonate: "{host} uses the free software, Sharkey. We would highly appreciate your donations so development of Sharkey can continue!"
roles: "Roles" roles: "Roles"
role: "Role" role: "Role"
noRole: "Role not found" noRole: "Role not found"
@ -1075,7 +1075,7 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "These roles must be public
cancelReactionConfirm: "Really delete your reaction?" cancelReactionConfirm: "Really delete your reaction?"
changeReactionConfirm: "Really change your reaction?" changeReactionConfirm: "Really change your reaction?"
later: "Later" later: "Later"
goToMisskey: "To Misskey" goToMisskey: "To Sharkey"
additionalEmojiDictionary: "Additional emoji dictionaries" additionalEmojiDictionary: "Additional emoji dictionaries"
installed: "Installed" installed: "Installed"
branding: "Branding" branding: "Branding"
@ -1120,6 +1120,7 @@ notifyNotes: "Notify about new notes"
unnotifyNotes: "Stop notifying about new notes" unnotifyNotes: "Stop notifying about new notes"
authentication: "Authentication" authentication: "Authentication"
authenticationRequiredToContinue: "Please authenticate to continue" authenticationRequiredToContinue: "Please authenticate to continue"
showRenotes: "Include renotes"
_announcement: _announcement:
forExistingUsers: "Existing users only" forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1141,7 +1142,7 @@ _initialAccountSetting:
pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device." pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device."
initialAccountSettingCompleted: "Profile setup complete!" initialAccountSettingCompleted: "Profile setup complete!"
haveFun: "Enjoy {name}!" haveFun: "Enjoy {name}!"
ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}." ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Sharkey), please visit {link}."
skipAreYouSure: "Really skip profile setup?" skipAreYouSure: "Really skip profile setup?"
laterAreYouSure: "Really do profile setup later?" laterAreYouSure: "Really do profile setup later?"
_serverRules: _serverRules:
@ -1163,7 +1164,7 @@ _accountMigration:
moveTo: "Migrate this account to a different one" moveTo: "Migrate this account to a different one"
moveToLabel: "Account to move to:" moveToLabel: "Account to move to:"
moveCannotBeUndone: "Account migration cannot be undone." moveCannotBeUndone: "Account migration cannot be undone."
moveAccountDescription: "This will migrate your account to a different one.\n ・Followers from this account will automatically be migrated to the new account\n ・This account will unfollow all users it is currently following\n ・You will be unable to create new notes etc. on this account\n\nWhile migration of followers is automatic, you must manually prepare some steps to migrate the list of users you are following. To do so, carry out a follows export that you will later import on the new account in the settings menu. The same procedure applies to your lists as well as your muted and blocked users.\n\n(This explanation applies to Misskey v13.12.0 and later. Other ActivityPub software, such as Mastodon, might function differently.)" moveAccountDescription: "This will migrate your account to a different one.\n ・Followers from this account will automatically be migrated to the new account\n ・This account will unfollow all users it is currently following\n ・You will be unable to create new notes etc. on this account\n\nWhile migration of followers is automatic, you must manually prepare some steps to migrate the list of users you are following. To do so, carry out a follows export that you will later import on the new account in the settings menu. The same procedure applies to your lists as well as your muted and blocked users.\n\n(This explanation applies to Sharkey v13.12.0 and later. Other ActivityPub software, such as Mastodon, might function differently.)"
moveAccountHowTo: "To migrate, first create an alias for this account on the account to move to.\nAfter you have created the alias, enter the account to move to in the following format: @username@server.example.com" moveAccountHowTo: "To migrate, first create an alias for this account on the account to move to.\nAfter you have created the alias, enter the account to move to in the following format: @username@server.example.com"
startMigration: "Migrate" startMigration: "Migrate"
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore." migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore."
@ -1176,7 +1177,7 @@ _achievements:
_notes1: _notes1:
title: "just setting up my msky" title: "just setting up my msky"
description: "Post your first note" description: "Post your first note"
flavor: "Have a good time with Misskey!" flavor: "Have a good time with Sharkey!"
_notes10: _notes10:
title: "Some notes" title: "Some notes"
description: "Post 10 notes" description: "Post 10 notes"
@ -1272,7 +1273,7 @@ _achievements:
_login1000: _login1000:
title: "Master of Notes III" title: "Master of Notes III"
description: "Log in for a total of 1,000 days" description: "Log in for a total of 1,000 days"
flavor: "Thank you for using Misskey!" flavor: "Thank you for using Sharkey!"
_noteClipped1: _noteClipped1:
title: "Must... clip..." title: "Must... clip..."
description: "Clip your first note" description: "Clip your first note"
@ -1332,18 +1333,18 @@ _achievements:
title: "Likes Achievements" title: "Likes Achievements"
description: "Look at your list of achievements for at least 3 minutes" description: "Look at your list of achievements for at least 3 minutes"
_iLoveMisskey: _iLoveMisskey:
title: "I Love Misskey" title: "I Love Sharkey"
description: "Post \"I ❤ #Misskey\"" description: "Post \"I ❤ #Sharkey\""
flavor: "Misskey's development team greatly appreciates your support!" flavor: "Sharkey's development team greatly appreciates your support!"
_foundTreasure: _foundTreasure:
title: "Treasure Hunt" title: "Treasure Hunt"
description: "You've found the hidden treasure" description: "You've found the hidden treasure"
_client30min: _client30min:
title: "Short break" title: "Short break"
description: "Keep Misskey opened for at least 30 minutes" description: "Keep Sharkey opened for at least 30 minutes"
_client60min: _client60min:
title: "No \"Miss\" in Misskey" title: "No \"Miss\" in Sharkey"
description: "Keep Misskey opened for at least 60 minutes" description: "Keep Sharkey opened for at least 60 minutes"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "Nevermind" title: "Nevermind"
description: "Delete a note within a minute of posting it" description: "Delete a note within a minute of posting it"
@ -1711,7 +1712,7 @@ _time:
hour: "Hour(s)" hour: "Hour(s)"
day: "Day(s)" day: "Day(s)"
_timelineTutorial: _timelineTutorial:
title: "How to use Misskey" title: "How to use Sharkey"
step1_1: "This is the \"timeline\". All \"notes\" submitted on {name} will be chronologically displayed here." step1_1: "This is the \"timeline\". All \"notes\" submitted on {name} will be chronologically displayed here."
step1_2: "There are a few different timelines. For example, the \"Home timeline\" will contain notes of users you follow, and the \"Local timeline\" will contain notes from all users of {name}." step1_2: "There are a few different timelines. For example, the \"Home timeline\" will contain notes of users you follow, and the \"Local timeline\" will contain notes from all users of {name}."
step2_1: "Let's try posting a note next. You can do so by pressing the button with a pencil icon." step2_1: "Let's try posting a note next. You can do so by pressing the button with a pencil icon."

2
locales/index.d.ts vendored
View file

@ -1124,6 +1124,7 @@ export interface Locale {
"authentication": string; "authentication": string;
"authenticationRequiredToContinue": string; "authenticationRequiredToContinue": string;
"dateAndTime": string; "dateAndTime": string;
"showRenotes": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;
@ -2277,6 +2278,7 @@ export interface Locale {
"markSensitiveDriveFile": string; "markSensitiveDriveFile": string;
"unmarkSensitiveDriveFile": string; "unmarkSensitiveDriveFile": string;
"resolveAbuseReport": string; "resolveAbuseReport": string;
"createInvitation": string;
}; };
} }
declare const locales: { declare const locales: {

View file

@ -1121,6 +1121,7 @@ unnotifyNotes: "投稿の通知を解除"
authentication: "認証" authentication: "認証"
authenticationRequiredToContinue: "続けるには認証を行ってください" authenticationRequiredToContinue: "続けるには認証を行ってください"
dateAndTime: "日時" dateAndTime: "日時"
showRenotes: "リノートを表示"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
@ -2190,3 +2191,4 @@ _moderationLogTypes:
markSensitiveDriveFile: "ファイルをセンシティブ付与" markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決" resolveAbuseReport: "通報を解決"
createInvitation: "招待コードを作成"

View file

@ -10,6 +10,7 @@ import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { generateInviteCode } from '@/misc/generate-invite-code.js'; import { generateInviteCode } from '@/misc/generate-invite-code.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -60,6 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private inviteCodeEntityService: InviteCodeEntityService, private inviteCodeEntityService: InviteCodeEntityService,
private idService: IdService, private idService: IdService,
private moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) { if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) {
@ -78,6 +80,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
const tickets = await Promise.all(ticketsPromises); const tickets = await Promise.all(ticketsPromises);
this.moderationLogService.log(me, 'createInvitation', {
invitations: tickets,
});
return await this.inviteCodeEntityService.packMany(tickets, me); return await this.inviteCodeEntityService.packMany(tickets, me);
}); });
} }

View file

@ -4,6 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import type { NotesRepository } from '@/models/_.js'; import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
@ -40,6 +41,7 @@ export const paramDef = {
properties: { properties: {
withFiles: { type: 'boolean', default: false }, withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false }, withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
@ -88,6 +90,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');
} }
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
//#endregion //#endregion
const timeline = await query.limit(ps.limit).getMany(); const timeline = await query.limit(ps.limit).getMany();

View file

@ -52,6 +52,7 @@ export const paramDef = {
includeLocalRenotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean', default: false }, withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false }, withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
}, },
required: [], required: [],
} as const; } as const;
@ -137,6 +138,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');
} }
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
//#endregion //#endregion
const timeline = await query.limit(ps.limit).getMany(); const timeline = await query.limit(ps.limit).getMany();

View file

@ -42,6 +42,7 @@ export const paramDef = {
properties: { properties: {
withFiles: { type: 'boolean', default: false }, withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false }, withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
fileType: { type: 'array', items: { fileType: { type: 'array', items: {
type: 'string', type: 'string',
} }, } },
@ -110,6 +111,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
} }
} }
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
//#endregion //#endregion
const timeline = await query.limit(ps.limit).getMany(); const timeline = await query.limit(ps.limit).getMany();

View file

@ -42,6 +42,7 @@ export const paramDef = {
includeLocalRenotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean', default: false }, withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false }, withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
}, },
required: [], required: [],
} as const; } as const;
@ -126,6 +127,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');
} }
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
//#endregion //#endregion
const timeline = await query.limit(ps.limit).getMany(); const timeline = await query.limit(ps.limit).getMany();

View file

@ -49,6 +49,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true }, includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withFiles: { withFiles: {
type: 'boolean', type: 'boolean',
default: false, default: false,
@ -130,6 +132,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
})); }));
} }
if (!ps.withReplies) {
query.andWhere('note.replyId IS NULL');
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');
} }

View file

@ -41,7 +41,8 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
userId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' },
includeReplies: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
@ -114,10 +115,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
} }
if (!ps.includeReplies) { if (!ps.withReplies) {
query.andWhere('note.replyId IS NULL'); query.andWhere('note.replyId IS NULL');
} }
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.includeMyRenotes === false) { if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => { query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :userId', { userId: user.id }); qb.orWhere('note.userId != :userId', { userId: user.id });

View file

@ -19,6 +19,7 @@ class GlobalTimelineChannel extends Channel {
public static shouldShare = true; public static shouldShare = true;
public static requireCredential = false; public static requireCredential = false;
private withReplies: boolean; private withReplies: boolean;
private withRenotes: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -37,7 +38,8 @@ class GlobalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return; if (!policies.gtlAvailable) return;
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -68,6 +70,8 @@ class GlobalTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
} }
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// Ignore notes from instances the user has muted // Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;

View file

@ -17,6 +17,7 @@ class HomeTimelineChannel extends Channel {
public static shouldShare = true; public static shouldShare = true;
public static requireCredential = true; public static requireCredential = true;
private withReplies: boolean; private withReplies: boolean;
private withRenotes: boolean;
constructor( constructor(
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -30,7 +31,8 @@ class HomeTimelineChannel extends Channel {
@bindThis @bindThis
public async init(params: any) { public async init(params: any) {
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
} }
@ -77,6 +79,8 @@ class HomeTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
} }
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return; if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View file

@ -19,6 +19,7 @@ class HybridTimelineChannel extends Channel {
public static shouldShare = true; public static shouldShare = true;
public static requireCredential = true; public static requireCredential = true;
private withReplies: boolean; private withReplies: boolean;
private withRenotes: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -37,7 +38,8 @@ class HybridTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return; if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -89,6 +91,8 @@ class HybridTimelineChannel extends Channel {
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
} }
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return; if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View file

@ -18,6 +18,7 @@ class LocalTimelineChannel extends Channel {
public static shouldShare = true; public static shouldShare = true;
public static requireCredential = false; public static requireCredential = false;
private withReplies: boolean; private withReplies: boolean;
private withRenotes: boolean;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
@ -36,7 +37,8 @@ class LocalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return; if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
@ -68,6 +70,8 @@ class LocalTimelineChannel extends Channel {
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
} }
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return; if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View file

@ -56,6 +56,7 @@ export const moderationLogTypes = [
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
'createInvitation',
] as const; ] as const;
export type ModerationLogPayloads = { export type ModerationLogPayloads = {
@ -198,4 +199,7 @@ export type ModerationLogPayloads = {
report: any; report: any;
forwarded: boolean; forwarded: boolean;
}; };
createInvitation: {
invitations: any[];
};
}; };

View file

@ -821,8 +821,10 @@ function showActions(ev) {
action: () => { action: () => {
action.handler({ action.handler({
text: text, text: text,
cw: cw,
}, (key, value) => { }, (key, value) => {
if (key === 'text') { text = value; } if (key === 'text') { text = value; }
if (key === 'cw') { useCw = value !== null; cw = value; }
}); });
}, },
})), ev.currentTarget ?? ev.target); })), ev.currentTarget ?? ev.target);

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emojiUrls="note.emojis"/> <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emojiUrls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" v-on:click.stop>RN: ...</MkA> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" v-on:click.stop>RN: ...</MkA>
</div> </div>
<details v-if="note.files.length > 0"> <details v-if="note.files.length > 0" :open="!defaultStore.state.collapseFiles">
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary> <summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
<MkMediaList :mediaList="note.files"/> <MkMediaList :mediaList="note.files"/>
</details> </details>
@ -37,6 +37,7 @@ import MkPoll from '@/components/MkPoll.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { shouldCollapsed } from '@/scripts/collapsed.js'; import { shouldCollapsed } from '@/scripts/collapsed.js';
import { defaultStore } from '@/store.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
const props = defineProps<{ const props = defineProps<{

View file

@ -15,14 +15,19 @@ import * as sound from '@/scripts/sound.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = defineProps<{ const props = withDefaults(defineProps<{
src: string; src: string;
list?: string; list?: string;
antenna?: string; antenna?: string;
channel?: string; channel?: string;
role?: string; role?: string;
sound?: boolean; sound?: boolean;
}>(); withRenotes?: boolean;
withReplies?: boolean;
}>(), {
withRenotes: true,
withReplies: false,
});
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'note'): void; (ev: 'note'): void;
@ -62,10 +67,12 @@ if (props.src === 'antenna') {
} else if (props.src === 'home') { } else if (props.src === 'home') {
endpoint = 'notes/timeline'; endpoint = 'notes/timeline';
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}; };
connection = stream.useChannel('homeTimeline', { connection = stream.useChannel('homeTimeline', {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}); });
connection.on('note', prepend); connection.on('note', prepend);
@ -73,28 +80,34 @@ if (props.src === 'antenna') {
} else if (props.src === 'local') { } else if (props.src === 'local') {
endpoint = 'notes/local-timeline'; endpoint = 'notes/local-timeline';
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}; };
connection = stream.useChannel('localTimeline', { connection = stream.useChannel('localTimeline', {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}); });
connection.on('note', prepend); connection.on('note', prepend);
} else if (props.src === 'social') { } else if (props.src === 'social') {
endpoint = 'notes/hybrid-timeline'; endpoint = 'notes/hybrid-timeline';
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}; };
connection = stream.useChannel('hybridTimeline', { connection = stream.useChannel('hybridTimeline', {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}); });
connection.on('note', prepend); connection.on('note', prepend);
} else if (props.src === 'global') { } else if (props.src === 'global') {
endpoint = 'notes/global-timeline'; endpoint = 'notes/global-timeline';
query = { query = {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}; };
connection = stream.useChannel('globalTimeline', { connection = stream.useChannel('globalTimeline', {
withReplies: defaultStore.state.showTimelineReplies, withRenotes: props.withRenotes,
withReplies: props.withReplies,
}); });
connection.on('note', prepend); connection.on('note', prepend);
} else if (props.src === 'mentions') { } else if (props.src === 'mentions') {
@ -116,9 +129,13 @@ if (props.src === 'antenna') {
} else if (props.src === 'list') { } else if (props.src === 'list') {
endpoint = 'notes/user-list-timeline'; endpoint = 'notes/user-list-timeline';
query = { query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
listId: props.list, listId: props.list,
}; };
connection = stream.useChannel('userList', { connection = stream.useChannel('userList', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
listId: props.list, listId: props.list,
}); });
connection.on('note', prepend); connection.on('note', prepend);

View file

@ -114,8 +114,7 @@ function showMenu(ev) {
} }
function exploreOtherServers() { function exploreOtherServers() {
// TODO: window.open('https://join.misskey.page/instances', '_blank');
window.open('https://join.misskey.page/ja-JP/instances', '_blank');
} }
</script> </script>

View file

@ -45,7 +45,7 @@ import { onMounted, onUnmounted, ref, inject } from 'vue';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import XTabs, { Tab } from './MkPageHeader.tabs.vue'; import XTabs, { Tab } from './MkPageHeader.tabs.vue';
import { scrollToTop } from '@/scripts/scroll.js'; import { scrollToTop } from '@/scripts/scroll.js';
import { globalEvents } from '@/events'; import { globalEvents } from '@/events.js';
import { injectPageMetadata } from '@/scripts/page-metadata.js'; import { injectPageMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';

View file

@ -84,20 +84,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</a> </a>
</div> </div>
</FormSection> </FormSection>
<FormSection>
<template #label>Special thanks</template>
<div class="_gaps" style="text-align: center;">
<div>
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
</div>
<div>
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
</div>
<div>
<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="100" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
</div>
</div>
</FormSection>
</div> </div>
</MkSpacer> </MkSpacer>
</div> </div>

View file

@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span> <span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
<span v-else-if="log.type === 'updateRole'">: {{ log.info.before.name }}</span> <span v-else-if="log.type === 'updateRole'">: {{ log.info.before.name }}</span>
<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span> <span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
<span v-else-if="log.type === 'addCustomEmoji'">: {{ log.info.emoji.name }}</span>
<span v-else-if="log.type === 'updateCustomEmoji'">: {{ log.info.before.name }}</span> <span v-else-if="log.type === 'updateCustomEmoji'">: {{ log.info.before.name }}</span>
<span v-else-if="log.type === 'markSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span> <span v-else-if="log.type === 'markSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span> <span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>

View file

@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch> <MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch> <MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts.pinnedList }}</template> <template #label>{{ i18n.ts.pinnedList }}</template>
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ --> <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
@ -47,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch> <MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
<MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch> <MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch> <MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
<MkSwitch v-model="collapseFiles">Collapse files</MkSwitch>
<MkSwitch v-model="autoloadConversation">Load conversation on replies</MkSwitch> <MkSwitch v-model="autoloadConversation">Load conversation on replies</MkSwitch>
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch> <MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
@ -225,6 +225,7 @@ const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNot
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter')); const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize')); const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const collapseFiles = computed(defaultStore.makeGetterSetter('collapseFiles'));
const autoloadConversation = computed(defaultStore.makeGetterSetter('autoloadConversation')); const autoloadConversation = computed(defaultStore.makeGetterSetter('autoloadConversation'));
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
@ -251,7 +252,6 @@ const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance')); const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance'));
const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
const showTimelineReplies = computed(defaultStore.makeGetterSetter('showTimelineReplies'));
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
watch(lang, () => { watch(lang, () => {

View file

@ -15,9 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tl"> <div :class="$style.tl">
<MkTimeline <MkTimeline
ref="tlComponent" ref="tlComponent"
:key="src" :key="src + withRenotes + withReplies"
:src="src.split(':')[0]" :src="src.split(':')[0]"
:list="src.split(':')[1]" :list="src.split(':')[1]"
:withRenotes="withRenotes"
:withReplies="withReplies"
:sound="true" :sound="true"
@queue="queueUpdated" @queue="queueUpdated"
/> />
@ -58,6 +60,8 @@ const rootEl = $shallowRef<HTMLElement>();
let queue = $ref(0); let queue = $ref(0);
let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global'); let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global');
const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) }); const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
const withRenotes = $ref(true);
const withReplies = $ref(false);
watch($$(src), () => queue = 0); watch($$(src), () => queue = 0);
@ -129,7 +133,23 @@ function focus(): void {
tlComponent.focus(); tlComponent.focus();
} }
const headerActions = $computed(() => []); const headerActions = $computed(() => [{
icon: 'ti ti-dots',
text: i18n.ts.options,
handler: (ev) => {
os.popupMenu([{
type: 'switch',
text: i18n.ts.showRenotes,
icon: 'ti ti-repeat',
ref: $$(withRenotes),
}, {
type: 'switch',
text: i18n.ts.withReplies,
icon: 'ti ti-arrow-back-up',
ref: $$(withReplies),
}], ev.currentTarget ?? ev.target);
},
}]);
const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
key: 'list:' + l.id, key: 'list:' + l.id,

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header> <template #header>
<MkTab v-model="include" :class="$style.tab"> <MkTab v-model="include" :class="$style.tab">
<option :value="null">{{ i18n.ts.notes }}</option> <option :value="null">{{ i18n.ts.notes }}</option>
<option value="replies">{{ i18n.ts.notesAndReplies }}</option> <option value="all">{{ i18n.ts.all }}</option>
<option value="files">{{ i18n.ts.withFiles }}</option> <option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab> </MkTab>
</template> </template>
@ -36,7 +36,8 @@ const pagination = {
limit: 10, limit: 10,
params: computed(() => ({ params: computed(() => ({
userId: props.user.id, userId: props.user.id,
includeReplies: include.value === 'replies' || include.value === 'files', withRenotes: include.value === 'all',
withReplies: include.value === 'all' || include.value === 'files',
withFiles: include.value === 'files', withFiles: include.value === 'files',
})), })),
}; };
@ -51,7 +52,7 @@ const pagination = {
.tl { .tl {
background: var(--bg); background: var(--bg);
border-radius: var(--radius); border-radius: var(--radius);
overflow: clip; overflow: clip;
} }
</style> </style>

View file

@ -65,6 +65,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account', where: 'account',
default: true, default: true,
}, },
collapseFiles: {
where: 'account',
default: true,
},
rememberNoteVisibility: { rememberNoteVisibility: {
where: 'account', where: 'account',
default: false, default: false,
@ -127,7 +131,6 @@ export const defaultStore = markRaw(new Storage('base', {
'-', '-',
'announcements', 'announcements',
'search', 'search',
'lookup',
'-', '-',
'favorites', 'favorites',
'drive', 'drive',

View file

@ -30,6 +30,8 @@ export type Column = {
roleId?: string; roleId?: string;
includingTypes?: typeof notificationTypes[number][]; includingTypes?: typeof notificationTypes[number][];
tl?: 'home' | 'local' | 'social' | 'global'; tl?: 'home' | 'local' | 'social' | 'global';
withRenotes?: boolean;
withReplies?: boolean;
}; };
export const deckStore = markRaw(new Storage('deck', { export const deckStore = markRaw(new Storage('deck', {

View file

@ -20,12 +20,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</p> </p>
<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p> <p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
</div> </div>
<MkTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl"/> <MkTimeline
v-else-if="column.tl"
ref="timeline"
:key="column.tl + withRenotes + withReplies"
:src="column.tl"
:withRenotes="withRenotes"
:withReplies="withReplies"
/>
</XColumn> </XColumn>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted, watch } from 'vue';
import XColumn from './column.vue'; import XColumn from './column.vue';
import { removeColumn, updateColumn, Column } from './deck-store.js'; import { removeColumn, updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue'; import MkTimeline from '@/components/MkTimeline.vue';
@ -43,6 +50,20 @@ let disabled = $ref(false);
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
const withRenotes = $ref(props.column.withRenotes ?? true);
const withReplies = $ref(props.column.withReplies ?? false);
watch($$(withRenotes), v => {
updateColumn(props.column.id, {
withRenotes: v,
});
});
watch($$(withReplies), v => {
updateColumn(props.column.id, {
withReplies: v,
});
});
onMounted(() => { onMounted(() => {
if (props.column.tl == null) { if (props.column.tl == null) {
@ -82,6 +103,14 @@ const menu = [{
icon: 'ti ti-pencil', icon: 'ti ti-pencil',
text: i18n.ts.timeline, text: i18n.ts.timeline,
action: setType, action: setType,
}, {
type: 'switch',
text: i18n.ts.showRenotes,
ref: $$(withRenotes),
}, {
type: 'switch',
text: i18n.ts.withReplies,
ref: $$(withReplies),
}]; }];
</script> </script>

View file

@ -2604,10 +2604,13 @@ type ModerationLog = {
} | { } | {
type: 'unmarkSensitiveDriveFile'; type: 'unmarkSensitiveDriveFile';
info: ModerationLogPayloads['unmarkSensitiveDriveFile']; info: ModerationLogPayloads['unmarkSensitiveDriveFile'];
} | {
type: 'createInvitation';
info: ModerationLogPayloads['createInvitation'];
}); });
// @public (undocumented) // @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport"]; export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation"];
// @public (undocumented) // @public (undocumented)
export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"]; export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];

View file

@ -74,6 +74,7 @@ export const moderationLogTypes = [
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
'createInvitation',
] as const; ] as const;
export type ModerationLogPayloads = { export type ModerationLogPayloads = {
@ -216,4 +217,7 @@ export type ModerationLogPayloads = {
report: any; report: any;
forwarded: boolean; forwarded: boolean;
}; };
createInvitation: {
invitations: any[];
};
}; };

View file

@ -666,4 +666,7 @@ export type ModerationLog = {
} | { } | {
type: 'unmarkSensitiveDriveFile'; type: 'unmarkSensitiveDriveFile';
info: ModerationLogPayloads['unmarkSensitiveDriveFile']; info: ModerationLogPayloads['unmarkSensitiveDriveFile'];
} | {
type: 'createInvitation';
info: ModerationLogPayloads['createInvitation'];
}); });