Merge branch 'develop' of https://codeberg.org/calckey/calckey into notifications
|
@ -1,7 +1,8 @@
|
||||||
pipeline:
|
pipeline:
|
||||||
testCommit:
|
testCommit:
|
||||||
image: node:latest
|
image: node:alpine
|
||||||
commands:
|
commands:
|
||||||
|
- apk add --no-cache cargo python3 make g++
|
||||||
- cp .config/ci.yml .config/default.yml
|
- cp .config/ci.yml .config/default.yml
|
||||||
- corepack enable
|
- corepack enable
|
||||||
- corepack prepare pnpm@latest --activate
|
- corepack prepare pnpm@latest --activate
|
||||||
|
|
|
@ -27,9 +27,9 @@
|
||||||
- Notable differences:
|
- Notable differences:
|
||||||
- Improved UI/UX (especially on mobile)
|
- Improved UI/UX (especially on mobile)
|
||||||
- Improved notifications
|
- Improved notifications
|
||||||
- Fediverse account migration
|
|
||||||
- Improved instance security
|
- Improved instance security
|
||||||
- Improved accessibility
|
- Improved accessibility
|
||||||
|
- Improved threads
|
||||||
- Recommended Instances timeline
|
- Recommended Instances timeline
|
||||||
- OCR image captioning
|
- OCR image captioning
|
||||||
- New and improved Groups
|
- New and improved Groups
|
||||||
|
|
1037
locales/ca-ES.yml
|
@ -68,8 +68,8 @@ import: "Import"
|
||||||
export: "Export"
|
export: "Export"
|
||||||
files: "Files"
|
files: "Files"
|
||||||
download: "Download"
|
download: "Download"
|
||||||
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Posts\
|
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? It\
|
||||||
\ with this file attached will also be deleted."
|
\ will be removed from all posts that contain it as an attachment."
|
||||||
unfollowConfirm: "Are you sure that you want to unfollow {name}?"
|
unfollowConfirm: "Are you sure that you want to unfollow {name}?"
|
||||||
exportRequested: "You've requested an export. This may take a while. It will be added\
|
exportRequested: "You've requested an export. This may take a while. It will be added\
|
||||||
\ to your Drive once completed."
|
\ to your Drive once completed."
|
||||||
|
@ -197,6 +197,7 @@ perHour: "Per Hour"
|
||||||
perDay: "Per Day"
|
perDay: "Per Day"
|
||||||
stopActivityDelivery: "Stop sending activities"
|
stopActivityDelivery: "Stop sending activities"
|
||||||
blockThisInstance: "Block this instance"
|
blockThisInstance: "Block this instance"
|
||||||
|
silenceThisInstance: "Silence this instance"
|
||||||
operations: "Operations"
|
operations: "Operations"
|
||||||
software: "Software"
|
software: "Software"
|
||||||
version: "Version"
|
version: "Version"
|
||||||
|
@ -218,10 +219,13 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
|
||||||
blockedInstances: "Blocked Instances"
|
blockedInstances: "Blocked Instances"
|
||||||
blockedInstancesDescription: "List the hostnames of the instances that you want to\
|
blockedInstancesDescription: "List the hostnames of the instances that you want to\
|
||||||
\ block. Listed instances will no longer be able to communicate with this instance."
|
\ block. Listed instances will no longer be able to communicate with this instance."
|
||||||
|
silencedInstances: "Silenced Instances"
|
||||||
|
silencedInstancesDescription: "List the hostnames of the instances that you want to\
|
||||||
|
\ silence. Accounts in the listed instances are treated as \"Silenced\", can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
|
||||||
hiddenTags: "Hidden Hashtags"
|
hiddenTags: "Hidden Hashtags"
|
||||||
hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\
|
hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\
|
||||||
\ to hide from trending and explore. Hidden hashtags are still discoverable via\
|
\ to hide from trending and explore. Hidden hashtags are still discoverable via\
|
||||||
\ other means."
|
\ other means. Blocked instances are not affected even if listed here."
|
||||||
muteAndBlock: "Mutes and Blocks"
|
muteAndBlock: "Mutes and Blocks"
|
||||||
mutedUsers: "Muted users"
|
mutedUsers: "Muted users"
|
||||||
blockedUsers: "Blocked users"
|
blockedUsers: "Blocked users"
|
||||||
|
@ -240,6 +244,7 @@ noCustomEmojis: "There are no emoji"
|
||||||
noJobs: "There are no jobs"
|
noJobs: "There are no jobs"
|
||||||
federating: "Federating"
|
federating: "Federating"
|
||||||
blocked: "Blocked"
|
blocked: "Blocked"
|
||||||
|
silenced: "Silenced"
|
||||||
suspended: "Suspended"
|
suspended: "Suspended"
|
||||||
all: "All"
|
all: "All"
|
||||||
subscribing: "Subscribing"
|
subscribing: "Subscribing"
|
||||||
|
@ -829,7 +834,7 @@ active: "Active"
|
||||||
offline: "Offline"
|
offline: "Offline"
|
||||||
notRecommended: "Not recommended"
|
notRecommended: "Not recommended"
|
||||||
botProtection: "Bot Protection"
|
botProtection: "Bot Protection"
|
||||||
instanceBlocking: "Blocked Instances"
|
instanceBlocking: "Federation Block/Silence"
|
||||||
selectAccount: "Select account"
|
selectAccount: "Select account"
|
||||||
switchAccount: "Switch account"
|
switchAccount: "Switch account"
|
||||||
enabled: "Enabled"
|
enabled: "Enabled"
|
||||||
|
@ -1042,7 +1047,7 @@ moveFromLabel: "Account you're moving from:"
|
||||||
moveFromDescription: "This will set an alias of your old account so that you can move\
|
moveFromDescription: "This will set an alias of your old account so that you can move\
|
||||||
\ from that account to this current one. Do this BEFORE moving from your older account.\
|
\ from that account to this current one. Do this BEFORE moving from your older account.\
|
||||||
\ Please enter the tag of the account formatted like @person@instance.com"
|
\ Please enter the tag of the account formatted like @person@instance.com"
|
||||||
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}?\
|
migrationConfirm: "Are you absolutely sure you want to migrate your account to {account}?\
|
||||||
\ Once you do this, you won't be able to reverse it, and you won't be able to use\
|
\ Once you do this, you won't be able to reverse it, and you won't be able to use\
|
||||||
\ your account normally again.\nAlso, please ensure that you've set this current\
|
\ your account normally again.\nAlso, please ensure that you've set this current\
|
||||||
\ account as the account you're moving from."
|
\ account as the account you're moving from."
|
||||||
|
@ -1197,7 +1202,7 @@ _mfm:
|
||||||
inlineMath: "Math (Inline)"
|
inlineMath: "Math (Inline)"
|
||||||
inlineMathDescription: "Display math formulas (KaTeX) in-line"
|
inlineMathDescription: "Display math formulas (KaTeX) in-line"
|
||||||
blockMath: "Math (Block)"
|
blockMath: "Math (Block)"
|
||||||
blockMathDescription: "Display multi-line math formulas (KaTeX) in a block"
|
blockMathDescription: "Display math formulas (KaTeX) in a block"
|
||||||
quote: "Quote"
|
quote: "Quote"
|
||||||
quoteDescription: "Displays content as a quote."
|
quoteDescription: "Displays content as a quote."
|
||||||
emoji: "Custom Emoji"
|
emoji: "Custom Emoji"
|
||||||
|
@ -1237,6 +1242,14 @@ _mfm:
|
||||||
sparkleDescription: "Gives content a sparkling particle effect."
|
sparkleDescription: "Gives content a sparkling particle effect."
|
||||||
rotate: "Rotate"
|
rotate: "Rotate"
|
||||||
rotateDescription: "Turns content by a specified angle."
|
rotateDescription: "Turns content by a specified angle."
|
||||||
|
position: "Position"
|
||||||
|
positionDescription: "Move content by a specified amount."
|
||||||
|
scale: "Scale"
|
||||||
|
scaleDescription: "Scale content by a specified amount."
|
||||||
|
foreground: "Foreground color"
|
||||||
|
foregroundDescription: "Change the foreground color of text."
|
||||||
|
background: "Background color"
|
||||||
|
backgroundDescription: "Change the background color of text."
|
||||||
plain: "Plain"
|
plain: "Plain"
|
||||||
plainDescription: "Deactivates the effects of all MFM contained within this MFM\
|
plainDescription: "Deactivates the effects of all MFM contained within this MFM\
|
||||||
\ effect."
|
\ effect."
|
||||||
|
|
223
locales/fi.yml
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
username: Käyttäjänimi
|
||||||
|
fetchingAsApObject: Hae Fedeversestä
|
||||||
|
gotIt: Selvä!
|
||||||
|
cancel: Peruuta
|
||||||
|
enterUsername: Anna käyttäjänimi
|
||||||
|
renotedBy: Buustannut {user}
|
||||||
|
noNotes: Ei lähetyksiä
|
||||||
|
noNotifications: Ei ilmoituksia
|
||||||
|
instance: Instanssi
|
||||||
|
settings: Asetukset
|
||||||
|
basicSettings: Perusasetukset
|
||||||
|
otherSettings: Muut asetukset
|
||||||
|
openInWindow: Avaa ikkunaan
|
||||||
|
profile: Profiili
|
||||||
|
timeline: Aikajana
|
||||||
|
noAccountDescription: Käyttäjä ei ole vielä kirjoittanut kuvaustaan vielä.
|
||||||
|
login: Kirjaudu sisään
|
||||||
|
loggingIn: Kirjautuu sisään
|
||||||
|
logout: Kirjaudu ulos
|
||||||
|
uploading: Tallentaa ylös...
|
||||||
|
save: Tallenna
|
||||||
|
favorites: Kirjanmerkit
|
||||||
|
unfavorite: Poista kirjanmerkeistä
|
||||||
|
favorited: Lisätty kirjanmerkkeihin.
|
||||||
|
alreadyFavorited: Lisätty jo kirjanmerkkeihin.
|
||||||
|
cantFavorite: Ei voitu lisätä kirjanmerkkeihin.
|
||||||
|
pin: Kiinnitä profiiliin
|
||||||
|
unpin: Irroita profiilista
|
||||||
|
delete: Poista
|
||||||
|
forgotPassword: Unohtunut salasana
|
||||||
|
search: Etsi
|
||||||
|
notifications: Ilmoitukset
|
||||||
|
password: Salasana
|
||||||
|
ok: OK
|
||||||
|
noThankYou: Ei kiitos
|
||||||
|
signup: Rekisteröidy
|
||||||
|
users: Käyttäjät
|
||||||
|
addUser: Lisää käyttäjä
|
||||||
|
addInstance: Lisää instanssi
|
||||||
|
favorite: Lisää kirjanmerkkeihin
|
||||||
|
copyContent: Kopioi sisältö
|
||||||
|
deleteAndEdit: Poista ja muokkaa
|
||||||
|
copyLink: Kopioi linkki
|
||||||
|
makeFollowManuallyApprove: Seuraajapyyntö vaatii hyväksymistä
|
||||||
|
follow: Seuraa
|
||||||
|
pinned: Kiinnitä profiiliin
|
||||||
|
followRequestPending: Seuraajapyyntö odottaa
|
||||||
|
you: Sinä
|
||||||
|
unrenote: Peruuta buustaus
|
||||||
|
reaction: Reaktiot
|
||||||
|
reactionSettingDescription2: Vedä uudelleenjärjestelläksesi, napsauta poistaaksesi,
|
||||||
|
paina "+" lisätäksesi.
|
||||||
|
attachCancel: Poista liite
|
||||||
|
enterFileName: Anna tiedostonimi
|
||||||
|
mute: Hiljennä
|
||||||
|
unmute: Poista hiljennys
|
||||||
|
headlineMisskey: Avoimen lähdekoodin, hajautettu sosiaalisen median alusta, joka on
|
||||||
|
ikuisesti ilmainen! 🚀
|
||||||
|
monthAndDay: '{day}/{month}'
|
||||||
|
deleteAndEditConfirm: Oletko varma, että haluat poistaa tämän lähetyksen ja muokata
|
||||||
|
sitä? Menetät kaikki reaktiot, buustaukset ja vastaukset lähetyksestäsi.
|
||||||
|
addToList: Lisää listaan
|
||||||
|
sendMessage: Lähetä viesti
|
||||||
|
reply: Vastaa
|
||||||
|
loadMore: Lataa enemmän
|
||||||
|
showMore: Näytä enemmän
|
||||||
|
receiveFollowRequest: Seuraajapyyntö vastaanotettu
|
||||||
|
followRequestAccepted: Seuraajapyyntö hyväksytty
|
||||||
|
mentions: Maininnat
|
||||||
|
importAndExport: Tuo/Vie Tietosisältö
|
||||||
|
import: Tuo
|
||||||
|
export: Vie
|
||||||
|
files: Tiedostot
|
||||||
|
download: Lataa
|
||||||
|
unfollowConfirm: Oletko varma, ettet halua seurata enää käyttäjää {name}?
|
||||||
|
noLists: Sinulla ei ole listoja
|
||||||
|
note: Lähetys
|
||||||
|
notes: Lähetykset
|
||||||
|
following: Seuraa
|
||||||
|
createList: Luo lista
|
||||||
|
manageLists: Hallitse listoja
|
||||||
|
error: Virhe
|
||||||
|
somethingHappened: On tapahtunut virhe
|
||||||
|
retry: Yritä uudelleen
|
||||||
|
pageLoadError: Virhe ladattaessa sivua.
|
||||||
|
serverIsDead: Tämä palvelin ei vastaa. Yritä hetken kuluttua uudelleen.
|
||||||
|
youShouldUpgradeClient: Nähdäksesi tämän sivun, virkistä päivittääksesi asiakasohjelmasi.
|
||||||
|
privacy: Tietosuoja
|
||||||
|
defaultNoteVisibility: Oletusnäkyvyys
|
||||||
|
followRequest: Seuraajapyyntö
|
||||||
|
followRequests: Seuraajapyynnöt
|
||||||
|
unfollow: Poista seuraaminen
|
||||||
|
enterEmoji: Syötä emoji
|
||||||
|
renote: Buustaa
|
||||||
|
renoted: Buustattu.
|
||||||
|
cantRenote: Tätä lähetystä ei voi buustata.
|
||||||
|
cantReRenote: Buustausta ei voi buustata.
|
||||||
|
quote: Lainaus
|
||||||
|
pinnedNote: Lukittu lähetys
|
||||||
|
clickToShow: Napsauta nähdäksesi
|
||||||
|
sensitive: Herkkää sisältöä (NSFW)
|
||||||
|
add: Lisää
|
||||||
|
enableEmojiReactions: Ota käyttöön emoji-reaktiot
|
||||||
|
showEmojisInReactionNotifications: Näytä emojit reaktioilmoituksissa
|
||||||
|
reactionSetting: Reaktiot näytettäväksi reaktiovalitsimessa
|
||||||
|
rememberNoteVisibility: Muista lähetyksen näkyvyysasetukset
|
||||||
|
markAsSensitive: Merkitse herkäksi sisällöksi (NSFW)
|
||||||
|
unmarkAsSensitive: Poista merkintä herkkää sisältöä (NSFW)
|
||||||
|
renoteMute: Hiljennä buustit
|
||||||
|
renoteUnmute: Poista buustien hiljennys
|
||||||
|
block: Estä
|
||||||
|
unblock: Poista esto
|
||||||
|
unsuspend: Poista keskeytys
|
||||||
|
suspend: Keskeytys
|
||||||
|
blockConfirm: Oletko varma, että haluat estää tämän tilin?
|
||||||
|
unblockConfirm: Oletko varma, että haluat poistaa tämän tilin eston?
|
||||||
|
selectAntenna: Valitse antenni
|
||||||
|
selectWidget: Valitse vimpain
|
||||||
|
editWidgets: Muokkaa vimpaimia
|
||||||
|
editWidgetsExit: Valmis
|
||||||
|
emoji: Emoji
|
||||||
|
emojis: Emojit
|
||||||
|
emojiName: Emojin nimi
|
||||||
|
emojiUrl: Emojin URL-linkki
|
||||||
|
cacheRemoteFiles: Taltioi etätiedostot välimuistiin
|
||||||
|
flagAsBot: Merkitse tili botiksi
|
||||||
|
flagAsBotDescription: Ota tämä vaihtoehto käyttöön, jos tätä tiliä ohjaa ohjelma.
|
||||||
|
Jos se on käytössä, se toimii lippuna muille kehittäjille, jotta estetään loputtomat
|
||||||
|
vuorovaikutusketjut muiden bottien kanssa ja säädetään Calckeyn sisäiset järjestelmät
|
||||||
|
käsittelemään tätä tiliä botina.
|
||||||
|
flagAsCat: Oletko kissa? 🐱
|
||||||
|
flagAsCatDescription: Saat kissan korvat ja puhut kuin kissa!
|
||||||
|
flagSpeakAsCat: Puhu kuin kissa
|
||||||
|
flagShowTimelineReplies: Näytä vastaukset aikajanalla
|
||||||
|
addAccount: Lisää tili
|
||||||
|
loginFailed: Kirjautuminen epäonnistui
|
||||||
|
showOnRemote: Katsele etäinstanssilla
|
||||||
|
general: Yleistä
|
||||||
|
accountMoved: 'Käyttäjä on muuttanut uuteen tiliin:'
|
||||||
|
wallpaper: Taustakuva
|
||||||
|
setWallpaper: Aseta taustakuva
|
||||||
|
searchWith: 'Etsi: {q}'
|
||||||
|
youHaveNoLists: Sinulla ei ole listoja
|
||||||
|
followConfirm: Oletko varma, että haluat seurata käyttäjää {name}?
|
||||||
|
host: Isäntä
|
||||||
|
selectUser: Valitse käyttäjä
|
||||||
|
annotation: Kommentit
|
||||||
|
registeredAt: Rekisteröity
|
||||||
|
latestRequestReceivedAt: Viimeisin pyyntö vastaanotettu
|
||||||
|
latestRequestSentAt: Viimeisin pyyntö lähetetty
|
||||||
|
storageUsage: Tallennustilan käyttö
|
||||||
|
charts: Kaaviot
|
||||||
|
stopActivityDelivery: Lopeta toimintojen lähettäminen
|
||||||
|
blockThisInstance: Estä tämä instanssi
|
||||||
|
operations: Toiminnot
|
||||||
|
metadata: Metatieto
|
||||||
|
monitor: Seuranta
|
||||||
|
jobQueue: Työjono
|
||||||
|
cpuAndMemory: Prosessori ja muisti
|
||||||
|
network: Verkko
|
||||||
|
disk: Levy
|
||||||
|
clearCachedFiles: Tyhjennä välimuisti
|
||||||
|
clearCachedFilesConfirm: Oletko varma, että haluat tyhjentää kaikki välimuistiin tallennetut
|
||||||
|
etätiedostot?
|
||||||
|
blockedInstances: Estetyt instanssit
|
||||||
|
hiddenTags: Piilotetut asiatunnisteet
|
||||||
|
mention: Maininta
|
||||||
|
copyUsername: Kopioi käyttäjänimi
|
||||||
|
searchUser: Etsi käyttäjää
|
||||||
|
showLess: Sulje
|
||||||
|
youGotNewFollower: seurasi sinua
|
||||||
|
directNotes: Yksityisviestit
|
||||||
|
driveFileDeleteConfirm: Oletko varma, että haluat poistaa tiedoston " {name}"? Lähetykset,
|
||||||
|
jotka sisältyvät tiedostoon, poistuvat myös.
|
||||||
|
importRequested: Olet pyytänyt viemistä. Tämä voi viedä hetken.
|
||||||
|
exportRequested: Olet pyytänyt tuomista. Tämä voi viedä hetken. Se lisätään asemaan
|
||||||
|
kun tuonti valmistuu.
|
||||||
|
lists: Listat
|
||||||
|
followers: Seuraajat
|
||||||
|
followsYou: Seuraa sinua
|
||||||
|
pageLoadErrorDescription: Tämä yleensä johtuu verkkovirheistä tai selaimen välimuistista.
|
||||||
|
Kokeile tyhjentämällä välimuisti ja yritä sitten hetken kuluttua uudelleen.
|
||||||
|
enterListName: Anna listalle nimi
|
||||||
|
withNFiles: '{n} tiedosto(t)'
|
||||||
|
instanceInfo: Instanssin tiedot
|
||||||
|
clearQueue: Tyhjennä jono
|
||||||
|
suspendConfirm: Oletko varma, että haluat keskeyttää tämän tilin?
|
||||||
|
unsuspendConfirm: Oletko varma, että haluat poistaa tämän tilin keskeytyksen?
|
||||||
|
selectList: Valitse lista
|
||||||
|
customEmojis: Kustomoitu Emoji
|
||||||
|
addEmoji: Lisää
|
||||||
|
settingGuide: Suositellut asetukset
|
||||||
|
cacheRemoteFilesDescription: Kun tämä asetus ei ole käytössä, etätiedostot on ladattu
|
||||||
|
suoraan etäinstanssilta. Asetuksen poistaminen käytöstä vähentää tallennustilan
|
||||||
|
käyttöä, mutta lisää verkkoliikennettä kun pienoiskuvat eivät muodostu.
|
||||||
|
flagSpeakAsCatDescription: Lähetyksesi nyanifioidaan, kun olet kissatilassa
|
||||||
|
flagShowTimelineRepliesDescription: Näyttää käyttäjien vastaukset muiden käyttäjien
|
||||||
|
lähetyksiin aikajanalla, jos se on päällä.
|
||||||
|
autoAcceptFollowed: Automaattisesti hyväksy seuraamispyynnöt käyttäjiltä, joita seuraat
|
||||||
|
perHour: Tunnissa
|
||||||
|
removeWallpaper: Poista taustakuva
|
||||||
|
recipient: Vastaanottaja(t)
|
||||||
|
federation: Federaatio
|
||||||
|
software: Ohjelmisto
|
||||||
|
proxyAccount: Proxy-tili
|
||||||
|
proxyAccountDescription: Välitystili (Proxy-tili) on tili, joka toimii käyttäjien
|
||||||
|
etäseuraajana tietyin edellytyksin. Kun käyttäjä esimerkiksi lisää etäkäyttäjän
|
||||||
|
luetteloon, etäkäyttäjän toimintaa ei toimiteta instanssiin, jos yksikään paikallinen
|
||||||
|
käyttäjä ei seuraa kyseistä käyttäjää, joten välitystili seuraa sen sijaan.
|
||||||
|
latestStatus: Viimeisin tila
|
||||||
|
selectInstance: Valitse instanssi
|
||||||
|
instances: Instanssit
|
||||||
|
perDay: Päivässä
|
||||||
|
version: Versio
|
||||||
|
statistics: Tilastot
|
||||||
|
clearQueueConfirmTitle: Oletko varma, että haluat tyhjentää jonon?
|
||||||
|
introMisskey: Tervetuloa! Calckey on avoimen lähdekoodin, hajautettu sosiaalisen median
|
||||||
|
alusta, joka on ikuisesti ilmainen! 🚀
|
||||||
|
clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jonossa, eivät
|
||||||
|
federoidu. Yleensä tätä toimintoa ei tarvita.
|
||||||
|
blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
|
||||||
|
Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.
|
||||||
|
_lang_: Suomi
|
|
@ -183,6 +183,7 @@ perHour: "1時間ごと"
|
||||||
perDay: "1日ごと"
|
perDay: "1日ごと"
|
||||||
stopActivityDelivery: "アクティビティの配送を停止"
|
stopActivityDelivery: "アクティビティの配送を停止"
|
||||||
blockThisInstance: "このインスタンスをブロック"
|
blockThisInstance: "このインスタンスをブロック"
|
||||||
|
silenceThisInstance: "このインスタンスをサイレンス"
|
||||||
operations: "操作"
|
operations: "操作"
|
||||||
software: "ソフトウェア"
|
software: "ソフトウェア"
|
||||||
version: "バージョン"
|
version: "バージョン"
|
||||||
|
@ -202,6 +203,8 @@ clearCachedFiles: "キャッシュをクリア"
|
||||||
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
|
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
|
||||||
blockedInstances: "ブロックしたインスタンス"
|
blockedInstances: "ブロックしたインスタンス"
|
||||||
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
|
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
|
||||||
|
silencedInstances: "サイレンスしたインスタンス"
|
||||||
|
silencedInstancesDescription: "サイレンスしたいインスタンスのホストを改行で区切って設定します。サイレンスされたインスタンスに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
|
||||||
muteAndBlock: "ミュートとブロック"
|
muteAndBlock: "ミュートとブロック"
|
||||||
mutedUsers: "ミュートしたユーザー"
|
mutedUsers: "ミュートしたユーザー"
|
||||||
blockedUsers: "ブロックしたユーザー"
|
blockedUsers: "ブロックしたユーザー"
|
||||||
|
@ -220,6 +223,7 @@ noCustomEmojis: "絵文字はありません"
|
||||||
noJobs: "ジョブはありません"
|
noJobs: "ジョブはありません"
|
||||||
federating: "連合中"
|
federating: "連合中"
|
||||||
blocked: "ブロック中"
|
blocked: "ブロック中"
|
||||||
|
silenced: "サイレンス中"
|
||||||
suspended: "配信停止"
|
suspended: "配信停止"
|
||||||
all: "全て"
|
all: "全て"
|
||||||
subscribing: "購読中"
|
subscribing: "購読中"
|
||||||
|
@ -768,7 +772,7 @@ active: "アクティブ"
|
||||||
offline: "オフライン"
|
offline: "オフライン"
|
||||||
notRecommended: "非推奨"
|
notRecommended: "非推奨"
|
||||||
botProtection: "Botプロテクション"
|
botProtection: "Botプロテクション"
|
||||||
instanceBlocking: "インスタンスブロック"
|
instanceBlocking: "連合ブロック・サイレンス"
|
||||||
selectAccount: "アカウントを選択"
|
selectAccount: "アカウントを選択"
|
||||||
switchAccount: "アカウントを切り替え"
|
switchAccount: "アカウントを切り替え"
|
||||||
enabled: "有効"
|
enabled: "有効"
|
||||||
|
@ -1079,7 +1083,7 @@ _mfm:
|
||||||
inlineMath: "数式(インライン)"
|
inlineMath: "数式(インライン)"
|
||||||
inlineMathDescription: "数式(KaTeX)をインラインで表示します。"
|
inlineMathDescription: "数式(KaTeX)をインラインで表示します。"
|
||||||
blockMath: "数式(ブロック)"
|
blockMath: "数式(ブロック)"
|
||||||
blockMathDescription: "複数行の数式(KaTeX)をブロックで表示します。"
|
blockMathDescription: "数式(KaTeX)をブロックで表示します。"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
quoteDescription: "内容が引用であることを示せます。"
|
quoteDescription: "内容が引用であることを示せます。"
|
||||||
emoji: "カスタム絵文字"
|
emoji: "カスタム絵文字"
|
||||||
|
@ -1120,6 +1124,7 @@ _mfm:
|
||||||
rotateDescription: "指定した角度で回転させます。"
|
rotateDescription: "指定した角度で回転させます。"
|
||||||
plain: "プレーン"
|
plain: "プレーン"
|
||||||
plainDescription: "内側の構文を全て無効にします。"
|
plainDescription: "内側の構文を全て無効にします。"
|
||||||
|
position: 位置
|
||||||
_instanceTicker:
|
_instanceTicker:
|
||||||
none: "表示しない"
|
none: "表示しない"
|
||||||
remote: "リモートユーザーに表示"
|
remote: "リモートユーザーに表示"
|
||||||
|
@ -1128,7 +1133,7 @@ _serverDisconnectedBehavior:
|
||||||
reload: "自動でリロード"
|
reload: "自動でリロード"
|
||||||
dialog: "ダイアログで警告"
|
dialog: "ダイアログで警告"
|
||||||
quiet: "控えめに警告"
|
quiet: "控えめに警告"
|
||||||
nothing: "何も起こらない"
|
nothing: "何もしない"
|
||||||
_channel:
|
_channel:
|
||||||
create: "チャンネルを作成"
|
create: "チャンネルを作成"
|
||||||
edit: "チャンネルを編集"
|
edit: "チャンネルを編集"
|
||||||
|
|
|
@ -986,7 +986,7 @@ _registry:
|
||||||
createKey: "Новый ключ"
|
createKey: "Новый ключ"
|
||||||
_aboutMisskey:
|
_aboutMisskey:
|
||||||
about: "Calckey это форк Misskey, сделанный ThatOneCalculator, разработка которого\
|
about: "Calckey это форк Misskey, сделанный ThatOneCalculator, разработка которого\
|
||||||
\ начал с 2022."
|
\ началась с 2022."
|
||||||
contributors: "Основные соавторы"
|
contributors: "Основные соавторы"
|
||||||
allContributors: "Все соавторы"
|
allContributors: "Все соавторы"
|
||||||
source: "Исходный код"
|
source: "Исходный код"
|
||||||
|
|
|
@ -1009,9 +1009,9 @@ _mfm:
|
||||||
blockCode: "代码(块)"
|
blockCode: "代码(块)"
|
||||||
blockCodeDescription: "语法高亮显示整块程序代码。"
|
blockCodeDescription: "语法高亮显示整块程序代码。"
|
||||||
inlineMath: "数学公式(内嵌)"
|
inlineMath: "数学公式(内嵌)"
|
||||||
inlineMathDescription: "显示内嵌的KaTex公式。"
|
inlineMathDescription: "显示内嵌的KaTeX公式。"
|
||||||
blockMath: "数学公式(块)"
|
blockMath: "数学公式(块)"
|
||||||
blockMathDescription: "显示整块的多行KaTex数学公式。"
|
blockMathDescription: "显示整块的KaTeX数学公式。"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
quoteDescription: "可以用来表示引用的内容。"
|
quoteDescription: "可以用来表示引用的内容。"
|
||||||
emoji: "自定义表情符号"
|
emoji: "自定义表情符号"
|
||||||
|
|
|
@ -64,7 +64,7 @@ import: "匯入"
|
||||||
export: "匯出"
|
export: "匯出"
|
||||||
files: "檔案"
|
files: "檔案"
|
||||||
download: "下載"
|
download: "下載"
|
||||||
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n"
|
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。"
|
||||||
unfollowConfirm: "確定要取消追隨{name}嗎?"
|
unfollowConfirm: "確定要取消追隨{name}嗎?"
|
||||||
exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。"
|
exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。"
|
||||||
importRequested: "已請求匯入。這可能會花一點時間"
|
importRequested: "已請求匯入。這可能會花一點時間"
|
||||||
|
@ -291,7 +291,7 @@ emptyDrive: "雲端硬碟為空"
|
||||||
emptyFolder: "資料夾為空"
|
emptyFolder: "資料夾為空"
|
||||||
unableToDelete: "無法刪除"
|
unableToDelete: "無法刪除"
|
||||||
inputNewFileName: "輸入檔案名稱"
|
inputNewFileName: "輸入檔案名稱"
|
||||||
inputNewDescription: "請輸入新標題 "
|
inputNewDescription: "請輸入新標題"
|
||||||
inputNewFolderName: "輸入新資料夾的名稱"
|
inputNewFolderName: "輸入新資料夾的名稱"
|
||||||
circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。"
|
circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。"
|
||||||
hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
|
hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
|
||||||
|
@ -324,7 +324,7 @@ yearX: "{year}年"
|
||||||
pages: "頁面"
|
pages: "頁面"
|
||||||
integration: "整合"
|
integration: "整合"
|
||||||
connectService: "己連結"
|
connectService: "己連結"
|
||||||
disconnectService: "己斷開 "
|
disconnectService: "己斷開"
|
||||||
enableLocalTimeline: "開啟本地時間軸"
|
enableLocalTimeline: "開啟本地時間軸"
|
||||||
enableGlobalTimeline: "啟用公開時間軸"
|
enableGlobalTimeline: "啟用公開時間軸"
|
||||||
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
|
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
|
||||||
|
@ -336,7 +336,7 @@ driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
|
||||||
inMb: "以Mbps為單位"
|
inMb: "以Mbps為單位"
|
||||||
iconUrl: "圖像URL"
|
iconUrl: "圖像URL"
|
||||||
bannerUrl: "橫幅圖像URL"
|
bannerUrl: "橫幅圖像URL"
|
||||||
backgroundImageUrl: "背景圖片的來源網址 "
|
backgroundImageUrl: "背景圖片的來源網址"
|
||||||
basicInfo: "基本資訊"
|
basicInfo: "基本資訊"
|
||||||
pinnedUsers: "置頂用戶"
|
pinnedUsers: "置頂用戶"
|
||||||
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。"
|
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。"
|
||||||
|
@ -490,7 +490,7 @@ useObjectStorage: "使用Object Storage"
|
||||||
objectStorageBaseUrl: "Base URL"
|
objectStorageBaseUrl: "Base URL"
|
||||||
objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理,请指定其URL,例如S3:“https://<bucket>.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/<bucket>”"
|
objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理,请指定其URL,例如S3:“https://<bucket>.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/<bucket>”"
|
||||||
objectStorageBucket: "儲存空間(Bucket)"
|
objectStorageBucket: "儲存空間(Bucket)"
|
||||||
objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 "
|
objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。"
|
||||||
objectStoragePrefix: "前綴"
|
objectStoragePrefix: "前綴"
|
||||||
objectStoragePrefixDesc: "它存儲在此前綴目錄下。"
|
objectStoragePrefixDesc: "它存儲在此前綴目錄下。"
|
||||||
objectStorageEndpoint: "端點(Endpoint)"
|
objectStorageEndpoint: "端點(Endpoint)"
|
||||||
|
@ -560,8 +560,8 @@ disablePlayer: "關閉播放器"
|
||||||
expandTweet: "展開推文"
|
expandTweet: "展開推文"
|
||||||
themeEditor: "主題編輯器"
|
themeEditor: "主題編輯器"
|
||||||
description: "描述"
|
description: "描述"
|
||||||
describeFile: "添加標題 "
|
describeFile: "添加標題"
|
||||||
enterFileDescription: "輸入標題 "
|
enterFileDescription: "輸入標題"
|
||||||
author: "作者"
|
author: "作者"
|
||||||
leaveConfirm: "有未保存的更改。要放棄嗎?"
|
leaveConfirm: "有未保存的更改。要放棄嗎?"
|
||||||
manage: "管理"
|
manage: "管理"
|
||||||
|
@ -865,7 +865,7 @@ driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
|
||||||
driveCapOverrideCaption: "如果指定0以下的值,就會被取消。"
|
driveCapOverrideCaption: "如果指定0以下的值,就會被取消。"
|
||||||
requireAdminForView: "必須以管理者帳號登入才可以檢視。"
|
requireAdminForView: "必須以管理者帳號登入才可以檢視。"
|
||||||
isSystemAccount: "由系統自動建立與管理的帳號。"
|
isSystemAccount: "由系統自動建立與管理的帳號。"
|
||||||
typeToConfirm: "要執行這項操作,請輸入 {x} "
|
typeToConfirm: "要執行這項操作,請輸入 {x}"
|
||||||
deleteAccount: "刪除帳號"
|
deleteAccount: "刪除帳號"
|
||||||
document: "文件"
|
document: "文件"
|
||||||
numberOfPageCache: "快取頁面數"
|
numberOfPageCache: "快取頁面數"
|
||||||
|
@ -876,7 +876,7 @@ statusbar: "狀態列"
|
||||||
pleaseSelect: "請選擇"
|
pleaseSelect: "請選擇"
|
||||||
reverse: "翻轉"
|
reverse: "翻轉"
|
||||||
colored: "彩色"
|
colored: "彩色"
|
||||||
refreshInterval: "更新間隔"
|
refreshInterval: "更新間隔 "
|
||||||
label: "標籤"
|
label: "標籤"
|
||||||
type: "類型"
|
type: "類型"
|
||||||
speed: "速度"
|
speed: "速度"
|
||||||
|
@ -895,7 +895,7 @@ activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,
|
||||||
navbar: "導覽列"
|
navbar: "導覽列"
|
||||||
shuffle: "隨機"
|
shuffle: "隨機"
|
||||||
account: "帳戶"
|
account: "帳戶"
|
||||||
move: "移動 "
|
move: "移動"
|
||||||
customKaTeXMacro: "自定義 KaTeX 宏"
|
customKaTeXMacro: "自定義 KaTeX 宏"
|
||||||
customKaTeXMacroDescription: "使用宏來輕鬆的輸入數學表達式吧!宏的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\\
|
customKaTeXMacroDescription: "使用宏來輕鬆的輸入數學表達式吧!宏的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\\
|
||||||
name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉個例子,\\\
|
name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉個例子,\\\
|
||||||
|
@ -933,11 +933,11 @@ _accountDelete:
|
||||||
inProgress: "正在刪除"
|
inProgress: "正在刪除"
|
||||||
_ad:
|
_ad:
|
||||||
back: "返回"
|
back: "返回"
|
||||||
reduceFrequencyOfThisAd: "降低此廣告的頻率 "
|
reduceFrequencyOfThisAd: "降低此廣告的頻率"
|
||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
|
enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
|
||||||
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 "
|
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。"
|
||||||
contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。 "
|
contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。"
|
||||||
_gallery:
|
_gallery:
|
||||||
my: "我的貼文"
|
my: "我的貼文"
|
||||||
liked: "喜歡的貼文"
|
liked: "喜歡的貼文"
|
||||||
|
@ -1000,7 +1000,7 @@ _mfm:
|
||||||
url: "URL"
|
url: "URL"
|
||||||
urlDescription: "可以展示URL位址。"
|
urlDescription: "可以展示URL位址。"
|
||||||
link: "鏈接"
|
link: "鏈接"
|
||||||
linkDescription: "您可以將特定範圍的文章與 URL 相關聯。 "
|
linkDescription: "您可以將特定範圍的文章與 URL 相關聯。"
|
||||||
bold: "粗體"
|
bold: "粗體"
|
||||||
boldDescription: "可以將文字顯示为粗體来強調。"
|
boldDescription: "可以將文字顯示为粗體来強調。"
|
||||||
small: "縮小"
|
small: "縮小"
|
||||||
|
@ -1012,9 +1012,9 @@ _mfm:
|
||||||
blockCode: "程式碼(區塊)"
|
blockCode: "程式碼(區塊)"
|
||||||
blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。"
|
blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。"
|
||||||
inlineMath: "數學公式(內嵌)"
|
inlineMath: "數學公式(內嵌)"
|
||||||
inlineMathDescription: "顯示內嵌的KaTex數學公式。"
|
inlineMathDescription: "顯示內嵌的KaTeX數學公式。"
|
||||||
blockMath: "數學公式(方塊)"
|
blockMath: "數學公式(方塊)"
|
||||||
blockMathDescription: "以區塊顯示複數行的KaTex數學式。"
|
blockMathDescription: "以區塊顯示KaTeX數學式。"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
quoteDescription: "可以用來表示引用的内容。"
|
quoteDescription: "可以用來表示引用的内容。"
|
||||||
emoji: "自訂表情符號"
|
emoji: "自訂表情符號"
|
||||||
|
@ -1805,3 +1805,6 @@ migration: 遷移
|
||||||
homeTimeline: 主頁時間軸
|
homeTimeline: 主頁時間軸
|
||||||
swipeOnDesktop: 允許在桌面上進行手機式滑動
|
swipeOnDesktop: 允許在桌面上進行手機式滑動
|
||||||
logoImageUrl: 圖標網址
|
logoImageUrl: 圖標網址
|
||||||
|
addInstance: 增加一個實例
|
||||||
|
noInstances: 沒有實例
|
||||||
|
flagSpeakAsCat: 像貓一樣地說話
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "calckey",
|
"name": "calckey",
|
||||||
"version": "13.2.0-dev38",
|
"version": "13.2.0-dev41",
|
||||||
"codename": "aqua",
|
"codename": "aqua",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -40,6 +40,8 @@
|
||||||
"@bull-board/ui": "^4.10.2",
|
"@bull-board/ui": "^4.10.2",
|
||||||
"@napi-rs/cli": "^2.15.0",
|
"@napi-rs/cli": "^2.15.0",
|
||||||
"@tensorflow/tfjs": "^3.21.0",
|
"@tensorflow/tfjs": "^3.21.0",
|
||||||
|
"focus-trap": "^7.2.0",
|
||||||
|
"focus-trap-vue": "^4.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"seedrandom": "^3.0.5"
|
"seedrandom": "^3.0.5"
|
||||||
},
|
},
|
||||||
|
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 473 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 6 KiB |
|
@ -1,25 +1 @@
|
||||||
<svg id="svg10" version="1.1" sodipodi:docname="title_float.svg" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="1.95 0.97 167.97 103.23">
|
<svg viewBox="1.95 0.97 128 128" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" gradientTransform="rotate(90)"><stop offset="5%" stop-color="#9ccfd8" style="--darkreader-inline-stopcolor:#265760"/><stop offset="95%" stop-color="#31748f" style="--darkreader-inline-stopcolor:#275d72"/></linearGradient><defs><linearGradient xlink:href="#a" id="f" gradientTransform="scale(1.27567 .7839)" x1="-43.77" y1="98.469" x2="-27.05" y2="137.466" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="d" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="e" gradientTransform="scale(1.27567 .7839)" x1="-43.77" y1="98.468" x2="-8.156" y2="98.468" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="c" gradientTransform="scale(1.27567 .7839)" x1="1.571" y1="1.27" x2="133.179" y2="1.27" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="b" gradientTransform="scale(1.27567 .7839)" x1="1.571" y1="1.27" x2="133.179" y2="1.27" gradientUnits="userSpaceOnUse"/></defs><g style="fill:url(#b)" transform="translate(.934 25.196) scale(.75646)"><g style="fill:url(#c)" fill="url(#a)" word-spacing="0" letter-spacing="0" font-family="'OTADESIGN Rounded'" font-weight="400"><g transform="translate(-55.341 -52.023) scale(.26953)" style="fill:url(#d)"/><g style="fill:url(#e)"><path style="fill:url(#f)" d="M-41.832 77.19c-3.868 0-7.177 1.358-9.93 4.074-2.716 2.752-4.074 6.063-4.074 9.931 0 3.869 1.358 7.04 4.074 9.793 2.753 2.716 6.064 4.073 9.932 4.073 3.831 0 7.122-1.357 9.875-4.073.855-.855 1.283-1.896 1.283-3.123 0-1.228-.428-2.271-1.283-3.127-.856-.855-1.897-1.281-3.123-1.281-1.229 0-2.27.426-3.125 1.281-1.004 1.042-2.213 1.563-3.627 1.563-3.035-.31-5.208-2.263-5.246-5.106.038-2.842 2.21-4.935 5.244-5.246 1.414 0 2.623.52 3.627 1.563.855.855 1.898 1.283 3.127 1.283 1.226 0 2.267-.428 3.123-1.283.855-.856 1.283-1.897 1.283-3.125 0-1.227-.428-2.268-1.283-3.123-2.753-2.716-6.046-4.075-9.877-4.075zm20.902 6.91c-2.88 0-5.353 1.02-7.422 3.06-.642.643-.964 1.426-.964 2.348 0 .923.322 1.706.964 2.35.644.642 1.427.962 2.348.962.924 0 1.707-.32 2.35-.963.754-.783 1.662-1.173 2.724-1.173 1.09 0 2.026.376 2.809 1.13a3.909 3.909 0 0 1 1.135 2.811c0 1.062-.393 1.97-1.176 2.725-.392.419-.868.7-1.426.84-.141.027-.252.012-.336-.044-.056-.084-.028-.168.084-.251l.84-.881c.643-.643.965-1.411.965-2.305 0-.922-.28-1.663-.838-2.223-.559-.559-1.343-.84-2.35-.84-.698 0-1.397.35-2.095 1.05l-4.866 4.822c-.643.643-.964 1.426-.964 2.347 0 .923.321 1.705.964 2.348 1.957 1.93 4.375 2.894 7.254 2.894 2.908 0 5.396-1.034 7.465-3.103 2.041-2.041 3.06-4.5 3.06-7.379 0-2.907-1.019-5.396-3.06-7.465-2.069-2.04-4.557-3.06-7.465-3.06z" transform="translate(208.34 -284.25) scale(3.6954)" clip-rule="evenodd" fill-rule="evenodd"/></g></g></g></svg>
|
||||||
<sodipodi:namedview id="namedview21" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="1.1507704" inkscape:cx="260.69492" inkscape:cy="102.54" inkscape:window-width="1600" inkscape:window-height="931" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg10"/>
|
|
||||||
<metadata id="metadata16">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
|
||||||
<dc:title/>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<linearGradient id="myGradient" gradientTransform="rotate(90)">
|
|
||||||
<stop offset="5%" stop-color="#9ccfd8" id="stop5" style="--darkreader-inline-stopcolor: #265760;" data-darkreader-inline-stopcolor=""/>
|
|
||||||
<stop offset="95%" stop-color="#31748f" id="stop7" style="--darkreader-inline-stopcolor: #275d72;" data-darkreader-inline-stopcolor=""/>
|
|
||||||
</linearGradient>
|
|
||||||
<defs id="defs14"/>
|
|
||||||
<g id="g8" fill="url('#myGradient')" word-spacing="0" letter-spacing="0" font-family="OTADESIGN Rounded" font-weight="400">
|
|
||||||
<g id="g17">
|
|
||||||
<g transform="matrix(.26953 0 0 .26953 -55.341 -52.023)" id="g11"/>
|
|
||||||
<g transform="matrix(3.6954 0 0 3.6954 208.34 -284.25)" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" id="g15">
|
|
||||||
<path d="m -41.8312,77.19 c -3.8683,0 -7.1782,1.3578 -9.9311,4.0734 -2.716,2.7525 -4.0734,6.0628 -4.0734,9.9311 5.0539,0.04979 6.082,0.01348 8.7525,0.0011 0.0024,-4.51e-4 0.0044,-6.05e-4 0.0069,-0.0011 0.03779,-2.8423 2.2103,-4.9346 5.2451,-5.2451 1.4137,0 2.6227,0.52089 3.6268,1.5629 0.855,0.8548 1.897,1.2822 3.1257,1.2822 1.2258,0 2.2676,-0.42741 3.1236,-1.2822 0.85499,-0.85567 1.2833,-1.8976 1.2833,-3.1257 0,-1.2264 -0.42828,-2.2673 -1.2833,-3.1231 -2.7528,-2.7156 -6.0446,-4.0734 -9.8761,-4.0734 z m -5.252,14.006 c -3.4453,-5.5934 -3.4667,0.08539 -8.7525,-0.0011 0,3.8683 1.3584,7.0406 4.0744,9.7931 2.7528,2.7156 6.0623,4.0734 9.9305,4.0734 3.8315,0 7.1238,-1.3578 9.8766,-4.0734 0.85499,-0.85577 1.2827,-1.8967 1.2827,-3.1231 0,-1.2282 -0.42775,-2.2701 -1.2827,-3.1257 -0.85596,-0.8548 -1.8978,-1.2822 -3.1236,-1.2822 -1.2287,0 -2.2707,0.42741 -3.1257,1.2822 -1.0041,1.042 -2.2136,1.5623 -3.6273,1.5623 -3.0348,-0.31051 -5.2084,-2.2633 -5.2462,-5.1056 -0.0024,1.1e-5 -0.0039,-1.2e-5 -0.0063,0 z m 26.154,-7.0965 c -2.8795,0 -5.3538,1.0204 -7.4227,3.0612 -0.64257,0.64316 -0.96404,1.4255 -0.96404,2.3472 0,0.92303 0.32146,1.7062 0.96404,2.3493 0.64331,0.64243 1.4265,0.96351 2.3477,0.96351 0.92347,0 1.7068,-0.32108 2.3493,-0.96351 0.75465,-0.78308 1.6632,-1.1744 2.7256,-1.1744 1.0894,0 2.0261,0.37695 2.8091,1.1316 0.75536,0.78309 1.1332,1.7201 1.1332,2.8102 0,1.0617 -0.39242,1.9703 -1.1754,2.7256 -0.39149,0.4193 -0.86676,0.69884 -1.4249,0.83878 -0.14116,0.02773 -0.25248,0.01369 -0.33614,-0.04175 -0.05605,-0.08456 -0.02751,-0.16827 0.08456,-0.25211 l 0.83825,-0.88053 c 0.64329,-0.64315 0.9651,-1.412 0.9651,-2.306 0,-0.92236 -0.27937,-1.6632 -0.83825,-2.2225 -0.55888,-0.55932 -1.3422,-0.83878 -2.3493,-0.83878 -0.6986,0 -1.397,0.34902 -2.0956,1.0475 l -4.8651,4.8223 c -0.64328,0.6438 -0.96457,1.4271 -0.96457,2.3488 0,0.92302 0.32128,1.7053 0.96457,2.3477 1.9568,1.9293 4.3751,2.8942 7.2546,2.8942 2.9072,0 5.3945,-1.0343 7.4634,-3.103 2.0412,-2.0409 3.0618,-4.501 3.0618,-7.3804 0,-2.9072 -1.0206,-5.3952 -3.0618,-7.4639 -2.0689,-2.0409 -4.5562,-3.0612 -7.4634,-3.0612 z" clip-rule="evenodd" fill-rule="nonzero" stroke-miterlimit="2" stroke-width="0" id="path13" sodipodi:nodetypes="cccccccscsccccccscscscccccccscscscscccccscsccscscsccc"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 21 KiB |
BIN
packages/backend/assets/inverse wordmark.png
Normal file
After Width: | Height: | Size: 21 KiB |
|
@ -1,65 +1 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<svg xml:space="preserve" viewBox="0 0 512 512" height="512" width="512" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop style="stop-color:#31748f;stop-opacity:1" offset="0"/><stop style="stop-color:#9ccfd8;stop-opacity:1" offset="1"/></linearGradient><linearGradient xlink:href="#a" id="b" x1="254.819" y1="411.542" x2="259.34" y2="-1.41" gradientUnits="userSpaceOnUse"/></defs><path style="fill:url(#b);fill-opacity:1;stroke-width:.996356" d="M0 0v512h512V0Z"/><g style="font-weight:400;font-family:"OTADESIGN Rounded";letter-spacing:0;word-spacing:0;fill:#fff"><g transform="translate(-38.55 37.929) scale(.5619)" style="fill:#fff"/><path style="fill:#fff" d="M-41.832 77.19c-3.868 0-7.177 1.358-9.93 4.074-2.716 2.752-4.074 6.063-4.074 9.931 0 3.869 1.358 7.04 4.074 9.793 2.753 2.716 6.064 4.073 9.932 4.073 3.831 0 7.122-1.357 9.875-4.073.855-.855 1.283-1.896 1.283-3.123 0-1.228-.428-2.271-1.283-3.127-.856-.855-1.897-1.281-3.123-1.281-1.229 0-2.27.426-3.125 1.281-1.004 1.042-2.213 1.563-3.627 1.563-3.035-.31-5.208-2.263-5.246-5.106.038-2.842 2.21-4.935 5.244-5.246 1.414 0 2.623.52 3.627 1.563.855.855 1.898 1.283 3.127 1.283 1.226 0 2.267-.428 3.123-1.283.855-.856 1.283-1.897 1.283-3.125 0-1.227-.428-2.268-1.283-3.123-2.753-2.716-6.046-4.075-9.877-4.075zm20.902 6.91c-2.88 0-5.353 1.02-7.422 3.06-.642.643-.964 1.426-.964 2.348 0 .923.322 1.706.964 2.35.644.642 1.427.962 2.348.962.924 0 1.707-.32 2.35-.963.754-.783 1.662-1.173 2.724-1.173 1.09 0 2.026.376 2.809 1.13a3.909 3.909 0 0 1 1.135 2.811c0 1.062-.393 1.97-1.176 2.725-.392.419-.868.7-1.426.84-.141.027-.252.012-.336-.044-.056-.084-.028-.168.084-.251l.84-.881c.643-.643.965-1.411.965-2.305 0-.922-.28-1.663-.838-2.223-.559-.559-1.343-.84-2.35-.84-.698 0-1.397.35-2.095 1.05l-4.866 4.822c-.643.643-.964 1.426-.964 2.347 0 .923.321 1.705.964 2.348 1.957 1.93 4.375 2.894 7.254 2.894 2.908 0 5.396-1.034 7.465-3.103 2.041-2.041 3.06-4.5 3.06-7.379 0-2.907-1.019-5.396-3.06-7.465-2.069-2.04-4.557-3.06-7.465-3.06z" transform="translate(511.15 -446.2) scale(7.70387)" clip-rule="evenodd" fill-rule="evenodd"/></g></svg>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="512.0px"
|
|
||||||
height="512.0px"
|
|
||||||
viewBox="0 0 512.0 512.0"
|
|
||||||
version="1.1"
|
|
||||||
id="SVGRoot"
|
|
||||||
sodipodi:docname="inverse wordmark.svg"
|
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview71"
|
|
||||||
pagecolor="#505050"
|
|
||||||
bordercolor="#eeeeee"
|
|
||||||
borderopacity="1"
|
|
||||||
inkscape:showpageshadow="0"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#505050"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
showgrid="true"
|
|
||||||
inkscape:zoom="0.50150542"
|
|
||||||
inkscape:cx="59.819892"
|
|
||||||
inkscape:cy="189.42966"
|
|
||||||
inkscape:window-width="1600"
|
|
||||||
inkscape:window-height="931"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1"><inkscape:grid
|
|
||||||
type="xygrid"
|
|
||||||
id="grid77" /></sodipodi:namedview><defs
|
|
||||||
id="defs66" /><g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"><path
|
|
||||||
id="rect136"
|
|
||||||
style="fill:#31748f;fill-opacity:1;stroke-width:0.996356"
|
|
||||||
d="M 0,0 V 512 H 512 V 0 Z" /><g
|
|
||||||
id="g17"
|
|
||||||
style="font-weight:400;font-family:'OTADESIGN Rounded';letter-spacing:0;word-spacing:0;fill:#ffffff"
|
|
||||||
transform="matrix(2.0847185,0,0,2.0847185,76.820648,146.38203)"><g
|
|
||||||
transform="matrix(0.26953,0,0,0.26953,-55.341,-52.023)"
|
|
||||||
id="g11"
|
|
||||||
style="fill:#ffffff" /><g
|
|
||||||
transform="matrix(3.6954,0,0,3.6954,208.34,-284.25)"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-miterlimit="2"
|
|
||||||
id="g15"
|
|
||||||
style="fill:#ffffff"><path
|
|
||||||
d="m -41.8312,77.19 c -3.8683,0 -7.1782,1.3578 -9.9311,4.0734 -2.716,2.7525 -4.0734,6.0628 -4.0734,9.9311 5.0539,0.04979 6.082,0.01348 8.7525,0.0011 0.0024,-4.51e-4 0.0044,-6.05e-4 0.0069,-0.0011 0.03779,-2.8423 2.2103,-4.9346 5.2451,-5.2451 1.4137,0 2.6227,0.52089 3.6268,1.5629 0.855,0.8548 1.897,1.2822 3.1257,1.2822 1.2258,0 2.2676,-0.42741 3.1236,-1.2822 0.85499,-0.85567 1.2833,-1.8976 1.2833,-3.1257 0,-1.2264 -0.42828,-2.2673 -1.2833,-3.1231 -2.7528,-2.7156 -6.0446,-4.0734 -9.8761,-4.0734 z m -5.252,14.006 c -3.4453,-5.5934 -3.4667,0.08539 -8.7525,-0.0011 0,3.8683 1.3584,7.0406 4.0744,9.7931 2.7528,2.7156 6.0623,4.0734 9.9305,4.0734 3.8315,0 7.1238,-1.3578 9.8766,-4.0734 0.85499,-0.85577 1.2827,-1.8967 1.2827,-3.1231 0,-1.2282 -0.42775,-2.2701 -1.2827,-3.1257 -0.85596,-0.8548 -1.8978,-1.2822 -3.1236,-1.2822 -1.2287,0 -2.2707,0.42741 -3.1257,1.2822 -1.0041,1.042 -2.2136,1.5623 -3.6273,1.5623 -3.0348,-0.31051 -5.2084,-2.2633 -5.2462,-5.1056 -0.0024,1.1e-5 -0.0039,-1.2e-5 -0.0063,0 z m 26.154,-7.0965 c -2.8795,0 -5.3538,1.0204 -7.4227,3.0612 -0.64257,0.64316 -0.96404,1.4255 -0.96404,2.3472 0,0.92303 0.32146,1.7062 0.96404,2.3493 0.64331,0.64243 1.4265,0.96351 2.3477,0.96351 0.92347,0 1.7068,-0.32108 2.3493,-0.96351 0.75465,-0.78308 1.6632,-1.1744 2.7256,-1.1744 1.0894,0 2.0261,0.37695 2.8091,1.1316 0.75536,0.78309 1.1332,1.7201 1.1332,2.8102 0,1.0617 -0.39242,1.9703 -1.1754,2.7256 -0.39149,0.4193 -0.86676,0.69884 -1.4249,0.83878 -0.14116,0.02773 -0.25248,0.01369 -0.33614,-0.04175 -0.05605,-0.08456 -0.02751,-0.16827 0.08456,-0.25211 l 0.83825,-0.88053 c 0.64329,-0.64315 0.9651,-1.412 0.9651,-2.306 0,-0.92236 -0.27937,-1.6632 -0.83825,-2.2225 -0.55888,-0.55932 -1.3422,-0.83878 -2.3493,-0.83878 -0.6986,0 -1.397,0.34902 -2.0956,1.0475 l -4.8651,4.8223 c -0.64328,0.6438 -0.96457,1.4271 -0.96457,2.3488 0,0.92302 0.32128,1.7053 0.96457,2.3477 1.9568,1.9293 4.3751,2.8942 7.2546,2.8942 2.9072,0 5.3945,-1.0343 7.4634,-3.103 2.0412,-2.0409 3.0618,-4.501 3.0618,-7.3804 0,-2.9072 -1.0206,-5.3952 -3.0618,-7.4639 -2.0689,-2.0409 -4.5562,-3.0612 -7.4634,-3.0612 z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
fill-rule="nonzero"
|
|
||||||
stroke-miterlimit="2"
|
|
||||||
stroke-width="0"
|
|
||||||
id="path13"
|
|
||||||
sodipodi:nodetypes="cccccccscsccccccscscscccccccscscscscccccscsccscscsccc"
|
|
||||||
style="fill:#ffffff" /></g></g></g></svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 20 KiB |
23
packages/backend/migration/1682777547198-LibreTranslate.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
export class LibreTranslate1682777547198 {
|
||||||
|
name = "LibreTranslate1682777547198";
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "meta"
|
||||||
|
ADD "libreTranslateApiUrl" character varying(512)
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "meta"
|
||||||
|
ADD "libreTranslateApiKey" character varying(128)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "meta" DROP COLUMN "libreTranslateApiKey"
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "meta" DROP COLUMN "libreTranslateApiUrl"
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
165
packages/backend/migration/1682891890317-InstanceSilence.js
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
export class InstanceSilence1682891890317 {
|
||||||
|
name = "InstanceSilence1682891890317";
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_renote_muting_createdAt"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@
|
||||||
"@bull-board/api": "^4.6.4",
|
"@bull-board/api": "^4.6.4",
|
||||||
"@bull-board/koa": "^4.6.4",
|
"@bull-board/koa": "^4.6.4",
|
||||||
"@bull-board/ui": "^4.6.4",
|
"@bull-board/ui": "^4.6.4",
|
||||||
"@calckey/megalodon": "5.1.24",
|
"@calckey/megalodon": "5.2.0",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@elastic/elasticsearch": "7.17.0",
|
"@elastic/elasticsearch": "7.17.0",
|
||||||
"@koa/cors": "3.4.3",
|
"@koa/cors": "3.4.3",
|
||||||
|
|
|
@ -89,6 +89,11 @@ export type Source = {
|
||||||
authKey?: string;
|
authKey?: string;
|
||||||
isPro?: boolean;
|
isPro?: boolean;
|
||||||
};
|
};
|
||||||
|
libreTranslate: {
|
||||||
|
managed?: boolean;
|
||||||
|
apiUrl?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
};
|
||||||
email: {
|
email: {
|
||||||
managed?: boolean;
|
managed?: boolean;
|
||||||
address?: string;
|
address?: string;
|
||||||
|
|
|
@ -18,3 +18,21 @@ export async function shouldBlockInstance(
|
||||||
(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
|
(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a specific host (punycoded) should be limited.
|
||||||
|
*
|
||||||
|
* @param host punycoded instance host
|
||||||
|
* @param meta a resolved Meta table
|
||||||
|
* @returns whether the given host should be limited
|
||||||
|
*/
|
||||||
|
export async function shouldSilenceInstance(
|
||||||
|
host: Instance["host"],
|
||||||
|
meta?: Meta,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { silencedHosts } = meta ?? (await fetchMeta());
|
||||||
|
return silencedHosts.some(
|
||||||
|
(silencedHost) =>
|
||||||
|
host === silencedHost || host.endsWith(`.${silencedHost}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -97,6 +97,11 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public blockedHosts: string[];
|
public blockedHosts: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, array: true, default: '{}',
|
||||||
|
})
|
||||||
|
public silencedHosts: string[];
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
@ -386,6 +391,18 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public deeplIsPro: boolean;
|
public deeplIsPro: boolean;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 512,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public libreTranslateApiUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public libreTranslateApiKey: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 512,
|
length: 512,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { db } from "@/db/postgre.js";
|
import { db } from "@/db/postgre.js";
|
||||||
import { Instance } from "@/models/entities/instance.js";
|
import { Instance } from "@/models/entities/instance.js";
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import {
|
||||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
shouldBlockInstance,
|
||||||
|
shouldSilenceInstance,
|
||||||
|
} from "@/misc/should-block-instance.js";
|
||||||
|
|
||||||
export const InstanceRepository = db.getRepository(Instance).extend({
|
export const InstanceRepository = db.getRepository(Instance).extend({
|
||||||
async pack(instance: Instance): Promise<Packed<"FederationInstance">> {
|
async pack(instance: Instance): Promise<Packed<"FederationInstance">> {
|
||||||
const meta = await fetchMeta();
|
|
||||||
return {
|
return {
|
||||||
id: instance.id,
|
id: instance.id,
|
||||||
caughtAt: instance.caughtAt.toISOString(),
|
caughtAt: instance.caughtAt.toISOString(),
|
||||||
|
@ -22,6 +23,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
|
||||||
isNotResponding: instance.isNotResponding,
|
isNotResponding: instance.isNotResponding,
|
||||||
isSuspended: instance.isSuspended,
|
isSuspended: instance.isSuspended,
|
||||||
isBlocked: await shouldBlockInstance(instance.host),
|
isBlocked: await shouldBlockInstance(instance.host),
|
||||||
|
isSilenced: await shouldSilenceInstance(instance.host),
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
softwareVersion: instance.softwareVersion,
|
softwareVersion: instance.softwareVersion,
|
||||||
openRegistrations: instance.openRegistrations,
|
openRegistrations: instance.openRegistrations,
|
||||||
|
|
|
@ -68,6 +68,11 @@ export const packedFederationInstanceSchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
isSilenced: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
softwareName: {
|
softwareName: {
|
||||||
type: "string",
|
type: "string",
|
||||||
optional: false,
|
optional: false,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { renderPerson } from "@/remote/activitypub/renderer/person.js";
|
||||||
import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
|
import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
|
||||||
import { inbox as processInbox } from "@/queue/index.js";
|
import { inbox as processInbox } from "@/queue/index.js";
|
||||||
import { isSelfHost, toPuny } from "@/misc/convert-host.js";
|
import { isSelfHost, toPuny } from "@/misc/convert-host.js";
|
||||||
import { Notes, Users, Emojis, NoteReactions } from "@/models/index.js";
|
import { Notes, Users, Emojis, NoteReactions, FollowRequests } from "@/models/index.js";
|
||||||
import type { ILocalUser, User } from "@/models/entities/user.js";
|
import type { ILocalUser, User } from "@/models/entities/user.js";
|
||||||
import { renderLike } from "@/remote/activitypub/renderer/like.js";
|
import { renderLike } from "@/remote/activitypub/renderer/like.js";
|
||||||
import { getUserKeypair } from "@/misc/keypair-store.js";
|
import { getUserKeypair } from "@/misc/keypair-store.js";
|
||||||
|
@ -330,7 +330,7 @@ router.get("/likes/:like", async (ctx) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// follow
|
// follow
|
||||||
router.get("/follows/:follower/:followee", async (ctx) => {
|
router.get("/follows/:follower/:followee", async (ctx: Router.RouterContext) => {
|
||||||
const verify = await checkFetch(ctx.req);
|
const verify = await checkFetch(ctx.req);
|
||||||
if (verify !== 200) {
|
if (verify !== 200) {
|
||||||
ctx.status = verify;
|
ctx.status = verify;
|
||||||
|
@ -365,4 +365,47 @@ router.get("/follows/:follower/:followee", async (ctx) => {
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// follow request
|
||||||
|
router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const followRequest = await FollowRequests.findOneBy({
|
||||||
|
id: ctx.params.followRequestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (followRequest == null) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [follower, followee] = await Promise.all([
|
||||||
|
Users.findOneBy({
|
||||||
|
id: followRequest.followerId,
|
||||||
|
host: IsNull(),
|
||||||
|
}),
|
||||||
|
Users.findOneBy({
|
||||||
|
id: followRequest.followeeId,
|
||||||
|
host: Not(IsNull()),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (follower == null || followee == null) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
|
||||||
|
} else {
|
||||||
|
ctx.set("Cache-Control", "public, max-age=180");
|
||||||
|
}
|
||||||
|
ctx.body = renderActivity(renderFollow(follower, followee));
|
||||||
|
setResponseType(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -30,6 +30,17 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.deeplIsPro = config.deepl.isPro;
|
set.deeplIsPro = config.deepl.isPro;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
config.libreTranslate.managed != null &&
|
||||||
|
config.libreTranslate.managed === true
|
||||||
|
) {
|
||||||
|
if (typeof config.libreTranslate.apiUrl === "string") {
|
||||||
|
set.libreTranslateApiUrl = config.libreTranslate.apiUrl;
|
||||||
|
}
|
||||||
|
if (typeof config.libreTranslate.apiKey === "string") {
|
||||||
|
set.libreTranslateApiKey = config.libreTranslate.apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (config.email.managed != null && config.email.managed === true) {
|
if (config.email.managed != null && config.email.managed === true) {
|
||||||
set.enableEmail = true;
|
set.enableEmail = true;
|
||||||
if (typeof config.email.address === "string") {
|
if (typeof config.email.address === "string") {
|
||||||
|
|
|
@ -259,6 +259,16 @@ export const meta = {
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
silencedHosts: {
|
||||||
|
type: "array",
|
||||||
|
optional: true,
|
||||||
|
nullable: false,
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
allowedHosts: {
|
allowedHosts: {
|
||||||
type: "array",
|
type: "array",
|
||||||
optional: true,
|
optional: true,
|
||||||
|
@ -512,7 +522,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
enableGithubIntegration: instance.enableGithubIntegration,
|
enableGithubIntegration: instance.enableGithubIntegration,
|
||||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
enableDiscordIntegration: instance.enableDiscordIntegration,
|
||||||
enableServiceWorker: instance.enableServiceWorker,
|
enableServiceWorker: instance.enableServiceWorker,
|
||||||
translatorAvailable: instance.deeplAuthKey != null,
|
translatorAvailable:
|
||||||
|
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
|
||||||
pinnedPages: instance.pinnedPages,
|
pinnedPages: instance.pinnedPages,
|
||||||
pinnedClipId: instance.pinnedClipId,
|
pinnedClipId: instance.pinnedClipId,
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
|
@ -523,6 +534,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
customSplashIcons: instance.customSplashIcons,
|
customSplashIcons: instance.customSplashIcons,
|
||||||
hiddenTags: instance.hiddenTags,
|
hiddenTags: instance.hiddenTags,
|
||||||
blockedHosts: instance.blockedHosts,
|
blockedHosts: instance.blockedHosts,
|
||||||
|
silencedHosts: instance.silencedHosts,
|
||||||
allowedHosts: instance.allowedHosts,
|
allowedHosts: instance.allowedHosts,
|
||||||
privateMode: instance.privateMode,
|
privateMode: instance.privateMode,
|
||||||
secureMode: instance.secureMode,
|
secureMode: instance.secureMode,
|
||||||
|
@ -564,6 +576,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
|
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
|
||||||
deeplAuthKey: instance.deeplAuthKey,
|
deeplAuthKey: instance.deeplAuthKey,
|
||||||
deeplIsPro: instance.deeplIsPro,
|
deeplIsPro: instance.deeplIsPro,
|
||||||
|
libreTranslateApiUrl: instance.libreTranslateApiUrl,
|
||||||
|
libreTranslateApiKey: instance.libreTranslateApiKey,
|
||||||
enableIpLogging: instance.enableIpLogging,
|
enableIpLogging: instance.enableIpLogging,
|
||||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||||
};
|
};
|
||||||
|
|
|
@ -61,6 +61,13 @@ export const paramDef = {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
silencedHosts: {
|
||||||
|
type: "array",
|
||||||
|
nullable: true,
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
allowedHosts: {
|
allowedHosts: {
|
||||||
type: "array",
|
type: "array",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -124,6 +131,8 @@ export const paramDef = {
|
||||||
summalyProxy: { type: "string", nullable: true },
|
summalyProxy: { type: "string", nullable: true },
|
||||||
deeplAuthKey: { type: "string", nullable: true },
|
deeplAuthKey: { type: "string", nullable: true },
|
||||||
deeplIsPro: { type: "boolean" },
|
deeplIsPro: { type: "boolean" },
|
||||||
|
libreTranslateApiUrl: { type: "string", nullable: true },
|
||||||
|
libreTranslateApiKey: { type: "string", nullable: true },
|
||||||
enableTwitterIntegration: { type: "boolean" },
|
enableTwitterIntegration: { type: "boolean" },
|
||||||
twitterConsumerKey: { type: "string", nullable: true },
|
twitterConsumerKey: { type: "string", nullable: true },
|
||||||
twitterConsumerSecret: { type: "string", nullable: true },
|
twitterConsumerSecret: { type: "string", nullable: true },
|
||||||
|
@ -217,6 +226,15 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(ps.silencedHosts)) {
|
||||||
|
let lastValue = "";
|
||||||
|
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
|
||||||
|
const lv = lastValue;
|
||||||
|
lastValue = h;
|
||||||
|
return h !== "" && h !== lv;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.themeColor !== undefined) {
|
if (ps.themeColor !== undefined) {
|
||||||
set.themeColor = ps.themeColor;
|
set.themeColor = ps.themeColor;
|
||||||
}
|
}
|
||||||
|
@ -515,6 +533,22 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.deeplIsPro = ps.deeplIsPro;
|
set.deeplIsPro = ps.deeplIsPro;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.libreTranslateApiUrl !== undefined) {
|
||||||
|
if (ps.libreTranslateApiUrl === "") {
|
||||||
|
set.libreTranslateApiUrl = null;
|
||||||
|
} else {
|
||||||
|
set.libreTranslateApiUrl = ps.libreTranslateApiUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.libreTranslateApiKey !== undefined) {
|
||||||
|
if (ps.libreTranslateApiKey === "") {
|
||||||
|
set.libreTranslateApiKey = null;
|
||||||
|
} else {
|
||||||
|
set.libreTranslateApiKey = ps.libreTranslateApiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.enableIpLogging !== undefined) {
|
if (ps.enableIpLogging !== undefined) {
|
||||||
set.enableIpLogging = ps.enableIpLogging;
|
set.enableIpLogging = ps.enableIpLogging;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const paramDef = {
|
||||||
notResponding: { type: "boolean", nullable: true },
|
notResponding: { type: "boolean", nullable: true },
|
||||||
suspended: { type: "boolean", nullable: true },
|
suspended: { type: "boolean", nullable: true },
|
||||||
federating: { type: "boolean", nullable: true },
|
federating: { type: "boolean", nullable: true },
|
||||||
|
silenced: { type: "boolean", nullable: true },
|
||||||
subscribing: { type: "boolean", nullable: true },
|
subscribing: { type: "boolean", nullable: true },
|
||||||
publishing: { type: "boolean", nullable: true },
|
publishing: { type: "boolean", nullable: true },
|
||||||
limit: { type: "integer", minimum: 1, maximum: 100, default: 30 },
|
limit: { type: "integer", minimum: 1, maximum: 100, default: 30 },
|
||||||
|
@ -115,6 +116,22 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof ps.silenced === "boolean") {
|
||||||
|
const meta = await fetchMeta(true);
|
||||||
|
if (ps.silenced) {
|
||||||
|
if (meta.silencedHosts.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
query.andWhere("instance.host IN (:...silences)", {
|
||||||
|
silences: meta.silencedHosts,
|
||||||
|
});
|
||||||
|
} else if (meta.silencedHosts.length > 0) {
|
||||||
|
query.andWhere("instance.host NOT IN (:...silences)", {
|
||||||
|
silences: meta.silencedHosts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof ps.notResponding === "boolean") {
|
if (typeof ps.notResponding === "boolean") {
|
||||||
if (ps.notResponding) {
|
if (ps.notResponding) {
|
||||||
query.andWhere("instance.isNotResponding = TRUE");
|
query.andWhere("instance.isNotResponding = TRUE");
|
||||||
|
|
|
@ -482,7 +482,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
|
||||||
enableServiceWorker: instance.enableServiceWorker,
|
enableServiceWorker: instance.enableServiceWorker,
|
||||||
|
|
||||||
translatorAvailable: instance.deeplAuthKey != null,
|
translatorAvailable:
|
||||||
|
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
|
||||||
defaultReaction: instance.defaultReaction,
|
defaultReaction: instance.defaultReaction,
|
||||||
|
|
||||||
...(ps.detail
|
...(ps.detail
|
||||||
|
|
|
@ -51,15 +51,54 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
const instance = await fetchMeta();
|
const instance = await fetchMeta();
|
||||||
|
|
||||||
if (instance.deeplAuthKey == null) {
|
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
|
||||||
return 204; // TODO: 良い感じのエラー返す
|
return 204; // TODO: 良い感じのエラー返す
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetLang = ps.targetLang;
|
let targetLang = ps.targetLang;
|
||||||
if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
|
if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
|
||||||
|
|
||||||
|
if (instance.libreTranslateApiUrl != null) {
|
||||||
|
const jsonBody = {
|
||||||
|
q: note.text,
|
||||||
|
source: "auto",
|
||||||
|
target: targetLang,
|
||||||
|
format: "text",
|
||||||
|
api_key: instance.libreTranslateApiKey ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = new URL(instance.libreTranslateApiUrl);
|
||||||
|
if (url.pathname.endsWith("/")) {
|
||||||
|
url.pathname = url.pathname.slice(0, -1);
|
||||||
|
}
|
||||||
|
if (!url.pathname.endsWith("/translate")) {
|
||||||
|
url.pathname += "/translate";
|
||||||
|
}
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(jsonBody),
|
||||||
|
agent: getAgentByUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = (await res.json()) as {
|
||||||
|
detectedLanguage?: {
|
||||||
|
confidence: number;
|
||||||
|
language: string;
|
||||||
|
};
|
||||||
|
translatedText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceLang: json.detectedLanguage?.language,
|
||||||
|
text: json.translatedText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("auth_key", instance.deeplAuthKey);
|
params.append("auth_key", instance.deeplAuthKey ?? "");
|
||||||
params.append("text", note.text);
|
params.append("text", note.text);
|
||||||
params.append("target_lang", targetLang);
|
params.append("target_lang", targetLang);
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
convertId,
|
convertId,
|
||||||
IdConvertType as IdType,
|
IdConvertType as IdType,
|
||||||
} from "../../../native-utils/built/index.js";
|
} from "../../../native-utils/built/index.js";
|
||||||
|
import { convertAttachment } from "./mastodon/converters.js";
|
||||||
|
|
||||||
// re-export native rust id conversion (function and enum)
|
// re-export native rust id conversion (function and enum)
|
||||||
export { IdType, convertId };
|
export { IdType, convertId };
|
||||||
|
@ -93,7 +94,7 @@ mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await client.uploadMedia(multipartData);
|
const data = await client.uploadMedia(multipartData);
|
||||||
ctx.body = data.data;
|
ctx.body = convertAttachment(data.data as Entity.Attachment);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -112,7 +113,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await client.uploadMedia(multipartData);
|
const data = await client.uploadMedia(multipartData);
|
||||||
ctx.body = data.data;
|
ctx.body = convertAttachment(data.data as Entity.Attachment);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { apiTimelineMastodon } from "./endpoints/timeline.js";
|
||||||
import { apiNotificationsMastodon } from "./endpoints/notifications.js";
|
import { apiNotificationsMastodon } from "./endpoints/notifications.js";
|
||||||
import { apiSearchMastodon } from "./endpoints/search.js";
|
import { apiSearchMastodon } from "./endpoints/search.js";
|
||||||
import { getInstance } from "./endpoints/meta.js";
|
import { getInstance } from "./endpoints/meta.js";
|
||||||
|
import { convertAnnouncement, convertFilter } from "./converters.js";
|
||||||
|
import { convertId, IdType } from "../index.js";
|
||||||
|
|
||||||
export function getClient(
|
export function getClient(
|
||||||
BASE_URL: string,
|
BASE_URL: string,
|
||||||
|
@ -68,7 +70,7 @@ export function apiMastodonCompatible(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getInstanceAnnouncements();
|
const data = await client.getInstanceAnnouncements();
|
||||||
ctx.body = data.data;
|
ctx.body = data.data.map(announcement => convertAnnouncement(announcement));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -83,7 +85,9 @@ export function apiMastodonCompatible(router: Router): void {
|
||||||
const accessTokens = ctx.request.headers.authorization;
|
const accessTokens = ctx.request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.dismissInstanceAnnouncement(ctx.params.id);
|
const data = await client.dismissInstanceAnnouncement(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -100,7 +104,7 @@ export function apiMastodonCompatible(router: Router): void {
|
||||||
// displayed without being logged in
|
// displayed without being logged in
|
||||||
try {
|
try {
|
||||||
const data = await client.getFilters();
|
const data = await client.getFilters();
|
||||||
ctx.body = data.data;
|
ctx.body = data.data.map(filter => convertFilter(filter));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
|
44
packages/backend/src/server/api/mastodon/converters.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Entity } from "@calckey/megalodon";
|
||||||
|
import { convertId, IdType } from "../index.js";
|
||||||
|
|
||||||
|
function simpleConvert(data: any) {
|
||||||
|
data.id = convertId(data.id, IdType.MastodonId);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertAccount(account: Entity.Account) { return simpleConvert(account); }
|
||||||
|
export function convertAnnouncement(announcement: Entity.Announcement) { return simpleConvert(announcement); }
|
||||||
|
export function convertAttachment(attachment: Entity.Attachment) { return simpleConvert(attachment); }
|
||||||
|
export function convertFilter(filter: Entity.Filter) { return simpleConvert(filter); }
|
||||||
|
export function convertList(list: Entity.List) { return simpleConvert(list); }
|
||||||
|
|
||||||
|
export function convertNotification(notification: Entity.Notification) {
|
||||||
|
notification.account = convertAccount(notification.account);
|
||||||
|
notification.id = convertId(notification.id, IdType.MastodonId);
|
||||||
|
if (notification.status)
|
||||||
|
notification.status = convertStatus(notification.status);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertPoll(poll: Entity.Poll) { return simpleConvert(poll); }
|
||||||
|
export function convertRelationship(relationship: Entity.Relationship) { return simpleConvert(relationship); }
|
||||||
|
|
||||||
|
export function convertStatus(status: Entity.Status) {
|
||||||
|
status.account = convertAccount(status.account);
|
||||||
|
status.id = convertId(status.id, IdType.MastodonId);
|
||||||
|
if (status.in_reply_to_account_id)
|
||||||
|
status.in_reply_to_account_id = convertId(status.in_reply_to_account_id, IdType.MastodonId);
|
||||||
|
if (status.in_reply_to_id)
|
||||||
|
status.in_reply_to_id = convertId(status.in_reply_to_id, IdType.MastodonId);
|
||||||
|
status.media_attachments = status.media_attachments.map(attachment => convertAttachment(attachment));
|
||||||
|
status.mentions = status.mentions.map(mention => ({
|
||||||
|
...mention,
|
||||||
|
id: convertId(mention.id, IdType.MastodonId),
|
||||||
|
}));
|
||||||
|
if (status.poll)
|
||||||
|
status.poll = convertPoll(status.poll);
|
||||||
|
if (status.reblog)
|
||||||
|
status.reblog = convertStatus(status.reblog);
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
|
@ -3,8 +3,9 @@ import { resolveUser } from "@/remote/resolve-user.js";
|
||||||
import Router from "@koa/router";
|
import Router from "@koa/router";
|
||||||
import { FindOptionsWhere, IsNull } from "typeorm";
|
import { FindOptionsWhere, IsNull } from "typeorm";
|
||||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||||
import { argsToBools, limitToInt } from "./timeline.js";
|
import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js";
|
||||||
import { convertId, IdType } from "../../index.js";
|
import { convertId, IdType } from "../../index.js";
|
||||||
|
import { convertAccount, convertList, convertRelationship, convertStatus } from "../converters.js";
|
||||||
|
|
||||||
const relationshipModel = {
|
const relationshipModel = {
|
||||||
id: "",
|
id: "",
|
||||||
|
@ -62,9 +63,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const data = await client.updateCredentials(
|
const data = await client.updateCredentials(
|
||||||
(ctx.request as any).body as any,
|
(ctx.request as any).body as any,
|
||||||
);
|
);
|
||||||
let resp = data.data;
|
ctx.body = convertAccount(data.data);
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -81,9 +80,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
(ctx.request.query as any).acct,
|
(ctx.request.query as any).acct,
|
||||||
"accounts",
|
"accounts",
|
||||||
);
|
);
|
||||||
let resp = data.data.accounts[0];
|
ctx.body = convertAccount(data.data.accounts[0]);
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -91,255 +88,6 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
ctx.body = e.response.data;
|
ctx.body = e.response.data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const calcId = convertId(ctx.params.id, IdType.CalckeyId);
|
|
||||||
const data = await client.getAccount(calcId);
|
|
||||||
let resp = data.data;
|
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
router.get<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/statuses",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.getAccountStatuses(
|
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
|
||||||
argsToBools(limitToInt(ctx.query as any)),
|
|
||||||
);
|
|
||||||
let resp = data.data;
|
|
||||||
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
|
||||||
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
|
||||||
resp[statIdx].in_reply_to_account_id = resp[statIdx]
|
|
||||||
.in_reply_to_account_id
|
|
||||||
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
|
|
||||||
: null;
|
|
||||||
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
|
|
||||||
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
|
|
||||||
: null;
|
|
||||||
let mentions = resp[statIdx].mentions;
|
|
||||||
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
|
|
||||||
resp[statIdx].mentions[mtnIdx].id = convertId(
|
|
||||||
mentions[mtnIdx].id,
|
|
||||||
IdType.MastodonId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.get<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/followers",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.getAccountFollowers(
|
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
|
||||||
limitToInt(ctx.query as any),
|
|
||||||
);
|
|
||||||
let resp = data.data;
|
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.get<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/following",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.getAccountFollowing(
|
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
|
||||||
limitToInt(ctx.query as any),
|
|
||||||
);
|
|
||||||
let resp = data.data;
|
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.get<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/lists",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.getAccountLists(ctx.params.id);
|
|
||||||
ctx.body = data.data;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.post<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/follow",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.followAccount(
|
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
|
||||||
);
|
|
||||||
let acct = data.data;
|
|
||||||
acct.following = true;
|
|
||||||
acct.id = convertId(acct.id, IdType.MastodonId);
|
|
||||||
ctx.body = acct;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.post<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/unfollow",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.unfollowAccount(
|
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
|
||||||
);
|
|
||||||
let acct = data.data;
|
|
||||||
acct.id = convertId(acct.id, IdType.MastodonId);
|
|
||||||
acct.following = false;
|
|
||||||
ctx.body = acct;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.post<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/block",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.blockAccount(
|
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
|
||||||
);
|
|
||||||
let resp = data.data;
|
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.post<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/unblock",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.unblockAccount(
|
|
||||||
convertId(ctx.params.id, IdType.MastodonId),
|
|
||||||
);
|
|
||||||
let resp = data.data;
|
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.post<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/mute",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.muteAccount(
|
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
|
||||||
(ctx.request as any).body as any,
|
|
||||||
);
|
|
||||||
let resp = data.data;
|
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.post<{ Params: { id: string } }>(
|
|
||||||
"/v1/accounts/:id/unmute",
|
|
||||||
async (ctx) => {
|
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
|
||||||
const data = await client.unmuteAccount(
|
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
|
||||||
);
|
|
||||||
let resp = data.data;
|
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
ctx.status = 401;
|
|
||||||
ctx.body = e.response.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
router.get("/v1/accounts/relationships", async (ctx) => {
|
router.get("/v1/accounts/relationships", async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
@ -364,11 +112,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await client.getRelationships(reqIds);
|
const data = await client.getRelationships(reqIds);
|
||||||
let resp = data.data;
|
ctx.body = data.data.map(relationship => convertRelationship(relationship));
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
let data = e.response.data;
|
let data = e.response.data;
|
||||||
|
@ -378,33 +122,228 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
ctx.body = data;
|
ctx.body = data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const calcId = convertId(ctx.params.id, IdType.CalckeyId);
|
||||||
|
const data = await client.getAccount(calcId);
|
||||||
|
ctx.body = convertAccount(data.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.get<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/statuses",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.getAccountStatuses(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
convertTimelinesArgsId(argsToBools(limitToInt(ctx.query as any))),
|
||||||
|
);
|
||||||
|
ctx.body = data.data.map(status => convertStatus(status));
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.get<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/followers",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.getAccountFollowers(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
convertTimelinesArgsId(limitToInt(ctx.query as any)),
|
||||||
|
);
|
||||||
|
ctx.body = data.data.map(account => convertAccount(account));
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.get<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/following",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.getAccountFollowing(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
convertTimelinesArgsId(limitToInt(ctx.query as any)),
|
||||||
|
);
|
||||||
|
ctx.body = data.data.map(account => convertAccount(account));
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.get<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/lists",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.getAccountLists(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = data.data.map(list => convertList(list));
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.post<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/follow",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.followAccount(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
);
|
||||||
|
let acct = convertRelationship(data.data);
|
||||||
|
acct.following = true;
|
||||||
|
ctx.body = acct;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.post<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/unfollow",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.unfollowAccount(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
);
|
||||||
|
let acct = convertRelationship(data.data);
|
||||||
|
acct.following = false;
|
||||||
|
ctx.body = acct;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.post<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/block",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.blockAccount(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
);
|
||||||
|
ctx.body = convertRelationship(data.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.post<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/unblock",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.unblockAccount(
|
||||||
|
convertId(ctx.params.id, IdType.MastodonId),
|
||||||
|
);
|
||||||
|
ctx.body = convertRelationship(data.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.post<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/mute",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.muteAccount(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
(ctx.request as any).body as any,
|
||||||
|
);
|
||||||
|
ctx.body = convertRelationship(data.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
router.post<{ Params: { id: string } }>(
|
||||||
|
"/v1/accounts/:id/unmute",
|
||||||
|
async (ctx) => {
|
||||||
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
|
try {
|
||||||
|
const data = await client.unmuteAccount(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
);
|
||||||
|
ctx.body = convertRelationship(data.data);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
console.error(e.response.data);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = e.response.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
router.get("/v1/bookmarks", async (ctx) => {
|
router.get("/v1/bookmarks", async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = (await client.getBookmarks(
|
const data = (await client.getBookmarks(
|
||||||
limitToInt(ctx.query as any),
|
convertTimelinesArgsId(limitToInt(ctx.query as any)),
|
||||||
)) as any;
|
));
|
||||||
let resp = data.data;
|
ctx.body = data.data.map(status => convertStatus(status));
|
||||||
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
|
||||||
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
|
||||||
resp[statIdx].in_reply_to_account_id = resp[statIdx]
|
|
||||||
.in_reply_to_account_id
|
|
||||||
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
|
|
||||||
: null;
|
|
||||||
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
|
|
||||||
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
|
|
||||||
: null;
|
|
||||||
let mentions = resp[statIdx].mentions;
|
|
||||||
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
|
|
||||||
resp[statIdx].mentions[mtnIdx].id = convertId(
|
|
||||||
mentions[mtnIdx].id,
|
|
||||||
IdType.MastodonId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -417,26 +356,8 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getFavourites(limitToInt(ctx.query as any));
|
const data = await client.getFavourites(convertTimelinesArgsId(limitToInt(ctx.query as any)));
|
||||||
let resp = data.data;
|
ctx.body = data.data.map(status => convertStatus(status));
|
||||||
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
|
||||||
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
|
||||||
resp[statIdx].in_reply_to_account_id = resp[statIdx]
|
|
||||||
.in_reply_to_account_id
|
|
||||||
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
|
|
||||||
: null;
|
|
||||||
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
|
|
||||||
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
|
|
||||||
: null;
|
|
||||||
let mentions = resp[statIdx].mentions;
|
|
||||||
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
|
|
||||||
resp[statIdx].mentions[mtnIdx].id = convertId(
|
|
||||||
mentions[mtnIdx].id,
|
|
||||||
IdType.MastodonId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -449,12 +370,8 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getMutes(limitToInt(ctx.query as any));
|
const data = await client.getMutes(convertTimelinesArgsId(limitToInt(ctx.query as any)));
|
||||||
let resp = data.data;
|
ctx.body = data.data.map(account => convertAccount(account));
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -467,12 +384,8 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getBlocks(limitToInt(ctx.query as any));
|
const data = await client.getBlocks(convertTimelinesArgsId(limitToInt(ctx.query as any)));
|
||||||
let resp = data.data;
|
ctx.body = data.data.map(account => convertAccount(account));
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -488,11 +401,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const data = await client.getFollowRequests(
|
const data = await client.getFollowRequests(
|
||||||
((ctx.query as any) || { limit: 20 }).limit,
|
((ctx.query as any) || { limit: 20 }).limit,
|
||||||
);
|
);
|
||||||
let resp = data.data;
|
ctx.body = data.data.map(account => convertAccount(account));
|
||||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
|
||||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
|
||||||
}
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -510,9 +419,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const data = await client.acceptFollowRequest(
|
const data = await client.acceptFollowRequest(
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
);
|
);
|
||||||
let resp = data.data;
|
ctx.body = convertRelationship(data.data);
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -531,9 +438,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
const data = await client.rejectFollowRequest(
|
const data = await client.rejectFollowRequest(
|
||||||
convertId(ctx.params.id, IdType.CalckeyId),
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
);
|
);
|
||||||
let resp = data.data;
|
ctx.body = convertRelationship(data.data);
|
||||||
resp.id = convertId(resp.id, IdType.MastodonId);
|
|
||||||
ctx.body = resp;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||||
import Router from "@koa/router";
|
import Router from "@koa/router";
|
||||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||||
|
import { IdType, convertId } from "../../index.js";
|
||||||
|
import { convertFilter } from "../converters.js";
|
||||||
|
|
||||||
export function apiFilterMastodon(router: Router): void {
|
export function apiFilterMastodon(router: Router): void {
|
||||||
router.get("/v1/filters", async (ctx) => {
|
router.get("/v1/filters", async (ctx) => {
|
||||||
|
@ -10,7 +12,7 @@ export function apiFilterMastodon(router: Router): void {
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const data = await client.getFilters();
|
const data = await client.getFilters();
|
||||||
ctx.body = data.data;
|
ctx.body = data.data.map(filter => convertFilter(filter));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -24,8 +26,10 @@ export function apiFilterMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const data = await client.getFilter(ctx.params.id);
|
const data = await client.getFilter(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertFilter(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -40,7 +44,7 @@ export function apiFilterMastodon(router: Router): void {
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const data = await client.createFilter(body.phrase, body.context, body);
|
const data = await client.createFilter(body.phrase, body.context, body);
|
||||||
ctx.body = data.data;
|
ctx.body = convertFilter(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -55,11 +59,11 @@ export function apiFilterMastodon(router: Router): void {
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const data = await client.updateFilter(
|
const data = await client.updateFilter(
|
||||||
ctx.params.id,
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
body.phrase,
|
body.phrase,
|
||||||
body.context,
|
body.context,
|
||||||
);
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = convertFilter(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -73,7 +77,9 @@ export function apiFilterMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const data = await client.deleteFilter(ctx.params.id);
|
const data = await client.deleteFilter(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||||
import Router from "@koa/router";
|
import Router from "@koa/router";
|
||||||
import { koaBody } from "koa-body";
|
import { koaBody } from "koa-body";
|
||||||
|
import { convertId, IdType } from "../../index.js";
|
||||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||||
import { toTextWithReaction } from "./timeline.js";
|
import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js";
|
||||||
|
import { convertNotification } from "../converters.js";
|
||||||
function toLimitToInt(q: any) {
|
function toLimitToInt(q: any) {
|
||||||
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
|
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
|
||||||
return q;
|
return q;
|
||||||
|
@ -15,9 +17,10 @@ export function apiNotificationsMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const data = await client.getNotifications(toLimitToInt(ctx.query));
|
const data = await client.getNotifications(convertTimelinesArgsId(toLimitToInt(ctx.query)));
|
||||||
const notfs = data.data;
|
const notfs = data.data;
|
||||||
const ret = notfs.map((n) => {
|
const ret = notfs.map((n) => {
|
||||||
|
n = convertNotification(n);
|
||||||
if (n.type !== "follow" && n.type !== "follow_request") {
|
if (n.type !== "follow" && n.type !== "follow_request") {
|
||||||
if (n.type === "reaction") n.type = "favourite";
|
if (n.type === "reaction") n.type = "favourite";
|
||||||
n.status = toTextWithReaction(
|
n.status = toTextWithReaction(
|
||||||
|
@ -43,8 +46,10 @@ export function apiNotificationsMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const dataRaw = await client.getNotification(ctx.params.id);
|
const dataRaw = await client.getNotification(
|
||||||
const data = dataRaw.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
const data = convertNotification(dataRaw.data);
|
||||||
if (data.type !== "follow" && data.type !== "follow_request") {
|
if (data.type !== "follow" && data.type !== "follow_request") {
|
||||||
if (data.type === "reaction") data.type = "favourite";
|
if (data.type === "reaction") data.type = "favourite";
|
||||||
ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0];
|
ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0];
|
||||||
|
@ -79,7 +84,9 @@ export function apiNotificationsMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const data = await client.dismissNotification(ctx.params.id);
|
const data = await client.dismissNotification(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
@ -3,7 +3,8 @@ import Router from "@koa/router";
|
||||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Converter } from "@calckey/megalodon";
|
import { Converter } from "@calckey/megalodon";
|
||||||
import { limitToInt } from "./timeline.js";
|
import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
|
||||||
|
import { convertAccount, convertStatus } from "../converters.js";
|
||||||
|
|
||||||
export function apiSearchMastodon(router: Router): void {
|
export function apiSearchMastodon(router: Router): void {
|
||||||
router.get("/v1/search", async (ctx) => {
|
router.get("/v1/search", async (ctx) => {
|
||||||
|
@ -12,7 +13,7 @@ export function apiSearchMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = ctx.request.body;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const query: any = limitToInt(ctx.query);
|
const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
|
||||||
const type = query.type || "";
|
const type = query.type || "";
|
||||||
const data = await client.search(query.q, type, query);
|
const data = await client.search(query.q, type, query);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
|
@ -27,18 +28,18 @@ export function apiSearchMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const query: any = limitToInt(ctx.query);
|
const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
|
||||||
const type = query.type;
|
const type = query.type;
|
||||||
if (type) {
|
if (type) {
|
||||||
const data = await client.search(query.q, type, query);
|
const data = await client.search(query.q, type, query);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data.accounts.map(account => convertAccount(account));
|
||||||
} else {
|
} else {
|
||||||
const acct = await client.search(query.q, "accounts", query);
|
const acct = await client.search(query.q, "accounts", query);
|
||||||
const stat = await client.search(query.q, "statuses", query);
|
const stat = await client.search(query.q, "statuses", query);
|
||||||
const tags = await client.search(query.q, "hashtags", query);
|
const tags = await client.search(query.q, "hashtags", query);
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
accounts: acct.data.accounts,
|
accounts: acct.data.accounts.map(account => convertAccount(account)),
|
||||||
statuses: stat.data.statuses,
|
statuses: stat.data.statuses.map(status => convertStatus(status)),
|
||||||
hashtags: tags.data.hashtags,
|
hashtags: tags.data.hashtags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -57,7 +58,7 @@ export function apiSearchMastodon(router: Router): void {
|
||||||
ctx.request.hostname,
|
ctx.request.hostname,
|
||||||
accessTokens,
|
accessTokens,
|
||||||
);
|
);
|
||||||
ctx.body = data;
|
ctx.body = data.map(status => convertStatus(status));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -69,12 +70,16 @@ export function apiSearchMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
try {
|
try {
|
||||||
const query: any = ctx.query;
|
const query: any = ctx.query;
|
||||||
const data = await getFeaturedUser(
|
let data = await getFeaturedUser(
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
ctx.request.hostname,
|
ctx.request.hostname,
|
||||||
accessTokens,
|
accessTokens,
|
||||||
query.limit || 20,
|
query.limit || 20,
|
||||||
);
|
);
|
||||||
|
data = data.map(suggestion => {
|
||||||
|
suggestion.account = convertAccount(suggestion.account);
|
||||||
|
return suggestion;
|
||||||
|
});
|
||||||
console.log(data);
|
console.log(data);
|
||||||
ctx.body = data;
|
ctx.body = data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -4,7 +4,9 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import querystring from "node:querystring";
|
import querystring from "node:querystring";
|
||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
import { limitToInt } from "./timeline.js";
|
import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
|
||||||
|
import { convertId, IdType } from "../../index.js";
|
||||||
|
import { convertAccount, convertAttachment, convertPoll, convertStatus } from "../converters.js";
|
||||||
|
|
||||||
function normalizeQuery(data: any) {
|
function normalizeQuery(data: any) {
|
||||||
const str = querystring.stringify(data);
|
const str = querystring.stringify(data);
|
||||||
|
@ -18,6 +20,8 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
let body: any = ctx.request.body;
|
let body: any = ctx.request.body;
|
||||||
|
if (body.in_reply_to_id)
|
||||||
|
body.in_reply_to_id = convertId(body.in_reply_to_id, IdType.CalckeyId);
|
||||||
if (
|
if (
|
||||||
(!body.poll && body["poll[options][]"]) ||
|
(!body.poll && body["poll[options][]"]) ||
|
||||||
(!body.media_ids && body["media_ids[]"])
|
(!body.media_ids && body["media_ids[]"])
|
||||||
|
@ -54,7 +58,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
body.sensitive =
|
body.sensitive =
|
||||||
typeof sensitive === "string" ? sensitive === "true" : sensitive;
|
typeof sensitive === "string" ? sensitive === "true" : sensitive;
|
||||||
const data = await client.postStatus(text, body);
|
const data = await client.postStatus(text, body);
|
||||||
ctx.body = data.data;
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -66,8 +70,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getStatus(ctx.params.id);
|
const data = await client.getStatus(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
);
|
||||||
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -79,7 +85,9 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.deleteStatus(ctx.params.id);
|
const data = await client.deleteStatus(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e.response.data, request.params.id);
|
console.error(e.response.data, request.params.id);
|
||||||
|
@ -100,10 +108,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const id = ctx.params.id;
|
const id = convertId(ctx.params.id, IdType.CalckeyId);
|
||||||
const data = await client.getStatusContext(
|
const data = await client.getStatusContext(
|
||||||
id,
|
id,
|
||||||
limitToInt(ctx.query as any),
|
convertTimelinesArgsId(limitToInt(ctx.query as any)),
|
||||||
);
|
);
|
||||||
const status = await client.getStatus(id);
|
const status = await client.getStatus(id);
|
||||||
let reqInstance = axios.create({
|
let reqInstance = axios.create({
|
||||||
|
@ -126,6 +134,8 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
text,
|
text,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
data.data.ancestors = data.data.ancestors.map(status => convertStatus(status));
|
||||||
|
data.data.descendants = data.data.descendants.map(status => convertStatus(status));
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -141,8 +151,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getStatusRebloggedBy(ctx.params.id);
|
const data = await client.getStatusRebloggedBy(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = data.data.map(account => convertAccount(account));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -165,11 +177,11 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const react = await getFirstReaction(BASE_URL, accessTokens);
|
const react = await getFirstReaction(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const a = (await client.createEmojiReaction(
|
const a = (await client.createEmojiReaction(
|
||||||
ctx.params.id,
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
react,
|
react,
|
||||||
)) as any;
|
)) as any;
|
||||||
//const data = await client.favouriteStatus(ctx.params.id) as any;
|
//const data = await client.favouriteStatus(ctx.params.id) as any;
|
||||||
ctx.body = a.data;
|
ctx.body = convertStatus(a.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -186,8 +198,11 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const react = await getFirstReaction(BASE_URL, accessTokens);
|
const react = await getFirstReaction(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.deleteEmojiReaction(ctx.params.id, react);
|
const data = await client.deleteEmojiReaction(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
react
|
||||||
|
);
|
||||||
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -203,8 +218,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.reblogStatus(ctx.params.id);
|
const data = await client.reblogStatus(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -220,8 +237,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.unreblogStatus(ctx.params.id);
|
const data = await client.unreblogStatus(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -237,8 +256,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.bookmarkStatus(ctx.params.id);
|
const data = await client.bookmarkStatus(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -254,8 +275,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = (await client.unbookmarkStatus(ctx.params.id)) as any;
|
const data = await client.unbookmarkStatus(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -271,8 +294,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.pinStatus(ctx.params.id);
|
const data = await client.pinStatus(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -288,8 +313,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.unpinStatus(ctx.params.id);
|
const data = await client.unpinStatus(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertStatus(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -302,8 +329,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getMedia(ctx.params.id);
|
const data = await client.getMedia(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertAttachment(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -316,10 +345,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.updateMedia(
|
const data = await client.updateMedia(
|
||||||
ctx.params.id,
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
ctx.request.body as any,
|
ctx.request.body as any,
|
||||||
);
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = convertAttachment(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -331,8 +360,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getPoll(ctx.params.id);
|
const data = await client.getPoll(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId)
|
||||||
|
);
|
||||||
|
ctx.body = convertPoll(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -347,10 +378,10 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.votePoll(
|
const data = await client.votePoll(
|
||||||
ctx.params.id,
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
(ctx.request.body as any).choices,
|
(ctx.request.body as any).choices,
|
||||||
);
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = convertPoll(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||||
import { statusModel } from "./status.js";
|
import { statusModel } from "./status.js";
|
||||||
import Autolinker from "autolinker";
|
import Autolinker from "autolinker";
|
||||||
import { ParsedUrlQuery } from "querystring";
|
import { ParsedUrlQuery } from "querystring";
|
||||||
|
import { convertAccount, convertList, convertStatus } from "../converters.js";
|
||||||
|
import { convertId, IdType } from "../../index.js";
|
||||||
|
|
||||||
export function limitToInt(q: ParsedUrlQuery) {
|
export function limitToInt(q: ParsedUrlQuery) {
|
||||||
let object: any = q;
|
let object: any = q;
|
||||||
|
@ -29,6 +31,16 @@ export function argsToBools(q: ParsedUrlQuery) {
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertTimelinesArgsId(q: ParsedUrlQuery) {
|
||||||
|
if (typeof q.min_id === "string")
|
||||||
|
q.min_id = convertId(q.min_id, IdType.CalckeyId);
|
||||||
|
if (typeof q.max_id === "string")
|
||||||
|
q.max_id = convertId(q.max_id, IdType.CalckeyId);
|
||||||
|
if (typeof q.since_id === "string")
|
||||||
|
q.since_id = convertId(q.since_id, IdType.CalckeyId);
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
export function toTextWithReaction(status: Entity.Status[], host: string) {
|
export function toTextWithReaction(status: Entity.Status[], host: string) {
|
||||||
return status.map((t) => {
|
return status.map((t) => {
|
||||||
if (!t) return statusModel(null, null, [], "no content");
|
if (!t) return statusModel(null, null, [], "no content");
|
||||||
|
@ -97,9 +109,10 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
try {
|
try {
|
||||||
const query: any = ctx.query;
|
const query: any = ctx.query;
|
||||||
const data = query.local
|
const data = query.local
|
||||||
? await client.getLocalTimeline(argsToBools(limitToInt(query)))
|
? await client.getLocalTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))))
|
||||||
: await client.getPublicTimeline(argsToBools(limitToInt(query)));
|
: await client.getPublicTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))));
|
||||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
let resp = data.data.map(status => convertStatus(status));
|
||||||
|
ctx.body = toTextWithReaction(resp, ctx.hostname);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -116,9 +129,10 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
try {
|
try {
|
||||||
const data = await client.getTagTimeline(
|
const data = await client.getTagTimeline(
|
||||||
ctx.params.hashtag,
|
ctx.params.hashtag,
|
||||||
argsToBools(limitToInt(ctx.query)),
|
convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))),
|
||||||
);
|
);
|
||||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
let resp = data.data.map(status => convertStatus(status));
|
||||||
|
ctx.body = toTextWithReaction(resp, ctx.hostname);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -132,8 +146,9 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getHomeTimeline(limitToInt(ctx.query));
|
const data = await client.getHomeTimeline(convertTimelinesArgsId(limitToInt(ctx.query)));
|
||||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
let resp = data.data.map(status => convertStatus(status));
|
||||||
|
ctx.body = toTextWithReaction(resp, ctx.hostname);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -149,10 +164,11 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getListTimeline(
|
const data = await client.getListTimeline(
|
||||||
ctx.params.listId,
|
convertId(ctx.params.listId, IdType.CalckeyId),
|
||||||
limitToInt(ctx.query),
|
convertTimelinesArgsId(limitToInt(ctx.query)),
|
||||||
);
|
);
|
||||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
let resp = data.data.map(status => convertStatus(status));
|
||||||
|
ctx.body = toTextWithReaction(resp, ctx.hostname);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -166,7 +182,7 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getConversationTimeline(limitToInt(ctx.query));
|
const data = await client.getConversationTimeline(convertTimelinesArgsId(limitToInt(ctx.query)));
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -181,7 +197,7 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getLists();
|
const data = await client.getLists();
|
||||||
ctx.body = data.data;
|
ctx.body = data.data.map(list => convertList(list));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -196,8 +212,10 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getList(ctx.params.id);
|
const data = await client.getList(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
);
|
||||||
|
ctx.body = convertList(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -212,7 +230,7 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.createList((ctx.request.body as any).title);
|
const data = await client.createList((ctx.request.body as any).title);
|
||||||
ctx.body = data.data;
|
ctx.body = convertList(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -227,8 +245,11 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.updateList(ctx.params.id, (ctx.request.body as any).title);
|
const data = await client.updateList(
|
||||||
ctx.body = data.data;
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
(ctx.request.body as any).title
|
||||||
|
);
|
||||||
|
ctx.body = convertList(data.data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -244,7 +265,9 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.deleteList(ctx.params.id);
|
const data = await client.deleteList(
|
||||||
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -262,10 +285,10 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.getAccountsInList(
|
const data = await client.getAccountsInList(
|
||||||
ctx.params.id,
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
ctx.query as any,
|
convertTimelinesArgsId(ctx.query as any),
|
||||||
);
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data.map(account => convertAccount(account));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
@ -282,8 +305,8 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.addAccountsToList(
|
const data = await client.addAccountsToList(
|
||||||
ctx.params.id,
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
(ctx.query as any).account_ids,
|
(ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)),
|
||||||
);
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -302,8 +325,8 @@ export function apiTimelineMastodon(router: Router): void {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
const data = await client.deleteAccountsFromList(
|
const data = await client.deleteAccountsFromList(
|
||||||
ctx.params.id,
|
convertId(ctx.params.id, IdType.CalckeyId),
|
||||||
(ctx.query as any).account_ids,
|
(ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)),
|
||||||
);
|
);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default async (ctx: Koa.Context) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = await validateEmailForAccount(emailAddress);
|
const { available } = await validateEmailForAccount(emailAddress);
|
||||||
if (!available) {
|
if (!available) {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -399,28 +399,31 @@ router.get("/notes/:note", async (ctx, next) => {
|
||||||
visibility: In(["public", "home"]),
|
visibility: In(["public", "home"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note) {
|
try {
|
||||||
const _note = await Notes.pack(note);
|
if (note) {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
|
const _note = await Notes.pack(note);
|
||||||
const meta = await fetchMeta();
|
|
||||||
await ctx.render("note", {
|
|
||||||
note: _note,
|
|
||||||
profile,
|
|
||||||
avatarUrl: await Users.getAvatarUrl(
|
|
||||||
await Users.findOneByOrFail({ id: note.userId }),
|
|
||||||
),
|
|
||||||
// TODO: Let locale changeable by instance setting
|
|
||||||
summary: getNoteSummary(_note),
|
|
||||||
instanceName: meta.name || "Calckey",
|
|
||||||
icon: meta.iconUrl,
|
|
||||||
privateMode: meta.privateMode,
|
|
||||||
themeColor: meta.themeColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.set("Cache-Control", "public, max-age=15");
|
const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
await ctx.render("note", {
|
||||||
|
note: _note,
|
||||||
|
profile,
|
||||||
|
avatarUrl: await Users.getAvatarUrl(
|
||||||
|
await Users.findOneByOrFail({ id: note.userId }),
|
||||||
|
),
|
||||||
|
// TODO: Let locale changeable by instance setting
|
||||||
|
summary: getNoteSummary(_note),
|
||||||
|
instanceName: meta.name || "Calckey",
|
||||||
|
icon: meta.iconUrl,
|
||||||
|
privateMode: meta.privateMode,
|
||||||
|
themeColor: meta.themeColor,
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
ctx.set("Cache-Control", "public, max-age=15");
|
||||||
}
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,11 +6,13 @@ import {
|
||||||
NoteThreadMutings,
|
NoteThreadMutings,
|
||||||
UserProfiles,
|
UserProfiles,
|
||||||
Users,
|
Users,
|
||||||
|
Followings,
|
||||||
} from "@/models/index.js";
|
} from "@/models/index.js";
|
||||||
import { genId } from "@/misc/gen-id.js";
|
import { genId } from "@/misc/gen-id.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User } from "@/models/entities/user.js";
|
||||||
import type { Notification } from "@/models/entities/notification.js";
|
import type { Notification } from "@/models/entities/notification.js";
|
||||||
import { sendEmailNotification } from "./send-email-notification.js";
|
import { sendEmailNotification } from "./send-email-notification.js";
|
||||||
|
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
|
||||||
|
|
||||||
export async function createNotification(
|
export async function createNotification(
|
||||||
notifieeId: User["id"],
|
notifieeId: User["id"],
|
||||||
|
@ -21,6 +23,26 @@ export async function createNotification(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.notifierId &&
|
||||||
|
["mention", "reply", "renote", "quote", "reaction"].includes(type)
|
||||||
|
) {
|
||||||
|
const notifier = await Users.findOneBy({ id: data.notifierId });
|
||||||
|
// suppress if the notifier does not exist or is silenced.
|
||||||
|
if (!notifier) return null;
|
||||||
|
|
||||||
|
// suppress if the notifier is silenced or in a silenced instance, and not followed by the notifiee.
|
||||||
|
if (
|
||||||
|
(notifier.isSilenced ||
|
||||||
|
(Users.isRemoteUser(notifier) &&
|
||||||
|
(await shouldSilenceInstance(notifier.host)))) &&
|
||||||
|
!(await Followings.exist({
|
||||||
|
where: { followerId: notifieeId, followeeId: data.notifierId },
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const profile = await UserProfiles.findOneBy({ userId: notifieeId });
|
const profile = await UserProfiles.findOneBy({ userId: notifieeId });
|
||||||
|
|
||||||
const isMuted = profile?.mutingNotificationTypes.includes(type);
|
const isMuted = profile?.mutingNotificationTypes.includes(type);
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
||||||
import { webhookDeliver } from "@/queue/index.js";
|
import { webhookDeliver } from "@/queue/index.js";
|
||||||
|
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
|
||||||
|
|
||||||
const logger = new Logger("following/create");
|
const logger = new Logger("following/create");
|
||||||
|
|
||||||
|
@ -226,13 +227,19 @@ export default async function (
|
||||||
});
|
});
|
||||||
|
|
||||||
// フォロー対象が鍵アカウントである or
|
// フォロー対象が鍵アカウントである or
|
||||||
|
// The follower is silenced, or
|
||||||
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||||||
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
|
||||||
|
// The follower is remote, the followee is local, and the follower is in a silenced instance.
|
||||||
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
||||||
if (
|
if (
|
||||||
followee.isLocked ||
|
followee.isLocked ||
|
||||||
|
follower.isSilenced ||
|
||||||
(followeeProfile.carefulBot && follower.isBot) ||
|
(followeeProfile.carefulBot && follower.isBot) ||
|
||||||
(Users.isLocalUser(follower) && Users.isRemoteUser(followee))
|
(Users.isLocalUser(follower) && Users.isRemoteUser(followee)) ||
|
||||||
|
(Users.isRemoteUser(follower) &&
|
||||||
|
Users.isLocalUser(followee) &&
|
||||||
|
(await shouldSilenceInstance(follower.host)))
|
||||||
) {
|
) {
|
||||||
let autoAccept = false;
|
let autoAccept = false;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type { User } from "@/models/entities/user.js";
|
||||||
import { Blockings, FollowRequests, Users } from "@/models/index.js";
|
import { Blockings, FollowRequests, Users } from "@/models/index.js";
|
||||||
import { genId } from "@/misc/gen-id.js";
|
import { genId } from "@/misc/gen-id.js";
|
||||||
import { createNotification } from "../../create-notification.js";
|
import { createNotification } from "../../create-notification.js";
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
|
||||||
export default async function (
|
export default async function (
|
||||||
follower: {
|
follower: {
|
||||||
|
@ -79,7 +80,13 @@ export default async function (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
|
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
|
||||||
const content = renderActivity(renderFollow(follower, followee));
|
const content = renderActivity(
|
||||||
|
renderFollow(
|
||||||
|
follower,
|
||||||
|
followee,
|
||||||
|
requestId ?? `${config.url}/follows/${followRequest.id}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
deliver(follower, content, followee.inbox);
|
deliver(follower, content, followee.inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ import {
|
||||||
} from "@/models/index.js";
|
} from "@/models/index.js";
|
||||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||||
import type { App } from "@/models/entities/app.js";
|
import type { App } from "@/models/entities/app.js";
|
||||||
import { Not, In } from "typeorm";
|
import { Not, In, IsNull } from "typeorm";
|
||||||
import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
|
import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
|
||||||
import { genId } from "@/misc/gen-id.js";
|
import { genId } from "@/misc/gen-id.js";
|
||||||
import {
|
import {
|
||||||
|
@ -66,6 +66,7 @@ import { Cache } from "@/misc/cache.js";
|
||||||
import type { UserProfile } from "@/models/entities/user-profile.js";
|
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||||
import { db } from "@/db/postgre.js";
|
import { db } from "@/db/postgre.js";
|
||||||
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
|
||||||
|
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
|
||||||
|
|
||||||
const mutedWordsCache = new Cache<
|
const mutedWordsCache = new Cache<
|
||||||
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
|
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
|
||||||
|
@ -166,6 +167,7 @@ export default async (
|
||||||
data: Option,
|
data: Option,
|
||||||
silent = false,
|
silent = false,
|
||||||
) =>
|
) =>
|
||||||
|
// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
|
||||||
new Promise<Note>(async (res, rej) => {
|
new Promise<Note>(async (res, rej) => {
|
||||||
// If you reply outside the channel, match the scope of the target.
|
// If you reply outside the channel, match the scope of the target.
|
||||||
// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
|
// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
|
||||||
|
@ -203,6 +205,15 @@ export default async (
|
||||||
data.visibility = "home";
|
data.visibility = "home";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enforce home visibility if the user is in a silenced instance.
|
||||||
|
if (
|
||||||
|
data.visibility === "public" &&
|
||||||
|
Users.isRemoteUser(user) &&
|
||||||
|
(await shouldSilenceInstance(user.host))
|
||||||
|
) {
|
||||||
|
data.visibility = "home";
|
||||||
|
}
|
||||||
|
|
||||||
// Reject if the target of the renote is a public range other than "Home or Entire".
|
// Reject if the target of the renote is a public range other than "Home or Entire".
|
||||||
if (
|
if (
|
||||||
data.renote &&
|
data.renote &&
|
||||||
|
|
|
@ -118,7 +118,7 @@ export default async (
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
// Create notification if the reaction target is a local user.
|
||||||
if (note.userHost === null) {
|
if (note.userHost === null) {
|
||||||
createNotification(note.userId, "reaction", {
|
createNotification(note.userId, "reaction", {
|
||||||
notifierId: user.id,
|
notifierId: user.id,
|
||||||
|
@ -143,7 +143,7 @@ export default async (
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region 配信
|
//#region deliver
|
||||||
if (Users.isLocalUser(user) && !note.localOnly) {
|
if (Users.isLocalUser(user) && !note.localOnly) {
|
||||||
const content = renderActivity(await renderLike(record, note));
|
const content = renderActivity(await renderLike(record, note));
|
||||||
const dm = new DeliverManager(user, content);
|
const dm = new DeliverManager(user, content);
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
node_modules
|
|
||||||
/built
|
|
||||||
/coverage
|
|
||||||
/.eslintrc.js
|
|
||||||
/jest.config.ts
|
|
||||||
/test
|
|
||||||
/test-d
|
|
|
@ -1,65 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
parserOptions: {
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
project: ["./tsconfig.json"],
|
|
||||||
},
|
|
||||||
plugins: ["@typescript-eslint"],
|
|
||||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
|
||||||
rules: {
|
|
||||||
indent: [
|
|
||||||
"error",
|
|
||||||
"tab",
|
|
||||||
{
|
|
||||||
SwitchCase: 1,
|
|
||||||
MemberExpression: "off",
|
|
||||||
flatTernaryExpressions: true,
|
|
||||||
ArrayExpression: "first",
|
|
||||||
ObjectExpression: "first",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"eol-last": ["error", "always"],
|
|
||||||
semi: ["error", "always"],
|
|
||||||
quotes: ["error", "single"],
|
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
|
||||||
"keyword-spacing": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
before: true,
|
|
||||||
after: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"key-spacing": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
beforeColon: false,
|
|
||||||
afterColon: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"space-infix-ops": ["error"],
|
|
||||||
"space-before-blocks": ["error", "always"],
|
|
||||||
"object-curly-spacing": ["error", "always"],
|
|
||||||
"nonblock-statement-body-position": ["error", "beside"],
|
|
||||||
eqeqeq: ["error", "always", { null: "ignore" }],
|
|
||||||
"no-multiple-empty-lines": ["error", { max: 1 }],
|
|
||||||
"no-multi-spaces": ["error"],
|
|
||||||
"no-var": ["error"],
|
|
||||||
"prefer-arrow-callback": ["error"],
|
|
||||||
"no-throw-literal": ["error"],
|
|
||||||
"no-param-reassign": ["warn"],
|
|
||||||
"no-constant-condition": ["warn"],
|
|
||||||
"no-empty-pattern": ["warn"],
|
|
||||||
"@typescript-eslint/no-unnecessary-condition": ["error"],
|
|
||||||
"@typescript-eslint/no-inferrable-types": ["warn"],
|
|
||||||
"@typescript-eslint/no-non-null-assertion": ["warn"],
|
|
||||||
"@typescript-eslint/explicit-function-return-type": ["warn"],
|
|
||||||
"@typescript-eslint/no-misused-promises": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
checksVoidReturn: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"@typescript-eslint/consistent-type-imports": "error",
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -9,9 +9,8 @@
|
||||||
"tsd": "tsd",
|
"tsd": "tsd",
|
||||||
"api": "pnpm api-extractor run --local --verbose",
|
"api": "pnpm api-extractor run --local --verbose",
|
||||||
"api-prod": "pnpm api-extractor run --verbose",
|
"api-prod": "pnpm api-extractor run --verbose",
|
||||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "pnpm typecheck && pnpm eslint",
|
"lint": "pnpm typecheck && pnpm rome check \"src/*.ts\"",
|
||||||
"jest": "jest --coverage --detectOpenHandles",
|
"jest": "jest --coverage --detectOpenHandles",
|
||||||
"test": "pnpm jest && pnpm tsd"
|
"test": "pnpm jest && pnpm tsd"
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,6 +55,7 @@ export type Endpoints = {
|
||||||
"admin/get-table-stats": { req: TODO; res: TODO };
|
"admin/get-table-stats": { req: TODO; res: TODO };
|
||||||
"admin/invite": { req: TODO; res: TODO };
|
"admin/invite": { req: TODO; res: TODO };
|
||||||
"admin/logs": { req: TODO; res: TODO };
|
"admin/logs": { req: TODO; res: TODO };
|
||||||
|
"admin/meta": { req: TODO; res: TODO };
|
||||||
"admin/reset-password": { req: TODO; res: TODO };
|
"admin/reset-password": { req: TODO; res: TODO };
|
||||||
"admin/resolve-abuse-user-report": { req: TODO; res: TODO };
|
"admin/resolve-abuse-user-report": { req: TODO; res: TODO };
|
||||||
"admin/resync-chart": { req: TODO; res: TODO };
|
"admin/resync-chart": { req: TODO; res: TODO };
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
"autosize": "5.0.2",
|
"autosize": "5.0.2",
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"broadcast-channel": "4.19.1",
|
"broadcast-channel": "4.19.1",
|
||||||
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git",
|
"browser-image-resizer": "github:misskey-dev/browser-image-resizer",
|
||||||
"calckey-js": "workspace:*",
|
"calckey-js": "workspace:*",
|
||||||
"chart.js": "4.1.1",
|
"chart.js": "4.1.1",
|
||||||
"chartjs-adapter-date-fns": "2.0.1",
|
"chartjs-adapter-date-fns": "2.0.1",
|
||||||
|
|
|
@ -195,8 +195,7 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: solid 2px var(--focus);
|
outline: auto;
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.inline {
|
&.inline {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
|
ref="el"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="{ showLess: modelValue, fade: !modelValue }"
|
:class="{ showLess: modelValue, fade: !modelValue }"
|
||||||
@click.stop="toggle"
|
@click.stop="toggle"
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { length } from "stringz";
|
import { length } from "stringz";
|
||||||
import * as misskey from "calckey-js";
|
import * as misskey from "calckey-js";
|
||||||
import { concat } from "@/scripts/array";
|
import { concat } from "@/scripts/array";
|
||||||
|
@ -27,6 +28,8 @@ const emit = defineEmits<{
|
||||||
(ev: "update:modelValue", v: boolean): void;
|
(ev: "update:modelValue", v: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const el = ref<HTMLElement>();
|
||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
return concat([
|
return concat([
|
||||||
props.note.text
|
props.note.text
|
||||||
|
@ -43,6 +46,14 @@ const label = computed(() => {
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
emit("update:modelValue", !props.modelValue);
|
emit("update:modelValue", !props.modelValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
el.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -62,7 +73,8 @@ const toggle = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover > span {
|
&:hover > span,
|
||||||
|
&:focus > span {
|
||||||
background: var(--cwFg) !important;
|
background: var(--cwFg) !important;
|
||||||
color: var(--cwBg) !important;
|
color: var(--cwBg) !important;
|
||||||
}
|
}
|
||||||
|
@ -73,6 +85,7 @@ const toggle = () => {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
> span {
|
> span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
|
@ -81,7 +94,8 @@ const toggle = () => {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
|
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus {
|
||||||
> span {
|
> span {
|
||||||
background: var(--panelHighlight);
|
background: var(--panelHighlight);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="thumbnail" class="zdjebgpv">
|
<button ref="thumbnail" class="zdjebgpv">
|
||||||
<ImgWithBlurhash
|
<ImgWithBlurhash
|
||||||
v-if="isThumbnailAvailable"
|
v-if="isThumbnailAvailable"
|
||||||
:hash="file.blurhash"
|
:hash="file.blurhash"
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
v-if="isThumbnailAvailable && is === 'video'"
|
v-if="isThumbnailAvailable && is === 'video'"
|
||||||
class="ph-file-video ph-bold ph-lg icon-sub"
|
class="ph-file-video ph-bold ph-lg icon-sub"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
> .icon-sub {
|
> .icon-sub {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -1,157 +1,160 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<FocusTrap v-bind:active="isActive">
|
||||||
class="omfetrab"
|
<div
|
||||||
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
|
class="omfetrab"
|
||||||
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
|
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
|
||||||
>
|
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
|
||||||
<input
|
tabindex="-1"
|
||||||
ref="search"
|
>
|
||||||
v-model.trim="q"
|
<input
|
||||||
class="search"
|
ref="search"
|
||||||
data-prevent-emoji-insert
|
v-model.trim="q"
|
||||||
:class="{ filled: q != null && q != '' }"
|
class="search"
|
||||||
:placeholder="i18n.ts.search"
|
data-prevent-emoji-insert
|
||||||
type="search"
|
:class="{ filled: q != null && q != '' }"
|
||||||
@paste.stop="paste"
|
:placeholder="i18n.ts.search"
|
||||||
@keyup.enter="done()"
|
type="search"
|
||||||
/>
|
@paste.stop="paste"
|
||||||
<div ref="emojis" class="emojis">
|
@keyup.enter="done()"
|
||||||
<section class="result">
|
/>
|
||||||
<div v-if="searchResultCustom.length > 0" class="body">
|
<div ref="emojis" class="emojis">
|
||||||
<button
|
<section class="result">
|
||||||
v-for="emoji in searchResultCustom"
|
<div v-if="searchResultCustom.length > 0" class="body">
|
||||||
:key="emoji.id"
|
|
||||||
class="_button item"
|
|
||||||
:title="emoji.name"
|
|
||||||
tabindex="0"
|
|
||||||
@click="chosen(emoji, $event)"
|
|
||||||
>
|
|
||||||
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
|
|
||||||
<img
|
|
||||||
class="emoji"
|
|
||||||
:src="
|
|
||||||
disableShowingAnimatedImages
|
|
||||||
? getStaticImageUrl(emoji.url)
|
|
||||||
: emoji.url
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="searchResultUnicode.length > 0" class="body">
|
|
||||||
<button
|
|
||||||
v-for="emoji in searchResultUnicode"
|
|
||||||
:key="emoji.name"
|
|
||||||
class="_button item"
|
|
||||||
:title="emoji.name"
|
|
||||||
tabindex="0"
|
|
||||||
@click="chosen(emoji, $event)"
|
|
||||||
>
|
|
||||||
<MkEmoji class="emoji" :emoji="emoji.char" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div v-if="tab === 'index'" class="group index">
|
|
||||||
<section v-if="showPinned">
|
|
||||||
<div class="body">
|
|
||||||
<button
|
<button
|
||||||
v-for="emoji in pinned"
|
v-for="emoji in searchResultCustom"
|
||||||
:key="emoji"
|
:key="emoji.id"
|
||||||
class="_button item"
|
class="_button item"
|
||||||
|
:title="emoji.name"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji
|
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
|
||||||
|
<img
|
||||||
class="emoji"
|
class="emoji"
|
||||||
:emoji="emoji"
|
:src="
|
||||||
:normal="true"
|
disableShowingAnimatedImages
|
||||||
|
? getStaticImageUrl(emoji.url)
|
||||||
|
: emoji.url
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="searchResultUnicode.length > 0" class="body">
|
||||||
|
<button
|
||||||
|
v-for="emoji in searchResultUnicode"
|
||||||
|
:key="emoji.name"
|
||||||
|
class="_button item"
|
||||||
|
:title="emoji.name"
|
||||||
|
tabindex="0"
|
||||||
|
@click="chosen(emoji, $event)"
|
||||||
|
>
|
||||||
|
<MkEmoji class="emoji" :emoji="emoji.char" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<div v-if="tab === 'index'" class="group index">
|
||||||
<header class="_acrylic">
|
<section v-if="showPinned">
|
||||||
<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
|
<div class="body">
|
||||||
{{ i18n.ts.recentUsed }}
|
<button
|
||||||
</header>
|
v-for="emoji in pinned"
|
||||||
<div class="body">
|
:key="emoji"
|
||||||
<button
|
class="_button item"
|
||||||
v-for="emoji in recentlyUsedEmojis"
|
tabindex="0"
|
||||||
:key="emoji"
|
@click="chosen(emoji, $event)"
|
||||||
class="_button item"
|
>
|
||||||
@click="chosen(emoji, $event)"
|
<MkEmoji
|
||||||
>
|
class="emoji"
|
||||||
<MkEmoji
|
:emoji="emoji"
|
||||||
class="emoji"
|
:normal="true"
|
||||||
:emoji="emoji"
|
/>
|
||||||
:normal="true"
|
</button>
|
||||||
/>
|
</div>
|
||||||
</button>
|
</section>
|
||||||
</div>
|
|
||||||
</section>
|
<section>
|
||||||
|
<header class="_acrylic">
|
||||||
|
<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
|
||||||
|
{{ i18n.ts.recentUsed }}
|
||||||
|
</header>
|
||||||
|
<div class="body">
|
||||||
|
<button
|
||||||
|
v-for="emoji in recentlyUsedEmojis"
|
||||||
|
:key="emoji"
|
||||||
|
class="_button item"
|
||||||
|
@click="chosen(emoji, $event)"
|
||||||
|
>
|
||||||
|
<MkEmoji
|
||||||
|
class="emoji"
|
||||||
|
:emoji="emoji"
|
||||||
|
:normal="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div v-once class="group">
|
||||||
|
<header>{{ i18n.ts.customEmojis }}</header>
|
||||||
|
<XSection
|
||||||
|
v-for="category in customEmojiCategories"
|
||||||
|
:key="'custom:' + category"
|
||||||
|
:initial-shown="false"
|
||||||
|
:emojis="
|
||||||
|
customEmojis
|
||||||
|
.filter((e) => e.category === category)
|
||||||
|
.map((e) => ':' + e.name + ':')
|
||||||
|
"
|
||||||
|
@chosen="chosen"
|
||||||
|
>{{ category || i18n.ts.other }}</XSection
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-once class="group">
|
||||||
|
<header>{{ i18n.ts.emoji }}</header>
|
||||||
|
<XSection
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category"
|
||||||
|
:emojis="
|
||||||
|
emojilist
|
||||||
|
.filter((e) => e.category === category)
|
||||||
|
.map((e) => e.char)
|
||||||
|
"
|
||||||
|
@chosen="chosen"
|
||||||
|
>{{ category }}</XSection
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="group">
|
<div class="tabs">
|
||||||
<header>{{ i18n.ts.customEmojis }}</header>
|
<button
|
||||||
<XSection
|
class="_button tab"
|
||||||
v-for="category in customEmojiCategories"
|
:class="{ active: tab === 'index' }"
|
||||||
:key="'custom:' + category"
|
@click="tab = 'index'"
|
||||||
:initial-shown="false"
|
|
||||||
:emojis="
|
|
||||||
customEmojis
|
|
||||||
.filter((e) => e.category === category)
|
|
||||||
.map((e) => ':' + e.name + ':')
|
|
||||||
"
|
|
||||||
@chosen="chosen"
|
|
||||||
>{{ category || i18n.ts.other }}</XSection
|
|
||||||
>
|
>
|
||||||
</div>
|
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
<div v-once class="group">
|
</button>
|
||||||
<header>{{ i18n.ts.emoji }}</header>
|
<button
|
||||||
<XSection
|
class="_button tab"
|
||||||
v-for="category in categories"
|
:class="{ active: tab === 'custom' }"
|
||||||
:key="category"
|
@click="tab = 'custom'"
|
||||||
:emojis="
|
|
||||||
emojilist
|
|
||||||
.filter((e) => e.category === category)
|
|
||||||
.map((e) => e.char)
|
|
||||||
"
|
|
||||||
@chosen="chosen"
|
|
||||||
>{{ category }}</XSection
|
|
||||||
>
|
>
|
||||||
|
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button tab"
|
||||||
|
:class="{ active: tab === 'unicode' }"
|
||||||
|
@click="tab = 'unicode'"
|
||||||
|
>
|
||||||
|
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button tab"
|
||||||
|
:class="{ active: tab === 'tags' }"
|
||||||
|
@click="tab = 'tags'"
|
||||||
|
>
|
||||||
|
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs">
|
</FocusTrap>
|
||||||
<button
|
|
||||||
class="_button tab"
|
|
||||||
:class="{ active: tab === 'index' }"
|
|
||||||
@click="tab = 'index'"
|
|
||||||
>
|
|
||||||
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="_button tab"
|
|
||||||
:class="{ active: tab === 'custom' }"
|
|
||||||
@click="tab = 'custom'"
|
|
||||||
>
|
|
||||||
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="_button tab"
|
|
||||||
:class="{ active: tab === 'unicode' }"
|
|
||||||
@click="tab = 'unicode'"
|
|
||||||
>
|
|
||||||
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="_button tab"
|
|
||||||
:class="{ active: tab === 'tags' }"
|
|
||||||
@click="tab = 'tags'"
|
|
||||||
>
|
|
||||||
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -171,6 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
|
||||||
import { emojiCategories, instance } from "@/instance";
|
import { emojiCategories, instance } from "@/instance";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
|
import { FocusTrap } from "focus-trap-vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|
|
@ -20,9 +20,12 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
compiledFormula(): any {
|
compiledFormula(): any {
|
||||||
return katex.renderToString(this.formula, {
|
const katexString = katex.renderToString(this.formula, {
|
||||||
throwOnError: false,
|
throwOnError: false,
|
||||||
} as any);
|
} as any);
|
||||||
|
return this.block
|
||||||
|
? `<div style="text-align:center">${katexString}</div>`
|
||||||
|
: katexString;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
{
|
{
|
||||||
yellow: instance.isNotResponding,
|
yellow: instance.isNotResponding,
|
||||||
red: instance.isBlocked,
|
red: instance.isBlocked,
|
||||||
|
purple: instance.isSilenced,
|
||||||
gray: instance.isSuspended,
|
gray: instance.isSuspended,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
|
@ -23,13 +24,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as misskey from "calckey-js";
|
import * as calckey from "calckey-js";
|
||||||
import MkMiniChart from "@/components/MkMiniChart.vue";
|
import MkMiniChart from "@/components/MkMiniChart.vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: misskey.entities.Instance;
|
instance: calckey.entities.Instance;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let chartValues = $ref<number[] | null>(null);
|
let chartValues = $ref<number[] | null>(null);
|
||||||
|
@ -135,6 +136,21 @@ function getInstanceIcon(instance): string {
|
||||||
background-size: 16px 16px;
|
background-size: 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:global(.purple) {
|
||||||
|
--c: rgba(196, 0, 255, 0.15);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--c) 16.67%,
|
||||||
|
transparent 16.67%,
|
||||||
|
transparent 50%,
|
||||||
|
var(--c) 50%,
|
||||||
|
var(--c) 66.67%,
|
||||||
|
transparent 66.67%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.gray) {
|
&:global(.gray) {
|
||||||
--c: var(--bg);
|
--c: var(--bg);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
|
|
|
@ -139,7 +139,8 @@ function close() {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--accentedBg);
|
background: var(--accentedBg);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -138,6 +138,10 @@ watch(
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
box-sizing: border-box;
|
||||||
|
&:focus-visible {
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
> .gif {
|
> .gif {
|
||||||
background-color: var(--fg);
|
background-color: var(--fg);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="el" class="sfhdhdhr">
|
<div ref="el" class="sfhdhdhr" tabindex="-1">
|
||||||
<MkMenu
|
<MkMenu
|
||||||
ref="menu"
|
ref="menu"
|
||||||
:items="items"
|
:items="items"
|
||||||
|
@ -23,7 +23,6 @@ import {
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import MkMenu from "./MkMenu.vue";
|
import MkMenu from "./MkMenu.vue";
|
||||||
import { MenuItem } from "@/types/menu";
|
import { MenuItem } from "@/types/menu";
|
||||||
import * as os from "@/os";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
|
|
|
@ -1,191 +1,202 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<FocusTrap :active="false" ref="focusTrap">
|
||||||
<div
|
<div tabindex="-1">
|
||||||
ref="itemsEl"
|
<div
|
||||||
v-hotkey="keymap"
|
ref="itemsEl"
|
||||||
class="rrevdjwt _popup _shadow"
|
class="rrevdjwt _popup _shadow"
|
||||||
:class="{ center: align === 'center', asDrawer }"
|
:class="{ center: align === 'center', asDrawer }"
|
||||||
:style="{
|
:style="{
|
||||||
width: width && !asDrawer ? width + 'px' : '',
|
width: width && !asDrawer ? width + 'px' : '',
|
||||||
maxHeight: maxHeight ? maxHeight + 'px' : '',
|
maxHeight: maxHeight ? maxHeight + 'px' : '',
|
||||||
}"
|
}"
|
||||||
@contextmenu.self="(e) => e.preventDefault()"
|
@contextmenu.self="(e) => e.preventDefault()"
|
||||||
>
|
>
|
||||||
<template v-for="(item, i) in items2">
|
<template v-for="(item, i) in items2">
|
||||||
<div v-if="item === null" class="divider"></div>
|
<div v-if="item === null" class="divider"></div>
|
||||||
<span v-else-if="item.type === 'label'" class="label item">
|
<span v-else-if="item.type === 'label'" class="label item">
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
<span :style="item.textStyle || ''">{{
|
||||||
</span>
|
item.text
|
||||||
<span
|
}}</span>
|
||||||
v-else-if="item.type === 'pending'"
|
|
||||||
:tabindex="i"
|
|
||||||
class="pending item"
|
|
||||||
>
|
|
||||||
<span><MkEllipsis /></span>
|
|
||||||
</span>
|
|
||||||
<MkA
|
|
||||||
v-else-if="item.type === 'link'"
|
|
||||||
:to="item.to"
|
|
||||||
:tabindex="i"
|
|
||||||
class="_button item"
|
|
||||||
@click.passive="close(true)"
|
|
||||||
@mouseenter.passive="onItemMouseEnter(item)"
|
|
||||||
@mouseleave.passive="onItemMouseLeave(item)"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
v-if="item.icon"
|
|
||||||
class="ph-fw ph-lg"
|
|
||||||
:class="item.icon"
|
|
||||||
></i>
|
|
||||||
<span v-else-if="item.icons">
|
|
||||||
<i
|
|
||||||
v-for="icon in item.icons"
|
|
||||||
class="ph-fw ph-lg"
|
|
||||||
:class="icon"
|
|
||||||
></i>
|
|
||||||
</span>
|
</span>
|
||||||
<MkAvatar
|
<span
|
||||||
v-if="item.avatar"
|
v-else-if="item.type === 'pending'"
|
||||||
:user="item.avatar"
|
class="pending item"
|
||||||
class="avatar"
|
|
||||||
/>
|
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
|
||||||
<span v-if="item.indicate" class="indicator"
|
|
||||||
><i class="ph-circle ph-fill"></i
|
|
||||||
></span>
|
|
||||||
</MkA>
|
|
||||||
<a
|
|
||||||
v-else-if="item.type === 'a'"
|
|
||||||
:href="item.href"
|
|
||||||
:target="item.target"
|
|
||||||
:download="item.download"
|
|
||||||
:tabindex="i"
|
|
||||||
class="_button item"
|
|
||||||
@click="close(true)"
|
|
||||||
@mouseenter.passive="onItemMouseEnter(item)"
|
|
||||||
@mouseleave.passive="onItemMouseLeave(item)"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
v-if="item.icon"
|
|
||||||
class="ph-fw ph-lg"
|
|
||||||
:class="item.icon"
|
|
||||||
></i>
|
|
||||||
<span v-else-if="item.icons">
|
|
||||||
<i
|
|
||||||
v-for="icon in item.icons"
|
|
||||||
class="ph-fw ph-lg"
|
|
||||||
:class="icon"
|
|
||||||
></i>
|
|
||||||
</span>
|
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
|
||||||
<span v-if="item.indicate" class="indicator"
|
|
||||||
><i class="ph-circle ph-fill"></i
|
|
||||||
></span>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
v-else-if="item.type === 'user' && !items.hidden"
|
|
||||||
:tabindex="i"
|
|
||||||
class="_button item"
|
|
||||||
:class="{ active: item.active }"
|
|
||||||
:disabled="item.active"
|
|
||||||
@click="clicked(item.action, $event)"
|
|
||||||
@mouseenter.passive="onItemMouseEnter(item)"
|
|
||||||
@mouseleave.passive="onItemMouseLeave(item)"
|
|
||||||
>
|
|
||||||
<MkAvatar :user="item.user" class="avatar" /><MkUserName
|
|
||||||
:user="item.user"
|
|
||||||
/>
|
|
||||||
<span v-if="item.indicate" class="indicator"
|
|
||||||
><i class="ph-circle ph-fill"></i
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
v-else-if="item.type === 'switch'"
|
|
||||||
:tabindex="i"
|
|
||||||
class="item"
|
|
||||||
@mouseenter.passive="onItemMouseEnter(item)"
|
|
||||||
@mouseleave.passive="onItemMouseLeave(item)"
|
|
||||||
>
|
|
||||||
<FormSwitch
|
|
||||||
v-model="item.ref"
|
|
||||||
:disabled="item.disabled"
|
|
||||||
class="form-switch"
|
|
||||||
:style="item.textStyle || ''"
|
|
||||||
>{{ item.text }}</FormSwitch
|
|
||||||
>
|
>
|
||||||
|
<span><MkEllipsis /></span>
|
||||||
|
</span>
|
||||||
|
<MkA
|
||||||
|
v-else-if="item.type === 'link'"
|
||||||
|
:to="item.to"
|
||||||
|
class="_button item"
|
||||||
|
@click.passive="close(true)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="item.icon"
|
||||||
|
></i>
|
||||||
|
<span v-else-if="item.icons">
|
||||||
|
<i
|
||||||
|
v-for="icon in item.icons"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="icon"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
<MkAvatar
|
||||||
|
v-if="item.avatar"
|
||||||
|
:user="item.avatar"
|
||||||
|
class="avatar"
|
||||||
|
disableLink
|
||||||
|
/>
|
||||||
|
<span :style="item.textStyle || ''">{{
|
||||||
|
item.text
|
||||||
|
}}</span>
|
||||||
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
|
</MkA>
|
||||||
|
<a
|
||||||
|
v-else-if="item.type === 'a'"
|
||||||
|
:href="item.href"
|
||||||
|
:target="item.target"
|
||||||
|
:download="item.download"
|
||||||
|
class="_button item"
|
||||||
|
@click="close(true)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="item.icon"
|
||||||
|
></i>
|
||||||
|
<span v-else-if="item.icons">
|
||||||
|
<i
|
||||||
|
v-for="icon in item.icons"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="icon"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
<span :style="item.textStyle || ''">{{
|
||||||
|
item.text
|
||||||
|
}}</span>
|
||||||
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
v-else-if="item.type === 'user' && !items.hidden"
|
||||||
|
class="_button item"
|
||||||
|
:class="{ active: item.active }"
|
||||||
|
:disabled="item.active"
|
||||||
|
@click="clicked(item.action, $event)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<MkAvatar
|
||||||
|
:user="item.user"
|
||||||
|
class="avatar"
|
||||||
|
disableLink
|
||||||
|
/><MkUserName :user="item.user" />
|
||||||
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-else-if="item.type === 'switch'"
|
||||||
|
class="item"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<FormSwitch
|
||||||
|
v-model="item.ref"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
class="form-switch"
|
||||||
|
:style="item.textStyle || ''"
|
||||||
|
>{{ item.text }}</FormSwitch
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-else-if="item.type === 'parent'"
|
||||||
|
class="_button item parent"
|
||||||
|
:class="{ childShowing: childShowingItem === item }"
|
||||||
|
@mouseenter="showChildren(item, $event)"
|
||||||
|
@click="showChildren(item, $event)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="item.icon"
|
||||||
|
></i>
|
||||||
|
<span v-else-if="item.icons">
|
||||||
|
<i
|
||||||
|
v-for="icon in item.icons"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="icon"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
<span :style="item.textStyle || ''">{{
|
||||||
|
item.text
|
||||||
|
}}</span>
|
||||||
|
<span class="caret"
|
||||||
|
><i
|
||||||
|
class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"
|
||||||
|
></i
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="!item.hidden"
|
||||||
|
class="_button item"
|
||||||
|
:class="{ danger: item.danger, active: item.active }"
|
||||||
|
:disabled="item.active"
|
||||||
|
@click="clicked(item.action, $event)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="item.icon"
|
||||||
|
></i>
|
||||||
|
<span v-else-if="item.icons">
|
||||||
|
<i
|
||||||
|
v-for="icon in item.icons"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="icon"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
<MkAvatar
|
||||||
|
v-if="item.avatar"
|
||||||
|
:user="item.avatar"
|
||||||
|
class="avatar"
|
||||||
|
disableLink
|
||||||
|
/>
|
||||||
|
<span :style="item.textStyle || ''">{{
|
||||||
|
item.text
|
||||||
|
}}</span>
|
||||||
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<span v-if="items2.length === 0" class="none item">
|
||||||
|
<span>{{ i18n.ts.none }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
</div>
|
||||||
v-else-if="item.type === 'parent'"
|
<div v-if="childMenu" class="child">
|
||||||
:tabindex="i"
|
<XChild
|
||||||
class="_button item parent"
|
ref="child"
|
||||||
:class="{ childShowing: childShowingItem === item }"
|
:items="childMenu"
|
||||||
@mouseenter="showChildren(item, $event)"
|
:target-element="childTarget"
|
||||||
>
|
:root-element="itemsEl"
|
||||||
<i
|
showing
|
||||||
v-if="item.icon"
|
@actioned="childActioned"
|
||||||
class="ph-fw ph-lg"
|
/>
|
||||||
:class="item.icon"
|
</div>
|
||||||
></i>
|
|
||||||
<span v-else-if="item.icons">
|
|
||||||
<i
|
|
||||||
v-for="icon in item.icons"
|
|
||||||
class="ph-fw ph-lg"
|
|
||||||
:class="icon"
|
|
||||||
></i>
|
|
||||||
</span>
|
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
|
||||||
<span class="caret"
|
|
||||||
><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-else-if="!item.hidden"
|
|
||||||
:tabindex="i"
|
|
||||||
class="_button item"
|
|
||||||
:class="{ danger: item.danger, active: item.active }"
|
|
||||||
:disabled="item.active"
|
|
||||||
@click="clicked(item.action, $event)"
|
|
||||||
@mouseenter.passive="onItemMouseEnter(item)"
|
|
||||||
@mouseleave.passive="onItemMouseLeave(item)"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
v-if="item.icon"
|
|
||||||
class="ph-fw ph-lg"
|
|
||||||
:class="item.icon"
|
|
||||||
></i>
|
|
||||||
<span v-else-if="item.icons">
|
|
||||||
<i
|
|
||||||
v-for="icon in item.icons"
|
|
||||||
class="ph-fw ph-lg"
|
|
||||||
:class="icon"
|
|
||||||
></i>
|
|
||||||
</span>
|
|
||||||
<MkAvatar
|
|
||||||
v-if="item.avatar"
|
|
||||||
:user="item.avatar"
|
|
||||||
class="avatar"
|
|
||||||
/>
|
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
|
||||||
<span v-if="item.indicate" class="indicator"
|
|
||||||
><i class="ph-circle ph-fill"></i
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<span v-if="items2.length === 0" class="none item">
|
|
||||||
<span>{{ i18n.ts.none }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="childMenu" class="child">
|
</FocusTrap>
|
||||||
<XChild
|
|
||||||
ref="child"
|
|
||||||
:items="childMenu"
|
|
||||||
:target-element="childTarget"
|
|
||||||
:root-element="itemsEl"
|
|
||||||
showing
|
|
||||||
@actioned="childActioned"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -206,8 +217,10 @@ import FormSwitch from "@/components/form/switch.vue";
|
||||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
|
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
import { FocusTrap } from "focus-trap-vue";
|
||||||
|
|
||||||
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
|
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
|
||||||
|
const focusTrap = ref();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
|
@ -228,12 +241,6 @@ let items2: InnerMenuItem[] = $ref([]);
|
||||||
|
|
||||||
let child = $ref<InstanceType<typeof XChild>>();
|
let child = $ref<InstanceType<typeof XChild>>();
|
||||||
|
|
||||||
let keymap = computed(() => ({
|
|
||||||
"up|k|shift+tab": focusUp,
|
|
||||||
"down|j|tab": focusDown,
|
|
||||||
esc: close,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let childShowingItem = $ref<MenuItem | null>();
|
let childShowingItem = $ref<MenuItem | null>();
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
@ -324,6 +331,8 @@ function focusDown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
focusTrap.value.activate();
|
||||||
|
|
||||||
if (props.viaKeyboard) {
|
if (props.viaKeyboard) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
focusNext(itemsEl.children[0], true, false);
|
focusNext(itemsEl.children[0], true, false);
|
||||||
|
@ -364,8 +373,7 @@ onBeforeUnmount(() => {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
overflow: hidden;
|
outline: none;
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -389,7 +397,8 @@ onBeforeUnmount(() => {
|
||||||
transform: translateY(0em);
|
transform: translateY(0em);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled):hover {
|
&:not(:disabled):hover,
|
||||||
|
&:focus-visible {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
@ -397,6 +406,9 @@ onBeforeUnmount(() => {
|
||||||
background: var(--accentedBg);
|
background: var(--accentedBg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:focus-visible:before {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
color: #eb6f92;
|
color: #eb6f92;
|
||||||
|
|
|
@ -14,54 +14,62 @@
|
||||||
:duration="transitionDuration"
|
:duration="transitionDuration"
|
||||||
appear
|
appear
|
||||||
@after-leave="emit('closed')"
|
@after-leave="emit('closed')"
|
||||||
|
@keyup.esc="emit('click')"
|
||||||
@enter="emit('opening')"
|
@enter="emit('opening')"
|
||||||
@after-enter="onOpened"
|
@after-enter="onOpened"
|
||||||
>
|
>
|
||||||
<div
|
<FocusTrap v-model:active="isActive">
|
||||||
v-show="manualShowing != null ? manualShowing : showing"
|
|
||||||
v-hotkey.global="keymap"
|
|
||||||
:class="[
|
|
||||||
$style.root,
|
|
||||||
{
|
|
||||||
[$style.drawer]: type === 'drawer',
|
|
||||||
[$style.dialog]: type === 'dialog' || type === 'dialog:top',
|
|
||||||
[$style.popup]: type === 'popup',
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
:style="{
|
|
||||||
zIndex,
|
|
||||||
pointerEvents: (manualShowing != null ? manualShowing : showing)
|
|
||||||
? 'auto'
|
|
||||||
: 'none',
|
|
||||||
'--transformOrigin': transformOrigin,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="_modalBg data-cy-bg"
|
v-show="manualShowing != null ? manualShowing : showing"
|
||||||
|
v-hotkey.global="keymap"
|
||||||
:class="[
|
:class="[
|
||||||
$style.bg,
|
$style.root,
|
||||||
{
|
{
|
||||||
[$style.bgTransparent]: isEnableBgTransparent,
|
[$style.drawer]: type === 'drawer',
|
||||||
'data-cy-transparent': isEnableBgTransparent,
|
[$style.dialog]:
|
||||||
|
type === 'dialog' || type === 'dialog:top',
|
||||||
|
[$style.popup]: type === 'popup',
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
:style="{ zIndex }"
|
:style="{
|
||||||
@click="onBgClick"
|
zIndex,
|
||||||
@mousedown="onBgClick"
|
pointerEvents: (
|
||||||
@contextmenu.prevent.stop="() => {}"
|
manualShowing != null ? manualShowing : showing
|
||||||
></div>
|
)
|
||||||
<div
|
? 'auto'
|
||||||
ref="content"
|
: 'none',
|
||||||
:class="[
|
'--transformOrigin': transformOrigin,
|
||||||
$style.content,
|
}"
|
||||||
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
|
tabindex="-1"
|
||||||
]"
|
v-focus
|
||||||
:style="{ zIndex }"
|
|
||||||
@click.self="onBgClick"
|
|
||||||
>
|
>
|
||||||
<slot :max-height="maxHeight" :type="type"></slot>
|
<div
|
||||||
|
class="_modalBg data-cy-bg"
|
||||||
|
:class="[
|
||||||
|
$style.bg,
|
||||||
|
{
|
||||||
|
[$style.bgTransparent]: isEnableBgTransparent,
|
||||||
|
'data-cy-transparent': isEnableBgTransparent,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:style="{ zIndex }"
|
||||||
|
@click="onBgClick"
|
||||||
|
@mousedown="onBgClick"
|
||||||
|
@contextmenu.prevent.stop="() => {}"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
ref="content"
|
||||||
|
:class="[
|
||||||
|
$style.content,
|
||||||
|
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
|
||||||
|
]"
|
||||||
|
:style="{ zIndex }"
|
||||||
|
@click.self="onBgClick"
|
||||||
|
>
|
||||||
|
<slot :max-height="maxHeight" :type="type"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FocusTrap>
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -71,6 +79,7 @@ import * as os from "@/os";
|
||||||
import { isTouchUsing } from "@/scripts/touch";
|
import { isTouchUsing } from "@/scripts/touch";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
import { deviceKind } from "@/scripts/device-kind";
|
import { deviceKind } from "@/scripts/device-kind";
|
||||||
|
import { FocusTrap } from "focus-trap-vue";
|
||||||
|
|
||||||
function getFixedContainer(el: Element | null): Element | null {
|
function getFixedContainer(el: Element | null): Element | null {
|
||||||
if (el == null || el.tagName === "BODY") return null;
|
if (el == null || el.tagName === "BODY") return null;
|
||||||
|
@ -166,6 +175,7 @@ let transitionDuration = $computed(() =>
|
||||||
|
|
||||||
let contentClicking = false;
|
let contentClicking = false;
|
||||||
|
|
||||||
|
const focusedElement = document.activeElement;
|
||||||
function close(opts: { useSendAnimation?: boolean } = {}) {
|
function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||||
if (opts.useSendAnimation) {
|
if (opts.useSendAnimation) {
|
||||||
useSendAnime = true;
|
useSendAnime = true;
|
||||||
|
@ -175,10 +185,12 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||||
if (props.src) props.src.style.pointerEvents = "auto";
|
if (props.src) props.src.style.pointerEvents = "auto";
|
||||||
showing = false;
|
showing = false;
|
||||||
emit("close");
|
emit("close");
|
||||||
|
focusedElement.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBgClick() {
|
function onBgClick() {
|
||||||
if (contentClicking) return;
|
if (contentClicking) return;
|
||||||
|
focusedElement.focus();
|
||||||
emit("click");
|
emit("click");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,6 +493,7 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
outline: none;
|
||||||
&.dialog {
|
&.dialog {
|
||||||
> .content {
|
> .content {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
contain: content;
|
contain: content;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
--root-margin: 24px;
|
--root-margin: 24px;
|
||||||
|
|
||||||
|
|
|
@ -3,59 +3,64 @@
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:prefer-type="'dialog'"
|
:prefer-type="'dialog'"
|
||||||
@click="onBgClick"
|
@click="onBgClick"
|
||||||
|
@keyup.esc="$emit('close')"
|
||||||
@closed="$emit('closed')"
|
@closed="$emit('closed')"
|
||||||
>
|
>
|
||||||
<div
|
<FocusTrap v-model:active="isActive">
|
||||||
ref="rootEl"
|
<div
|
||||||
class="ebkgoccj"
|
ref="rootEl"
|
||||||
:style="{
|
class="ebkgoccj"
|
||||||
width: `${width}px`,
|
:style="{
|
||||||
height: scroll
|
width: `${width}px`,
|
||||||
? height
|
height: scroll
|
||||||
? `${height}px`
|
? height
|
||||||
: null
|
? `${height}px`
|
||||||
: height
|
: null
|
||||||
? `min(${height}px, 100%)`
|
: height
|
||||||
: '100%',
|
? `min(${height}px, 100%)`
|
||||||
}"
|
: '100%',
|
||||||
@keydown="onKeydown"
|
}"
|
||||||
>
|
@keydown="onKeydown"
|
||||||
<div ref="headerEl" class="header">
|
tabindex="-1"
|
||||||
<button
|
>
|
||||||
v-if="withOkButton"
|
<div ref="headerEl" class="header">
|
||||||
class="_button"
|
<button
|
||||||
@click="$emit('close')"
|
v-if="withOkButton"
|
||||||
>
|
class="_button"
|
||||||
<i class="ph-x ph-bold ph-lg"></i>
|
@click="$emit('close')"
|
||||||
</button>
|
>
|
||||||
<span class="title">
|
<i class="ph-x ph-bold ph-lg"></i>
|
||||||
<slot name="header"></slot>
|
</button>
|
||||||
</span>
|
<span class="title">
|
||||||
<button
|
<slot name="header"></slot>
|
||||||
v-if="!withOkButton"
|
</span>
|
||||||
class="_button"
|
<button
|
||||||
@click="$emit('close')"
|
v-if="!withOkButton"
|
||||||
>
|
class="_button"
|
||||||
<i class="ph-x ph-bold ph-lg"></i>
|
@click="$emit('close')"
|
||||||
</button>
|
>
|
||||||
<button
|
<i class="ph-x ph-bold ph-lg"></i>
|
||||||
v-if="withOkButton"
|
</button>
|
||||||
class="_button"
|
<button
|
||||||
:disabled="okButtonDisabled"
|
v-if="withOkButton"
|
||||||
@click="$emit('ok')"
|
class="_button"
|
||||||
>
|
:disabled="okButtonDisabled"
|
||||||
<i class="ph-check ph-bold ph-lg"></i>
|
@click="$emit('ok')"
|
||||||
</button>
|
>
|
||||||
|
<i class="ph-check ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<slot :width="bodyWidth" :height="bodyHeight"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
</FocusTrap>
|
||||||
<slot :width="bodyWidth" :height="bodyHeight"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted } from "vue";
|
import { onMounted, onUnmounted } from "vue";
|
||||||
|
import { FocusTrap } from "focus-trap-vue";
|
||||||
import MkModal from "./MkModal.vue";
|
import MkModal from "./MkModal.vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
:detailedView="detailedView"
|
:detailedView="detailedView"
|
||||||
:parentId="appearNote.parentId"
|
:parentId="appearNote.parentId"
|
||||||
@push="(e) => router.push(notePage(e))"
|
@push="(e) => router.push(notePage(e))"
|
||||||
|
@focusfooter="footerEl.focus()"
|
||||||
></MkSubNoteContent>
|
></MkSubNoteContent>
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini />
|
<MkLoading v-if="translating" mini />
|
||||||
|
@ -117,7 +118,7 @@
|
||||||
<MkTime :time="appearNote.createdAt" mode="absolute" />
|
<MkTime :time="appearNote.createdAt" mode="absolute" />
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<footer ref="el" class="footer" @click.stop>
|
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
|
||||||
<XReactionsViewer
|
<XReactionsViewer
|
||||||
v-if="enableEmojiReactions"
|
v-if="enableEmojiReactions"
|
||||||
ref="reactionsViewer"
|
ref="reactionsViewer"
|
||||||
|
@ -278,6 +279,7 @@ const isRenote =
|
||||||
note.poll == null;
|
note.poll == null;
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement>();
|
||||||
|
const footerEl = ref<HTMLElement>();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = ref<HTMLElement>();
|
||||||
const starButton = ref<InstanceType<typeof XStarButton>>();
|
const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||||
|
@ -298,8 +300,8 @@ const keymap = {
|
||||||
r: () => reply(true),
|
r: () => reply(true),
|
||||||
"e|a|plus": () => react(true),
|
"e|a|plus": () => react(true),
|
||||||
q: () => renoteButton.value.renote(true),
|
q: () => renoteButton.value.renote(true),
|
||||||
"up|k|shift+tab": focusBefore,
|
"up|k": focusBefore,
|
||||||
"down|j|tab": focusAfter,
|
"down|j": focusAfter,
|
||||||
esc: blur,
|
esc: blur,
|
||||||
"m|o": () => menu(true),
|
"m|o": () => menu(true),
|
||||||
s: () => showContent.value !== showContent.value,
|
s: () => showContent.value !== showContent.value,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-size="{ min: [350, 500] }" class="fefdfafb">
|
<div v-size="{ min: [350, 500] }" class="fefdfafb">
|
||||||
<MkAvatar class="avatar" :user="$i" />
|
<MkAvatar class="avatar" :user="$i" disableLink />
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<MkUserName :user="$i" />
|
<MkUserName :user="$i" />
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
:note="note"
|
:note="note"
|
||||||
:parentId="appearNote.parentId"
|
:parentId="appearNote.parentId"
|
||||||
:conversation="conversation"
|
:conversation="conversation"
|
||||||
|
@focusfooter="footerEl.focus()"
|
||||||
/>
|
/>
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini />
|
<MkLoading v-if="translating" mini />
|
||||||
|
@ -46,7 +47,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer" @click.stop>
|
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
|
||||||
<XReactionsViewer
|
<XReactionsViewer
|
||||||
v-if="enableEmojiReactions"
|
v-if="enableEmojiReactions"
|
||||||
ref="reactionsViewer"
|
ref="reactionsViewer"
|
||||||
|
@ -212,6 +213,7 @@ const isRenote =
|
||||||
note.poll == null;
|
note.poll == null;
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement>();
|
||||||
|
const footerEl = ref<HTMLElement>();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = ref<HTMLElement>();
|
||||||
const starButton = ref<InstanceType<typeof XStarButton>>();
|
const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
:transparent-bg="true"
|
:transparent-bg="true"
|
||||||
@click="modal.close()"
|
@click="modal.close()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
|
tabindex="-1"
|
||||||
|
v-focus
|
||||||
>
|
>
|
||||||
<MkMenu
|
<MkMenu
|
||||||
:items="items"
|
:items="items"
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
:class="{ active: showPreview }"
|
:class="{ active: showPreview }"
|
||||||
@click="showPreview = !showPreview"
|
@click="showPreview = !showPreview"
|
||||||
>
|
>
|
||||||
<i class="ph-file-code ph-bold ph-lg"></i>
|
<i class="ph-binoculars ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="submit _buttonGradate"
|
class="submit _buttonGradate"
|
||||||
|
@ -462,15 +462,21 @@ if (
|
||||||
props.reply &&
|
props.reply &&
|
||||||
["home", "followers", "specified"].includes(props.reply.visibility)
|
["home", "followers", "specified"].includes(props.reply.visibility)
|
||||||
) {
|
) {
|
||||||
visibility = props.reply.visibility;
|
if (props.reply.visibility === 'home' && visibility === 'followers') {
|
||||||
if (props.reply.visibility === "specified") {
|
visibility = 'followers';
|
||||||
os.api("users/show", {
|
} else if (['home', 'followers'].includes(props.reply.visibility) && visibility === 'specified') {
|
||||||
userIds: props.reply.visibleUserIds.filter(
|
visibility = 'specified';
|
||||||
(uid) => uid !== $i.id && uid !== props.reply.userId
|
} else {
|
||||||
),
|
visibility = props.reply.visibility;
|
||||||
}).then((users) => {
|
}
|
||||||
users.forEach(pushVisibleUser);
|
if (visibility === 'specified') {
|
||||||
});
|
if (props.reply.visibleUserIds) {
|
||||||
|
os.api('users/show', {
|
||||||
|
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
|
||||||
|
}).then(users => {
|
||||||
|
users.forEach(pushVisibleUser);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (props.reply.userId !== $i.id) {
|
if (props.reply.userId !== $i.id) {
|
||||||
os.api("users/show", { userId: props.reply.userId }).then(
|
os.api("users/show", { userId: props.reply.userId }).then(
|
||||||
|
|
|
@ -154,22 +154,22 @@ export default defineComponent({
|
||||||
? i18n.ts.unmarkAsSensitive
|
? i18n.ts.unmarkAsSensitive
|
||||||
: i18n.ts.markAsSensitive,
|
: i18n.ts.markAsSensitive,
|
||||||
icon: file.isSensitive
|
icon: file.isSensitive
|
||||||
? "ph-eye-slash ph-bold ph-lg"
|
? "ph-eye ph-bold ph-lg"
|
||||||
: "ph-eye ph-bold ph-lg",
|
: "ph-eye-slash ph-bold ph-lg",
|
||||||
action: () => {
|
action: () => {
|
||||||
this.toggleSensitive(file);
|
this.toggleSensitive(file);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: i18n.ts.describeFile,
|
text: i18n.ts.describeFile,
|
||||||
icon: "ph-cursor-text ph-bold ph-lg",
|
icon: "ph-subtitles ph-bold ph-lg",
|
||||||
action: () => {
|
action: () => {
|
||||||
this.describe(file);
|
this.describe(file);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: i18n.ts.attachCancel,
|
text: i18n.ts.attachCancel,
|
||||||
icon: "ph-circle-wavy-warning ph-bold ph-lg",
|
icon: "ph-x ph-bold ph-lg",
|
||||||
action: () => {
|
action: () => {
|
||||||
this.detachMedia(file.id);
|
this.detachMedia(file.id);
|
||||||
},
|
},
|
||||||
|
@ -198,7 +198,6 @@ export default defineComponent({
|
||||||
height: 64px;
|
height: 64px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
|
||||||
&:hover > .remove {
|
&:hover > .remove {
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
v-if="modelValue"
|
|
||||||
class="fade _button"
|
|
||||||
@click.stop="toggle"
|
|
||||||
>
|
|
||||||
<span>{{ i18n.ts.showMore }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="!modelValue"
|
|
||||||
class="showLess _button"
|
|
||||||
@click.stop="toggle"
|
|
||||||
>
|
|
||||||
<span>{{ i18n.ts.showLess }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: "update:modelValue", v: boolean): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
emit("update:modelValue", !props.modelValue);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.fade {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
> span {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--panel);
|
|
||||||
padding: 0.4em 1em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
border-radius: 999px;
|
|
||||||
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
> span {
|
|
||||||
background: var(--panelHighlight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.showLess {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1em;
|
|
||||||
position: sticky;
|
|
||||||
bottom: var(--stickyBottom);
|
|
||||||
|
|
||||||
> span {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--panel);
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
border-radius: 999px;
|
|
||||||
box-shadow: 0 0 7px 7px var(--bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -35,7 +35,20 @@
|
||||||
class="content"
|
class="content"
|
||||||
:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
|
:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
|
||||||
>
|
>
|
||||||
<div class="body">
|
<XCwButton
|
||||||
|
ref="cwButton"
|
||||||
|
v-if="note.cw && !showContent"
|
||||||
|
v-model="showContent"
|
||||||
|
:note="note"
|
||||||
|
v-on:keydown="focusFooter"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="body"
|
||||||
|
v-bind="{
|
||||||
|
'aria-label': !showContent ? '' : null,
|
||||||
|
tabindex: !showContent ? '-1' : null,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<span v-if="note.deletedAt" style="opacity: 0.5"
|
<span v-if="note.deletedAt" style="opacity: 0.5"
|
||||||
>({{ i18n.ts.deleted }})</span
|
>({{ i18n.ts.deleted }})</span
|
||||||
>
|
>
|
||||||
|
@ -96,22 +109,33 @@
|
||||||
<XNoteSimple :note="note.renote" />
|
<XNoteSimple :note="note.renote" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="note.cw && !showContent"
|
||||||
|
tabindex="0"
|
||||||
|
v-on:focus="cwButton?.focus()"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
|
<XShowMoreButton
|
||||||
<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
|
v-if="isLong"
|
||||||
|
v-model="collapsed"
|
||||||
|
></XShowMoreButton>
|
||||||
|
<XCwButton
|
||||||
|
v-if="note.cw && showContent"
|
||||||
|
v-model="showContent"
|
||||||
|
:note="note"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {} from "vue";
|
import { ref } from "vue";
|
||||||
import * as misskey from "calckey-js";
|
import * as misskey from "calckey-js";
|
||||||
import * as mfm from "mfm-js";
|
import * as mfm from "mfm-js";
|
||||||
import XNoteSimple from "@/components/MkNoteSimple.vue";
|
import XNoteSimple from "@/components/MkNoteSimple.vue";
|
||||||
import XMediaList from "@/components/MkMediaList.vue";
|
import XMediaList from "@/components/MkMediaList.vue";
|
||||||
import XPoll from "@/components/MkPoll.vue";
|
import XPoll from "@/components/MkPoll.vue";
|
||||||
import MkUrlPreview from "@/components/MkUrlPreview.vue";
|
import MkUrlPreview from "@/components/MkUrlPreview.vue";
|
||||||
import XShowMoreButton from "./MkShowMoreButton.vue";
|
|
||||||
import XCwButton from "@/components/MkCwButton.vue";
|
import XCwButton from "@/components/MkCwButton.vue";
|
||||||
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
|
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
@ -126,20 +150,27 @@ const props = defineProps<{
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "push", v): void;
|
(ev: "push", v): void;
|
||||||
|
(ev: "focusfooter"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const cwButton = ref<HTMLElement>();
|
||||||
const isLong =
|
const isLong =
|
||||||
!props.detailedView &&
|
!props.detailedView &&
|
||||||
props.note.cw == null &&
|
props.note.cw == null &&
|
||||||
props.note.text != null &&
|
props.note.text != null &&
|
||||||
(props.note.text.split("\n").length > 9 || props.note.text.length > 500);
|
(props.note.text.split("\n").length > 9 || props.note.text.length > 500);
|
||||||
const collapsed = $ref(props.note.cw == null && isLong);
|
const collapsed = $ref(props.note.cw == null && isLong);
|
||||||
|
|
||||||
const urls = props.note.text
|
const urls = props.note.text
|
||||||
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
|
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let showContent = $ref(false);
|
let showContent = $ref(false);
|
||||||
|
|
||||||
|
function focusFooter(ev) {
|
||||||
|
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
|
||||||
|
emit("focusfooter");
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -231,6 +262,9 @@ let showContent = $ref(false);
|
||||||
margin-top: -50px;
|
margin-top: -50px;
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
}
|
}
|
||||||
&.collapsed > .body {
|
&.collapsed > .body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -253,6 +287,43 @@ let showContent = $ref(false);
|
||||||
top: 40px;
|
top: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.fade) {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
> span {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 0.4em 1em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
> span {
|
||||||
|
background: var(--panelHighlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.showLess) {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1em;
|
||||||
|
position: sticky;
|
||||||
|
bottom: var(--stickyBottom);
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 0 7px 7px var(--bg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
v-if="item.type === 'a'"
|
v-if="item.type === 'a'"
|
||||||
:href="item.href"
|
:href="item.href"
|
||||||
:target="item.target"
|
:target="item.target"
|
||||||
:tabindex="i"
|
|
||||||
class="_button item"
|
class="_button item"
|
||||||
:class="{ danger: item.danger, active: item.active }"
|
:class="{ danger: item.danger, active: item.active }"
|
||||||
>
|
>
|
||||||
|
@ -22,7 +21,6 @@
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-else-if="item.type === 'button'"
|
v-else-if="item.type === 'button'"
|
||||||
:tabindex="i"
|
|
||||||
class="_button item"
|
class="_button item"
|
||||||
:class="{ danger: item.danger, active: item.active }"
|
:class="{ danger: item.danger, active: item.active }"
|
||||||
:disabled="item.active"
|
:disabled="item.active"
|
||||||
|
@ -38,7 +36,6 @@
|
||||||
<MkA
|
<MkA
|
||||||
v-else
|
v-else
|
||||||
:to="item.to"
|
:to="item.to"
|
||||||
:tabindex="i"
|
|
||||||
class="_button item"
|
class="_button item"
|
||||||
:class="{ danger: item.danger, active: item.active }"
|
:class="{ danger: item.danger, active: item.active }"
|
||||||
>
|
>
|
||||||
|
@ -99,7 +96,8 @@ export default defineComponent({
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: var(--panelHighlight);
|
background: var(--panelHighlight);
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,10 @@
|
||||||
/></MkA>
|
/></MkA>
|
||||||
<p class="username"><MkAcct :user="user" /></p>
|
<p class="username"><MkAcct :user="user" /></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="description" :class="{ collapsed: isLong && collapsed }">
|
<div
|
||||||
|
class="description"
|
||||||
|
:class="{ collapsed: isLong && collapsed }"
|
||||||
|
>
|
||||||
<Mfm
|
<Mfm
|
||||||
v-if="user.description"
|
v-if="user.description"
|
||||||
:text="user.description"
|
:text="user.description"
|
||||||
|
@ -55,7 +58,20 @@
|
||||||
:custom-emojis="user.emojis"
|
:custom-emojis="user.emojis"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
|
<button
|
||||||
|
v-if="isLong && collapsed"
|
||||||
|
class="fade _button"
|
||||||
|
@click.stop="collapsed = false"
|
||||||
|
>
|
||||||
|
<span>{{ i18n.ts.showMore }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isLong && !collapsed"
|
||||||
|
class="showLess _button"
|
||||||
|
@click.stop="collapsed = true"
|
||||||
|
>
|
||||||
|
<span>{{ i18n.ts.showLess }}</span>
|
||||||
|
</button>
|
||||||
<div v-if="user.fields.length > 0" class="fields">
|
<div v-if="user.fields.length > 0" class="fields">
|
||||||
<dl
|
<dl
|
||||||
v-for="(field, i) in user.fields"
|
v-for="(field, i) in user.fields"
|
||||||
|
@ -115,7 +131,6 @@ import * as Acct from "calckey-js/built/acct";
|
||||||
import type * as misskey from "calckey-js";
|
import type * as misskey from "calckey-js";
|
||||||
import MkFollowButton from "@/components/MkFollowButton.vue";
|
import MkFollowButton from "@/components/MkFollowButton.vue";
|
||||||
import { userPage } from "@/filters/user";
|
import { userPage } from "@/filters/user";
|
||||||
import XShowMoreButton from "./MkShowMoreButton.vue";
|
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { $i } from "@/account";
|
import { $i } from "@/account";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
@ -137,14 +152,15 @@ let user = $ref<misskey.entities.UserDetailed | null>(null);
|
||||||
let top = $ref(0);
|
let top = $ref(0);
|
||||||
let left = $ref(0);
|
let left = $ref(0);
|
||||||
|
|
||||||
|
|
||||||
let isLong = $ref(false);
|
let isLong = $ref(false);
|
||||||
let collapsed = $ref(!isLong);
|
let collapsed = $ref(!isLong);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (typeof props.q === "object") {
|
if (typeof props.q === "object") {
|
||||||
user = props.q;
|
user = props.q;
|
||||||
isLong = (user.description.split("\n").length > 9 || user.description.length > 400);
|
isLong =
|
||||||
|
user.description.split("\n").length > 9 ||
|
||||||
|
user.description.length > 400;
|
||||||
} else {
|
} else {
|
||||||
const query = props.q.startsWith("@")
|
const query = props.q.startsWith("@")
|
||||||
? Acct.parse(props.q.substr(1))
|
? Acct.parse(props.q.substr(1))
|
||||||
|
@ -153,11 +169,12 @@ onMounted(() => {
|
||||||
os.api("users/show", query).then((res) => {
|
os.api("users/show", query).then((res) => {
|
||||||
if (!props.showing) return;
|
if (!props.showing) return;
|
||||||
user = res;
|
user = res;
|
||||||
isLong = (user.description.split("\n").length > 9 || user.description.length > 400);
|
isLong =
|
||||||
|
user.description.split("\n").length > 9 ||
|
||||||
|
user.description.length > 400;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const rect = props.source.getBoundingClientRect();
|
const rect = props.source.getBoundingClientRect();
|
||||||
const x =
|
const x =
|
||||||
rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.pageXOffset;
|
rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.pageXOffset;
|
||||||
|
@ -301,7 +318,7 @@ onMounted(() => {
|
||||||
|
|
||||||
> .fields {
|
> .fields {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
font-size: .8em;
|
font-size: 0.8em;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
|
||||||
> .field {
|
> .field {
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
:user="user"
|
:user="user"
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
|
disableLink
|
||||||
/>
|
/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkUserName :user="user" class="name" />
|
<MkUserName :user="user" class="name" />
|
||||||
|
@ -73,6 +74,7 @@
|
||||||
:user="user"
|
:user="user"
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
|
disableLink
|
||||||
/>
|
/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkUserName :user="user" class="name" />
|
<MkUserName :user="user" class="name" />
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
>
|
>
|
||||||
<div class="beaffaef">
|
<div class="beaffaef">
|
||||||
<div v-for="u in users" :key="u.id" class="user">
|
<div v-for="u in users" :key="u.id" class="user">
|
||||||
<MkAvatar class="avatar" :user="u" />
|
<MkAvatar class="avatar" :user="u" disableLink />
|
||||||
<MkUserName class="name" :user="u" :nowrap="true" />
|
<MkUserName class="name" :user="u" :nowrap="true" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="users.length < count" class="omitted">
|
<div v-if="users.length < count" class="omitted">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="vjoppmmu">
|
<div class="vjoppmmu">
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<header>
|
<header tabindex="-1" v-focus>
|
||||||
<MkSelect
|
<MkSelect
|
||||||
v-model="widgetAdderSelected"
|
v-model="widgetAdderSelected"
|
||||||
style="margin-bottom: var(--margin)"
|
style="margin-bottom: var(--margin)"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dwzlatin" :class="{ opened }">
|
<div class="dwzlatin" :class="{ opened }">
|
||||||
<div class="header _button" @click="toggle">
|
<button class="header _button" @click="toggle">
|
||||||
<span class="icon"><slot name="icon"></slot></span>
|
<span class="icon"><slot name="icon"></slot></span>
|
||||||
<span class="text"><slot name="label"></slot></span>
|
<span class="text"><slot name="label"></slot></span>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
|
<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
|
||||||
<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
|
<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
<KeepAlive>
|
<KeepAlive>
|
||||||
<div v-if="openedAtLeastOnce" v-show="opened" class="body">
|
<div v-if="openedAtLeastOnce" v-show="opened" class="body">
|
||||||
<MkSpacer :margin-min="14" :margin-max="22">
|
<MkSpacer :margin-min="14" :margin-max="22">
|
||||||
|
|
|
@ -66,6 +66,9 @@ function toggle(): void {
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--inputBorderHover) !important;
|
border-color: var(--inputBorderHover) !important;
|
||||||
}
|
}
|
||||||
|
&:focus-within {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
background-color: var(--accentedBg) !important;
|
background-color: var(--accentedBg) !important;
|
||||||
|
|
|
@ -99,6 +99,9 @@ const toggle = () => {
|
||||||
border-color: var(--inputBorderHover) !important;
|
border-color: var(--inputBorderHover) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:focus-within > .button {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
> .label {
|
> .label {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:user="$i"
|
:user="$i"
|
||||||
:disable-preview="true"
|
:disable-preview="true"
|
||||||
|
disableLink
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="metadata">
|
<template v-if="metadata">
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
:user="metadata.avatar"
|
:user="metadata.avatar"
|
||||||
:disable-preview="true"
|
:disable-preview="true"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
|
disableLink
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
v-else-if="metadata.icon && !narrow"
|
v-else-if="metadata.icon && !narrow"
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
:is="currentPageComponent"
|
:is="currentPageComponent"
|
||||||
:key="key"
|
:key="key"
|
||||||
v-bind="Object.fromEntries(currentPageProps)"
|
v-bind="Object.fromEntries(currentPageProps)"
|
||||||
|
tabindex="-1"
|
||||||
|
v-focus
|
||||||
|
style="outline: none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
|
|
3
packages/client/src/directives/focus.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
mounted: (el) => el.focus()
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import anim from "./anim";
|
||||||
import clickAnime from "./click-anime";
|
import clickAnime from "./click-anime";
|
||||||
import panel from "./panel";
|
import panel from "./panel";
|
||||||
import adaptiveBorder from "./adaptive-border";
|
import adaptiveBorder from "./adaptive-border";
|
||||||
|
import focus from "./focus";
|
||||||
|
|
||||||
export default function (app: App) {
|
export default function (app: App) {
|
||||||
app.directive("userPreview", userPreview);
|
app.directive("userPreview", userPreview);
|
||||||
|
@ -25,4 +26,5 @@ export default function (app: App) {
|
||||||
app.directive("click-anime", clickAnime);
|
app.directive("click-anime", clickAnime);
|
||||||
app.directive("panel", panel);
|
app.directive("panel", panel);
|
||||||
app.directive("adaptive-border", adaptiveBorder);
|
app.directive("adaptive-border", adaptiveBorder);
|
||||||
|
app.directive("focus", focus);
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,23 +76,32 @@ export default {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function showTooltip() {
|
||||||
|
window.clearTimeout(self.showTimer);
|
||||||
|
window.clearTimeout(self.hideTimer);
|
||||||
|
self.showTimer = window.setTimeout(self.show, delay);
|
||||||
|
}
|
||||||
|
function hideTooltip() {
|
||||||
|
window.clearTimeout(self.showTimer);
|
||||||
|
window.clearTimeout(self.hideTimer);
|
||||||
|
self.hideTimer = window.setTimeout(self.close, delay);
|
||||||
|
}
|
||||||
|
|
||||||
el.addEventListener(
|
el.addEventListener(
|
||||||
start,
|
start, showTooltip,
|
||||||
() => {
|
{ passive: true },
|
||||||
window.clearTimeout(self.showTimer);
|
);
|
||||||
window.clearTimeout(self.hideTimer);
|
el.addEventListener(
|
||||||
self.showTimer = window.setTimeout(self.show, delay);
|
"focusin", showTooltip,
|
||||||
},
|
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
el.addEventListener(
|
el.addEventListener(
|
||||||
end,
|
end, hideTooltip,
|
||||||
() => {
|
{ passive: true },
|
||||||
window.clearTimeout(self.showTimer);
|
);
|
||||||
window.clearTimeout(self.hideTimer);
|
el.addEventListener(
|
||||||
self.hideTimer = window.setTimeout(self.close, delay);
|
"focusout", hideTooltip,
|
||||||
},
|
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
||||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
||||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
||||||
|
<option value="silenced">{{ i18n.ts.silenced }}</option>
|
||||||
<option value="notResponding">
|
<option value="notResponding">
|
||||||
{{ i18n.ts.notResponding }}
|
{{ i18n.ts.notResponding }}
|
||||||
</option>
|
</option>
|
||||||
|
@ -105,13 +106,11 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import MkButton from "@/components/MkButton.vue";
|
|
||||||
import MkInput from "@/components/form/input.vue";
|
import MkInput from "@/components/form/input.vue";
|
||||||
import MkSelect from "@/components/form/select.vue";
|
import MkSelect from "@/components/form/select.vue";
|
||||||
import MkPagination from "@/components/MkPagination.vue";
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
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 * as os from "@/os";
|
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
let host = $ref("");
|
let host = $ref("");
|
||||||
|
@ -134,6 +133,8 @@ const pagination = {
|
||||||
? { suspended: true }
|
? { suspended: true }
|
||||||
: state === "blocked"
|
: state === "blocked"
|
||||||
? { blocked: true }
|
? { blocked: true }
|
||||||
|
: state === "silenced"
|
||||||
|
? { silenced: true }
|
||||||
: state === "notResponding"
|
: state === "notResponding"
|
||||||
? { notResponding: true }
|
? { notResponding: true }
|
||||||
: {}),
|
: {}),
|
||||||
|
@ -143,6 +144,7 @@ const pagination = {
|
||||||
function getStatus(instance) {
|
function getStatus(instance) {
|
||||||
if (instance.isSuspended) return "Suspended";
|
if (instance.isSuspended) return "Suspended";
|
||||||
if (instance.isBlocked) return "Blocked";
|
if (instance.isBlocked) return "Blocked";
|
||||||
|
if (instance.isSilenced) return "Silenced";
|
||||||
if (instance.isNotResponding) return "Error";
|
if (instance.isNotResponding) return "Error";
|
||||||
return "Alive";
|
return "Alive";
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,10 +313,8 @@ onUnmounted(() => {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
opacity: 1;
|
&:focus-visible,
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header
|
<template #header
|
||||||
><MkPageHeader
|
><MkPageHeader
|
||||||
v-model:tab="tab"
|
|
||||||
:actions="headerActions"
|
:actions="headerActions"
|
||||||
:tabs="headerTabs"
|
:tabs="headerTabs"
|
||||||
:display-back-button="true"
|
:display-back-button="true"
|
||||||
|
|
|
@ -7,13 +7,31 @@
|
||||||
:display-back-button="true"
|
:display-back-button="true"
|
||||||
/></template>
|
/></template>
|
||||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||||
|
<MkTab v-model="tab" class="_formBlock">
|
||||||
|
<option value="block">{{ i18n.ts.blockedInstances }}</option>
|
||||||
|
<option value="silence">{{ i18n.ts.silencedInstances }}</option>
|
||||||
|
</MkTab>
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<FormTextarea v-model="blockedHosts" class="_formBlock">
|
<FormTextarea
|
||||||
|
v-if="tab === 'block'"
|
||||||
|
v-model="blockedHosts"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
<span>{{ i18n.ts.blockedInstances }}</span>
|
<span>{{ i18n.ts.blockedInstances }}</span>
|
||||||
<template #caption>{{
|
<template #caption>{{
|
||||||
i18n.ts.blockedInstancesDescription
|
i18n.ts.blockedInstancesDescription
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
|
<FormTextarea
|
||||||
|
v-else-if="tab === 'silence'"
|
||||||
|
v-model="silencedHosts"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<span>{{ i18n.ts.silencedInstances }}</span>
|
||||||
|
<template #caption>{{
|
||||||
|
i18n.ts.silencedInstancesDescription
|
||||||
|
}}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
<FormButton primary class="_formBlock" @click="save"
|
<FormButton primary class="_formBlock" @click="save"
|
||||||
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
|
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
|
||||||
|
@ -29,21 +47,28 @@ import {} from "vue";
|
||||||
import FormButton from "@/components/MkButton.vue";
|
import FormButton from "@/components/MkButton.vue";
|
||||||
import FormTextarea from "@/components/form/textarea.vue";
|
import FormTextarea from "@/components/form/textarea.vue";
|
||||||
import FormSuspense from "@/components/form/suspense.vue";
|
import FormSuspense from "@/components/form/suspense.vue";
|
||||||
|
import MkTab from "@/components/MkTab.vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { fetchInstance } from "@/instance";
|
import { fetchInstance } from "@/instance";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||||
|
|
||||||
let blockedHosts: string = $ref("");
|
let blockedHosts: string = $ref("");
|
||||||
|
let silencedHosts: string = $ref("");
|
||||||
|
let tab = $ref("block");
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await os.api("admin/meta");
|
const meta = await os.api("admin/meta");
|
||||||
blockedHosts = meta.blockedHosts.join("\n");
|
if (meta) {
|
||||||
|
blockedHosts = meta.blockedHosts.join("\n");
|
||||||
|
silencedHosts = meta.silencedHosts.join("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
os.apiWithDialog("admin/update-meta", {
|
os.apiWithDialog("admin/update-meta", {
|
||||||
blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [],
|
blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [],
|
||||||
|
silencedHosts: silencedHosts.split("\n").map((h) => h.trim()) || [],
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,12 @@
|
||||||
class="user"
|
class="user"
|
||||||
:to="`/user-info/${user.id}`"
|
:to="`/user-info/${user.id}`"
|
||||||
>
|
>
|
||||||
<MkAvatar :user="user" class="avatar" indicator />
|
<MkAvatar
|
||||||
|
:user="user"
|
||||||
|
class="avatar"
|
||||||
|
indicator
|
||||||
|
disableLink
|
||||||
|
/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
|
@ -371,6 +371,34 @@
|
||||||
<template #label>Pro account</template>
|
<template #label>Pro account</template>
|
||||||
</FormSwitch>
|
</FormSwitch>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
<FormSection>
|
||||||
|
<template #label>Libre Translate</template>
|
||||||
|
|
||||||
|
<FormInput
|
||||||
|
v-model="libreTranslateApiUrl"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #prefix
|
||||||
|
><i class="ph-link ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
|
<template #label
|
||||||
|
>Libre Translate API URL</template
|
||||||
|
>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput
|
||||||
|
v-model="libreTranslateApiKey"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #prefix
|
||||||
|
><i class="ph-key ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
|
<template #label
|
||||||
|
>Libre Translate API Key</template
|
||||||
|
>
|
||||||
|
</FormInput>
|
||||||
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
@ -422,6 +450,8 @@ let swPublicKey: any = $ref(null);
|
||||||
let swPrivateKey: any = $ref(null);
|
let swPrivateKey: any = $ref(null);
|
||||||
let deeplAuthKey: string = $ref("");
|
let deeplAuthKey: string = $ref("");
|
||||||
let deeplIsPro: boolean = $ref(false);
|
let deeplIsPro: boolean = $ref(false);
|
||||||
|
let libreTranslateApiUrl: string = $ref("");
|
||||||
|
let libreTranslateApiKey: string = $ref("");
|
||||||
let defaultReaction: string = $ref("");
|
let defaultReaction: string = $ref("");
|
||||||
let defaultReactionCustom: string = $ref("");
|
let defaultReactionCustom: string = $ref("");
|
||||||
|
|
||||||
|
@ -456,6 +486,8 @@ async function init() {
|
||||||
swPrivateKey = meta.swPrivateKey;
|
swPrivateKey = meta.swPrivateKey;
|
||||||
deeplAuthKey = meta.deeplAuthKey;
|
deeplAuthKey = meta.deeplAuthKey;
|
||||||
deeplIsPro = meta.deeplIsPro;
|
deeplIsPro = meta.deeplIsPro;
|
||||||
|
libreTranslateApiUrl = meta.libreTranslateApiUrl;
|
||||||
|
libreTranslateApiKey = meta.libreTranslateApiKey;
|
||||||
defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
|
defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
|
||||||
? meta.defaultReaction
|
? meta.defaultReaction
|
||||||
: "custom";
|
: "custom";
|
||||||
|
@ -498,6 +530,8 @@ function save() {
|
||||||
swPrivateKey,
|
swPrivateKey,
|
||||||
deeplAuthKey,
|
deeplAuthKey,
|
||||||
deeplIsPro,
|
deeplIsPro,
|
||||||
|
libreTranslateApiUrl,
|
||||||
|
libreTranslateApiKey,
|
||||||
defaultReaction,
|
defaultReaction,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:user="req.follower"
|
:user="req.follower"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
|
disableLink
|
||||||
/>
|
/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
|
|
|
@ -98,6 +98,14 @@
|
||||||
@update:modelValue="toggleBlock"
|
@update:modelValue="toggleBlock"
|
||||||
>{{ i18n.ts.blockThisInstance }}</FormSwitch
|
>{{ i18n.ts.blockThisInstance }}</FormSwitch
|
||||||
>
|
>
|
||||||
|
<FormSwitch
|
||||||
|
v-model="isSilenced"
|
||||||
|
class="_formBlock"
|
||||||
|
@update:modelValue="toggleSilence"
|
||||||
|
>{{
|
||||||
|
i18n.ts.silenceThisInstance
|
||||||
|
}}</FormSwitch
|
||||||
|
>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
<MkButton @click="refreshMetadata"
|
<MkButton @click="refreshMetadata"
|
||||||
><i
|
><i
|
||||||
|
@ -329,7 +337,7 @@
|
||||||
import { watch } from "vue";
|
import { watch } from "vue";
|
||||||
import { Virtual } from "swiper";
|
import { Virtual } from "swiper";
|
||||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||||
import type * as misskey from "calckey-js";
|
import type * as calckey from "calckey-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";
|
||||||
|
@ -352,11 +360,13 @@ import "swiper/scss";
|
||||||
import "swiper/scss/virtual";
|
import "swiper/scss/virtual";
|
||||||
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
||||||
|
|
||||||
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
|
type AugmentedInstanceMetadata = calckey.entities.DetailedInstanceMetadata & {
|
||||||
blockedHosts: string[];
|
blockedHosts: string[];
|
||||||
|
silencedHosts: string[];
|
||||||
};
|
};
|
||||||
type AugmentedInstance = misskey.entities.Instance & {
|
type AugmentedInstance = calckey.entities.Instance & {
|
||||||
isBlocked: boolean;
|
isBlocked: boolean;
|
||||||
|
isSilenced: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -373,6 +383,7 @@ let meta = $ref<AugmentedInstanceMetadata | null>(null);
|
||||||
let instance = $ref<AugmentedInstance | null>(null);
|
let instance = $ref<AugmentedInstance | null>(null);
|
||||||
let suspended = $ref(false);
|
let suspended = $ref(false);
|
||||||
let isBlocked = $ref(false);
|
let isBlocked = $ref(false);
|
||||||
|
let isSilenced = $ref(false);
|
||||||
let faviconUrl = $ref(null);
|
let faviconUrl = $ref(null);
|
||||||
|
|
||||||
const usersPagination = {
|
const usersPagination = {
|
||||||
|
@ -386,16 +397,14 @@ const usersPagination = {
|
||||||
offsetMode: true,
|
offsetMode: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function init() {
|
|
||||||
meta = await os.api("admin/meta");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
|
meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
|
||||||
instance = (await os.api("federation/show-instance", {
|
instance = (await os.api("federation/show-instance", {
|
||||||
host: props.host,
|
host: props.host,
|
||||||
})) as AugmentedInstance;
|
})) as AugmentedInstance;
|
||||||
suspended = instance.isSuspended;
|
suspended = instance.isSuspended;
|
||||||
isBlocked = instance.isBlocked;
|
isBlocked = instance.isBlocked;
|
||||||
|
isSilenced = instance.isSilenced;
|
||||||
faviconUrl =
|
faviconUrl =
|
||||||
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
|
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
|
||||||
getProxiedImageUrlNullable(instance.iconUrl, "preview");
|
getProxiedImageUrlNullable(instance.iconUrl, "preview");
|
||||||
|
@ -417,6 +426,22 @@ async function toggleBlock() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleSilence() {
|
||||||
|
if (meta == null) return;
|
||||||
|
if (!instance) {
|
||||||
|
throw new Error(`Instance info not loaded`);
|
||||||
|
}
|
||||||
|
let silencedHosts: string[];
|
||||||
|
if (isSilenced) {
|
||||||
|
silencedHosts = meta.silencedHosts.concat([instance.host]);
|
||||||
|
} else {
|
||||||
|
silencedHosts = meta.silencedHosts.filter((x) => x !== instance!.host);
|
||||||
|
}
|
||||||
|
await os.api("admin/update-meta", {
|
||||||
|
silencedHosts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleSuspend(v) {
|
async function toggleSuspend(v) {
|
||||||
await os.api("admin/federation/update-instance", {
|
await os.api("admin/federation/update-instance", {
|
||||||
host: instance.host,
|
host: instance.host,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader /></template>
|
<template #header><MkPageHeader /></template>
|
||||||
<MkSpacer :content-max="800">
|
<MkSpacer :content-max="800">
|
||||||
<div class="mwysmxbg">
|
<div :class="$style.root">
|
||||||
<div>{{ i18n.ts._mfm.intro }}</div>
|
<div>{{ i18n.ts._mfm.intro }}</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="section _block">
|
<div class="section _block">
|
||||||
|
@ -137,6 +137,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.blockMath }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.blockMathDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_blockMath" />
|
||||||
|
<MkTextarea v-model="preview_blockMath"
|
||||||
|
><template #label>MFM</template></MkTextarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- deprecated
|
<!-- deprecated
|
||||||
<div class="section _block">
|
<div class="section _block">
|
||||||
<div class="title">{{ i18n.ts._mfm.search }}</div>
|
<div class="title">{{ i18n.ts._mfm.search }}</div>
|
||||||
|
@ -341,6 +353,54 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.position }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.positionDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_position" />
|
||||||
|
<MkTextarea v-model="preview_position"
|
||||||
|
><span>MFM</span></MkTextarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.scale }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.scaleDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_scale" />
|
||||||
|
<MkTextarea v-model="preview_scale"
|
||||||
|
><span>MFM</span></MkTextarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.foreground }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.foregroundDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_fg" />
|
||||||
|
<MkTextarea v-model="preview_fg"
|
||||||
|
><span>MFM</span></MkTextarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.background }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.backgroundDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_bg" />
|
||||||
|
<MkTextarea v-model="preview_bg"
|
||||||
|
><span>MFM</span></MkTextarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="section _block">
|
<div class="section _block">
|
||||||
<div class="title">{{ i18n.ts._mfm.plain }}</div>
|
<div class="title">{{ i18n.ts._mfm.plain }}</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -379,8 +439,11 @@ let preview_blockCode = $ref(
|
||||||
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```'
|
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```'
|
||||||
);
|
);
|
||||||
let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
|
let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
|
||||||
|
let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
|
||||||
let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
|
let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
|
||||||
let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`);
|
let preview_search = $ref(
|
||||||
|
`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`
|
||||||
|
);
|
||||||
let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");
|
let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");
|
||||||
let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
|
let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
|
||||||
let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");
|
let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");
|
||||||
|
@ -402,7 +465,17 @@ let preview_x4 = $ref("$[x4 🍮]");
|
||||||
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
|
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
|
||||||
let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
|
let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
|
||||||
let preview_sparkle = $ref("$[sparkle 🍮]");
|
let preview_sparkle = $ref("$[sparkle 🍮]");
|
||||||
let preview_rotate = $ref("$[rotate 🍮]");
|
let preview_rotate = $ref(
|
||||||
|
"$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]"
|
||||||
|
);
|
||||||
|
let preview_position = $ref(
|
||||||
|
"$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]"
|
||||||
|
);
|
||||||
|
let preview_scale = $ref(
|
||||||
|
"$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]"
|
||||||
|
);
|
||||||
|
let preview_fg = $ref("$[fg.color=ff0000 Text color]");
|
||||||
|
let preview_bg = $ref("$[bg.color=ff0000 Background color]");
|
||||||
let preview_plain = $ref(
|
let preview_plain = $ref(
|
||||||
"<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>"
|
"<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>"
|
||||||
);
|
);
|
||||||
|
@ -413,8 +486,8 @@ definePageMetadata({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.mwysmxbg {
|
.root {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|
||||||
> .section {
|
> .section {
|
||||||
|
|
|
@ -6,14 +6,14 @@
|
||||||
{{ i18n.ts.addAccount }}</FormButton
|
{{ i18n.ts.addAccount }}</FormButton
|
||||||
>
|
>
|
||||||
|
|
||||||
<div
|
<button
|
||||||
v-for="account in accounts"
|
v-for="account in accounts"
|
||||||
:key="account.id"
|
:key="account.id"
|
||||||
class="_panel _button lcjjdxlm"
|
class="_panel _button lcjjdxlm"
|
||||||
@click="menu(account, $event)"
|
@click="menu(account, $event)"
|
||||||
>
|
>
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<MkAvatar :user="account" class="avatar" />
|
<MkAvatar :user="account" class="avatar" disableLink />
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<MkAcct :user="account" />
|
<MkAcct :user="account" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -158,6 +158,8 @@ definePageMetadata({
|
||||||
.lcjjdxlm {
|
.lcjjdxlm {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: unset;
|
||||||
|
|
||||||
> .avatar {
|
> .avatar {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -308,6 +308,7 @@ function showMenu(ev) {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .fg {
|
> .fg {
|
||||||
|
|
|
@ -204,10 +204,6 @@ hr {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|