Merge branch 'develop' of https://codeberg.org/calckey/calckey into keyboard-accessability

This commit is contained in:
Freeplay 2023-04-30 12:31:41 -04:00
commit 7e2d5c45d2
32 changed files with 473 additions and 211 deletions

View File

@ -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

View File

@ -675,3 +675,48 @@ useGlobalSetting: Fes servir els ajustos globals
useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del
teu compte. Si es desactiva , es poden fer configuracions individuals. teu compte. Si es desactiva , es poden fer configuracions individuals.
other: Altres other: Altres
menu: Menú
addItem: Afegeix un element
divider: Divisor
relays: Relés
addRelay: Afegeix un Relé
inboxUrl: Adreça de la safata d'entrada
addedRelays: Relés afegits
serviceworkerInfo: Ha de estar activat per les notificacions push.
poll: Enquesta
deletedNote: Article eliminat
disablePlayer: Tancar el reproductor de vídeo
fileIdOrUrl: ID o adreça URL del fitxer
behavior: Comportament
regenerateLoginTokenDescription: Regenera el token que es fa servir de manera interna
durant l'inici de sessió. Normalment això no és necessari. Si es torna a genera
el token, es tancarà la sessió a tots els dispositius.
setMultipleBySeparatingWithSpace: Separa diferents entrades amb espais.
reportAbuseOf: Informa sobre {name}
sample: Exemple
abuseReports: Informes
reportAbuse: Informe
reporter: Informador
reporterOrigin: Origen d'el informador
forwardReport: Envia l'informe a una instancia remota
abuseReported: El teu informe ha sigut enviat. Moltes gràcies.
reporteeOrigin: Origen de l'informe
send: Enviar
abuseMarkAsResolved: Marcar l'informe com a resolt
visibility: Visibilitat
useCw: Amaga el contingut
enablePlayer: Obre el reproductor de vídeo
yourAccountSuspendedDescription: Aquest compte ha sigut suspesa per no seguir els
termes de servei del servidor o quelcom similar. Contacte amb l'administrador si
vols conèixer la raó amb més detall. Si us plau no facis un compte nou.
invisibleNote: Article ocult
enableInfiniteScroll: Carregar més de forma automàtica
fillAbuseReportDescription: Si us plau omple els detalls sobre aquest informe. Si
es sobre un article en concret, si us plau inclou l'adreça URL.
forwardReportIsAnonymous: Com a informador a l'instància remota no es mostrarà el
teu compte, si no un compte anònim.
openInNewTab: Obrir en una pestanya nova
openInSideView: Obrir a la vista lateral
defaultNavigationBehaviour: Navegació per defecte
editTheseSettingsMayBreakAccount: Si edites aquestes configuracions pots fer mal bé
el teu compte.

View File

@ -1042,7 +1042,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."

View File

@ -3,7 +3,7 @@ fetchingAsApObject: Hae Fedeversestä
gotIt: Selvä! gotIt: Selvä!
cancel: Peruuta cancel: Peruuta
enterUsername: Anna käyttäjänimi enterUsername: Anna käyttäjänimi
renotedBy: Buustannut {käyttäjä} renotedBy: Buustannut {user}
noNotes: Ei lähetyksiä noNotes: Ei lähetyksiä
noNotifications: Ei ilmoituksia noNotifications: Ei ilmoituksia
instance: Instanssi instance: Instanssi
@ -41,3 +41,183 @@ favorite: Lisää kirjanmerkkeihin
copyContent: Kopioi sisältö copyContent: Kopioi sisältö
deleteAndEdit: Poista ja muokkaa deleteAndEdit: Poista ja muokkaa
copyLink: Kopioi linkki 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

View File

@ -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",

BIN
packages/backend/assets/favicon.ico (Stored with Git LFS)

Binary file not shown.

BIN
packages/backend/assets/favicon.png (Stored with Git LFS)

Binary file not shown.

BIN
packages/backend/assets/favicon.svg (Stored with Git LFS)

Binary file not shown.

BIN
packages/backend/assets/icons/192.png (Stored with Git LFS)

Binary file not shown.

BIN
packages/backend/assets/icons/512.png (Stored with Git LFS)

Binary file not shown.

BIN
packages/backend/assets/inverse wordmark.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
packages/backend/assets/splash.png (Stored with Git LFS)

Binary file not shown.

View 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"
`);
}
}

View File

@ -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",

View File

@ -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;

View File

@ -386,6 +386,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,

View File

@ -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") {

View File

@ -512,7 +512,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,
@ -564,6 +565,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,
}; };

View File

@ -124,6 +124,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 },
@ -515,6 +517,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;
} }

View File

@ -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

View File

@ -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);

View File

@ -1,7 +0,0 @@
node_modules
/built
/coverage
/.eslintrc.js
/jest.config.ts
/test
/test-d

View File

@ -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",
},
};

View File

@ -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"
}, },

View File

@ -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;
}, },
}, },
}); });

View File

@ -89,7 +89,7 @@
/> />
</div> </div>
</div> </div>
<div class="tail" :class="{ collapsed }"> <div class="tail">
<header> <header>
<span v-if="notification.type === 'pollEnded'">{{ <span v-if="notification.type === 'pollEnded'">{{
i18n.ts._notification.pollEnded i18n.ts._notification.pollEnded
@ -112,11 +112,11 @@
v-if="notification.type === 'reaction'" v-if="notification.type === 'reaction'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="summary" :title="getNoteSummary(notification.note)"
> >
<i class="ph-quotes ph-fill ph-lg"></i> <i class="ph-quotes ph-fill ph-lg"></i>
<Mfm <Mfm
:text="summary" :text="getNoteSummary(notification.note)"
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:custom-emojis="notification.note.emojis" :custom-emojis="notification.note.emojis"
@ -142,10 +142,10 @@
v-if="notification.type === 'reply'" v-if="notification.type === 'reply'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="summary" :title="getNoteSummary(notification.note)"
> >
<Mfm <Mfm
:text="summary" :text="getNoteSummary(notification.note)"
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:custom-emojis="notification.note.emojis" :custom-emojis="notification.note.emojis"
@ -155,10 +155,10 @@
v-if="notification.type === 'mention'" v-if="notification.type === 'mention'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="summary" :title="getNoteSummary(notification.note)"
> >
<Mfm <Mfm
:text="summary" :text="getNoteSummary(notification.note)"
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:custom-emojis="notification.note.emojis" :custom-emojis="notification.note.emojis"
@ -168,10 +168,10 @@
v-if="notification.type === 'quote'" v-if="notification.type === 'quote'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="summary" :title="getNoteSummary(notification.note)"
> >
<Mfm <Mfm
:text="summary" :text="getNoteSummary(notification.note)"
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:custom-emojis="notification.note.emojis" :custom-emojis="notification.note.emojis"
@ -181,11 +181,11 @@
v-if="notification.type === 'pollVote'" v-if="notification.type === 'pollVote'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="summary" :title="getNoteSummary(notification.note)"
> >
<i class="ph-quotes ph-fill ph-lg"></i> <i class="ph-quotes ph-fill ph-lg"></i>
<Mfm <Mfm
:text="summary" :text="getNoteSummary(notification.note)"
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:custom-emojis="notification.note.emojis" :custom-emojis="notification.note.emojis"
@ -196,11 +196,11 @@
v-if="notification.type === 'pollEnded'" v-if="notification.type === 'pollEnded'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="summary" :title="getNoteSummary(notification.note)"
> >
<i class="ph-quotes ph-fill ph-lg"></i> <i class="ph-quotes ph-fill ph-lg"></i>
<Mfm <Mfm
:text="summary" :text="getNoteSummary(notification.note)"
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:custom-emojis="notification.note.emojis" :custom-emojis="notification.note.emojis"
@ -264,7 +264,6 @@
<span v-if="notification.type === 'app'" class="text"> <span v-if="notification.type === 'app'" class="text">
<Mfm :text="notification.body" :nowrap="!full" /> <Mfm :text="notification.body" :nowrap="!full" />
</span> </span>
<xShowMoreButton v-if="isLong" v-model="collapsed"></xShowMoreButton>
</div> </div>
</div> </div>
</template> </template>
@ -275,7 +274,6 @@ import * as misskey from "calckey-js";
import XReactionIcon from "@/components/MkReactionIcon.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
import XReactionTooltip from "@/components/MkReactionTooltip.vue"; import XReactionTooltip from "@/components/MkReactionTooltip.vue";
import XShowMoreButton from "./MkShowMoreButton.vue";
import { getNoteSummary } from "@/scripts/get-note-summary"; import { getNoteSummary } from "@/scripts/get-note-summary";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
@ -301,19 +299,12 @@ const props = withDefaults(
const elRef = ref<HTMLElement>(null); const elRef = ref<HTMLElement>(null);
const reactionRef = ref(null); const reactionRef = ref(null);
const summary = getNoteSummary(props.notification.note);
const showEmojiReactions = const showEmojiReactions =
defaultStore.state.enableEmojiReactions || defaultStore.state.enableEmojiReactions ||
defaultStore.state.showEmojisInReactionNotifications; defaultStore.state.showEmojisInReactionNotifications;
const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction) const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction)
? instance.defaultReaction ? instance.defaultReaction
: "⭐"; : "⭐";
const isLong = (summary.split("\n").length > 3 || summary.length > 200);
const collapsed = $ref(isLong);
let readObserver: IntersectionObserver | undefined; let readObserver: IntersectionObserver | undefined;
let connection; let connection;
@ -495,7 +486,6 @@ useTooltip(reactionRef, (showing) => {
} }
> .tail { > .tail {
position: relative;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -536,17 +526,6 @@ useTooltip(reactionRef, (showing) => {
margin-left: 4px; margin-left: 4px;
} }
} }
&.collapsed > .text {
display: block;
position: relative;
max-height: calc(4em + 50px);
overflow: hidden;
mask: linear-gradient(black calc(100% - 64px), transparent);
-webkit-mask: linear-gradient(
black calc(100% - 64px),
transparent
);
}
} }
} }
</style> </style>

View File

@ -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>

View File

@ -120,7 +120,6 @@ 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";
@ -145,7 +144,6 @@ const isLong =
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;
@ -274,6 +272,43 @@ function focusFooter(ev) {
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);
}
} }
} }
} }

View File

@ -55,7 +55,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 +128,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";

View File

@ -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();

View File

@ -87,8 +87,8 @@ importers:
specifier: ^4.6.4 specifier: ^4.6.4
version: 4.10.2 version: 4.10.2
'@calckey/megalodon': '@calckey/megalodon':
specifier: 5.1.24 specifier: 5.2.0
version: 5.1.24 version: 5.2.0
'@discordapp/twemoji': '@discordapp/twemoji':
specifier: 14.0.2 specifier: 14.0.2
version: 14.0.2 version: 14.0.2
@ -1384,8 +1384,8 @@ packages:
'@bull-board/api': 4.10.2 '@bull-board/api': 4.10.2
dev: false dev: false
/@calckey/megalodon@5.1.24: /@calckey/megalodon@5.2.0:
resolution: {integrity: sha512-VRd6x8MFQ2pMF0rnGF67/GVxgp/92CV7lg2XT1wnPAfQZ1NTsjwlDQX3HewEW3fSG/r7Nzh5WbIBXC8WMWKs9g==} resolution: {integrity: sha512-9MEjzKJPyd7o5bHGGlNq4oE1tMt22GUJ8o8tZXcXSpXlrSDb2rSwumirM1KXUWTW8G6NGi1leCM59gOBGLko3w==}
engines: {node: '>=15.0.0'} engines: {node: '>=15.0.0'}
dependencies: dependencies:
'@types/oauth': 0.9.1 '@types/oauth': 0.9.1
@ -5910,8 +5910,8 @@ packages:
requiresBuild: true requiresBuild: true
dev: false dev: false
/core-js@3.30.0: /core-js@3.30.1:
resolution: {integrity: sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==} resolution: {integrity: sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==}
requiresBuild: true requiresBuild: true
dev: true dev: true
@ -15413,7 +15413,7 @@ packages:
name: plyr name: plyr
version: 3.7.0 version: 3.7.0
dependencies: dependencies:
core-js: 3.30.0 core-js: 3.30.1
custom-event-polyfill: 1.0.7 custom-event-polyfill: 1.0.7
loadjs: 4.2.0 loadjs: 4.2.0
rangetouch: 2.0.1 rangetouch: 2.0.1