Merge branch 'develop' of https://codeberg.org/calckey/calckey into notifications

This commit is contained in:
Freeplay 2023-05-22 21:12:11 -04:00
commit 3a92e77c6c
36 changed files with 494 additions and 314 deletions

View file

@ -82,8 +82,8 @@ If you have access to a server that supports one of the sources below, I recomme
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommend) - 🍱 At least [Redis](https://redis.io/) v6 (v7 recommend)
- Web Proxy (one of the following) - Web Proxy (one of the following)
- 🍀 Nginx (recommended) - 🍀 Nginx (recommended)
- 🪶 Apache
- 🦦 Caddy - 🦦 Caddy
- 🪶 Apache
### 😗 Optional dependencies ### 😗 Optional dependencies
@ -107,7 +107,8 @@ git clone --depth 1 https://codeberg.org/calckey/calckey.git
cd calckey/ cd calckey/
``` ```
By default, you're on the development branch. Run `git checkout beta` or `git checkout main` to switch to the Beta/Main branches. > **Note**
> By default, you're on the main branch. Run `git checkout beta` or `git checkout develop` to switch to the Beta/Develop branches.
## 📩 Install dependencies ## 📩 Install dependencies
@ -128,11 +129,18 @@ npm i -g pm2
pm2 install pm2-logrotate pm2 install pm2-logrotate
``` ```
[`pm2-logrotate`](https://github.com/keymetrics/pm2-logrotate/blob/master/README.md) ensures that log files don't infinitely gather size, as Calckey produces a lot of logs. > **Note**
> [`pm2-logrotate`](https://github.com/keymetrics/pm2-logrotate/blob/master/README.md) ensures that log files don't infinitely gather size, as Calckey produces a lot of logs.
## 🐘 Create database ## 🐘 Create database
Assuming you set up PostgreSQL correctly, all you have to run is: In PostgreSQL (`psql`), run the following command:
```sql
CREATE DATABASE calckey WITH encoding = 'UTF8';
```
or run the following from the command line:
```sh ```sh
psql postgres -c "create database calckey with encoding = 'UTF8';" psql postgres -c "create database calckey with encoding = 'UTF8';"
@ -144,7 +152,8 @@ In Calckey's directory, fill out the `db` section of `.config/default.yml` with
Follow sonic's [installation guide](https://github.com/valeriansaliou/sonic#installation) Follow sonic's [installation guide](https://github.com/valeriansaliou/sonic#installation)
If you use IPv4: in Sonic's directory, edit the `config.cfg` file to change `inet` to `"0.0.0.0:1491"`. > **Note**
> If you use IPv4: in Sonic's directory, edit the `config.cfg` file to change `inet` to `"0.0.0.0:1491"`.
In Calckey's directory, fill out the `sonic` section of `.config/default.yml` with the correct information. In Calckey's directory, fill out the `sonic` section of `.config/default.yml` with the correct information.
@ -177,13 +186,6 @@ For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document](
- Run `sudo ln -s ./calckey.nginx.conf ../sites-enabled/calckey.nginx.conf` - Run `sudo ln -s ./calckey.nginx.conf ../sites-enabled/calckey.nginx.conf`
- Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service. - Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
### 🪶 Apache
- Run `sudo cp ./calckey.apache.conf /etc/apache2/sites-available/ && cd /etc/apache2/sites-available/`
- Edit `calckey.apache.conf` to reflect your instance properly
- Run `sudo a2ensite calckey.apache` to enable the site
- Run `sudo service apache2 restart` to reload apache2 configuration
### 🦦 Caddy ### 🦦 Caddy
- Add the following block to your `Caddyfile`, replacing `example.tld` with your own domain: - Add the following block to your `Caddyfile`, replacing `example.tld` with your own domain:
@ -194,6 +196,15 @@ example.tld {
``` ```
- Reload your caddy configuration - Reload your caddy configuration
### 🪶 Apache
> **Warning**
> Apache has some known problems with Calckey. Only use it if you have to.
- Run `sudo cp ./calckey.apache.conf /etc/apache2/sites-available/ && cd /etc/apache2/sites-available/`
- Edit `calckey.apache.conf` to reflect your instance properly
- Run `sudo a2ensite calckey.apache` to enable the site
- Run `sudo service apache2 restart` to reload apache2 configuration
## 🚀 Build and launch! ## 🚀 Build and launch!
### 🐢 NodeJS + pm2 ### 🐢 NodeJS + pm2

View file

@ -1290,7 +1290,7 @@ loadRawImages: Carregar les imatges originals en comptes de mostrar les miniatur
noteFavoritesCount: Nombre de notes afegides a favorits noteFavoritesCount: Nombre de notes afegides a favorits
useSystemFont: Fes servir la font per defecte del sistema useSystemFont: Fes servir la font per defecte del sistema
contact: Contacte contact: Contacte
clips: Clips clips: Retalls
experimentalFeatures: Característiques experimentals experimentalFeatures: Característiques experimentals
developer: Desenvolupador developer: Desenvolupador
makeExplorableDescription: Si desactives aquesta funció el teu compte no sortirà a makeExplorableDescription: Si desactives aquesta funció el teu compte no sortirà a
@ -1496,7 +1496,7 @@ gallery: Galeria
popularPosts: Pàgines populars popularPosts: Pàgines populars
shareWithNote: Comparteix amb una publicació shareWithNote: Comparteix amb una publicació
expiration: Data límit expiration: Data límit
memo: Memo memo: Recordatori
priority: Prioritat priority: Prioritat
high: Alta high: Alta
middle: Mitjana middle: Mitjana
@ -1537,7 +1537,7 @@ incorrectPassword: Contrasenya incorrecta.
clickToFinishEmailVerification: Feu clic a [{ok}] per completar la verificació del clickToFinishEmailVerification: Feu clic a [{ok}] per completar la verificació del
correu electrònic. correu electrònic.
overridedDeviceKind: Tipus de dispositiu overridedDeviceKind: Tipus de dispositiu
smartphone: Smartphone smartphone: Telèfon intel·ligent
tablet: Tauleta tablet: Tauleta
auto: Automàtic auto: Automàtic
recentNHours: Últimes {n} hores recentNHours: Últimes {n} hores
@ -1625,7 +1625,7 @@ customKaTeXMacroDescription: "Configura macros per escriure expressions matemàt
objectStorageRegion: Regió objectStorageRegion: Regió
objectStoragePrefix: Prefix objectStoragePrefix: Prefix
objectStoragePrefixDesc: Els fitxers es guardaran dins de carpetes amb aquest prefix. objectStoragePrefixDesc: Els fitxers es guardaran dins de carpetes amb aquest prefix.
objectStorageEndpoint: Endpoint objectStorageEndpoint: Extrem
newNoteRecived: Hi han notes noves newNoteRecived: Hi han notes noves
sounds: Sons sounds: Sons
listen: Escoltar listen: Escoltar
@ -1704,7 +1704,9 @@ oneWeek: Una setmana
reflectMayTakeTime: Pot trigar una mica a reflectir-se. reflectMayTakeTime: Pot trigar una mica a reflectir-se.
thereIsUnresolvedAbuseReportWarning: Hi ha informes sense resoldre. thereIsUnresolvedAbuseReportWarning: Hi ha informes sense resoldre.
driveCapOverrideLabel: Canvieu la capacitat del disc per a aquest usuari driveCapOverrideLabel: Canvieu la capacitat del disc per a aquest usuari
isSystemAccount: Un compte creat i operat automàticament pel sistema. isSystemAccount: Aquest compte és creat i operat automàticament pel sistema. Si us
plau, no modereu, editeu, suprimiu o modifiqueu aquest compte de cap forma, o podria
trencar el vostre servidor.
typeToConfirm: Introduïu {x} per confirmar typeToConfirm: Introduïu {x} per confirmar
deleteAccount: Suprimeix el compte deleteAccount: Suprimeix el compte
document: Documentació document: Documentació
@ -1746,7 +1748,7 @@ reverse: Revés
objectStorageBucket: Cubell objectStorageBucket: Cubell
objectStorageBucketDesc: Si us plau específica el nom del cubell que faràs servir objectStorageBucketDesc: Si us plau específica el nom del cubell que faràs servir
al teu proveïdor. al teu proveïdor.
clip: Clip clip: Retall
createNew: Crear una nova createNew: Crear una nova
optional: Opcional optional: Opcional
jumpToSpecifiedDate: Vés a una data concreta jumpToSpecifiedDate: Vés a una data concreta
@ -2056,3 +2058,5 @@ newer: Més nou
older: Més antic older: Més antic
silencedWarning: S'està mostrant aquesta pàgina per què aquest usuari és d'un servidor silencedWarning: S'està mostrant aquesta pàgina per què aquest usuari és d'un servidor
que l'administrador a silenciat, així que pot ser spam. que l'administrador a silenciat, així que pot ser spam.
jumpToPrevious: Vés a l'anterior
cw: Avís de contingut

View file

@ -1171,6 +1171,8 @@ _mfm:
sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt." sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt."
rotate: "Drehen" rotate: "Drehen"
rotateDescription: "Dreht den Inhalt um einen angegebenen Winkel." rotateDescription: "Dreht den Inhalt um einen angegebenen Winkel."
fade: "Ein-/Ausblenden"
fadeDescription: "Blended Inhalt ein and aus."
plain: "Schlicht" plain: "Schlicht"
plainDescription: "Deaktiviert jegliche MFM-Syntax, die sich innerhalb dieses MFM-Effekts\ plainDescription: "Deaktiviert jegliche MFM-Syntax, die sich innerhalb dieses MFM-Effekts\
\ befindet." \ befindet."

View file

@ -57,7 +57,7 @@ sendMessage: "Send a message"
copyUsername: "Copy username" copyUsername: "Copy username"
searchUser: "Search for a user" searchUser: "Search for a user"
reply: "Reply" reply: "Reply"
jumpToReply: "Jump to Reply" jumpToPrevious: "Jump to previous"
loadMore: "Load more" loadMore: "Load more"
showMore: "Show more" showMore: "Show more"
newer: "newer" newer: "newer"
@ -69,6 +69,7 @@ followRequestAccepted: "Follow request accepted"
mention: "Mention" mention: "Mention"
mentions: "Mentions" mentions: "Mentions"
directNotes: "Direct messages" directNotes: "Direct messages"
cw: "Content warning"
importAndExport: "Import/Export Data" importAndExport: "Import/Export Data"
import: "Import" import: "Import"
export: "Export" export: "Export"
@ -971,7 +972,7 @@ driveCapOverrideLabel: "Change the drive capacity for this user"
driveCapOverrideCaption: "Reset the capacity to default by inputting a value of 0\ driveCapOverrideCaption: "Reset the capacity to default by inputting a value of 0\
\ or lower." \ or lower."
requireAdminForView: "You must log in with an administrator account to view this." requireAdminForView: "You must log in with an administrator account to view this."
isSystemAccount: "An account created and automatically operated by the system." isSystemAccount: "This account is created and automatically operated by the system. Please do not moderate, edit, delete, or otherwise tamper with this account, or it may break your server."
typeToConfirm: "Please enter {x} to confirm" typeToConfirm: "Please enter {x} to confirm"
deleteAccount: "Delete account" deleteAccount: "Delete account"
document: "Documentation" document: "Documentation"
@ -1266,6 +1267,8 @@ _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."
fade: "Fade"
fadeDescription: "Fades content in and out."
position: "Position" position: "Position"
positionDescription: "Move content by a specified amount." positionDescription: "Move content by a specified amount."
scale: "Scale" scale: "Scale"

View file

@ -1077,6 +1077,8 @@ _mfm:
sparkle: "Paillettes" sparkle: "Paillettes"
sparkleDescription: "Ajoute un effet scintillant au contenu." sparkleDescription: "Ajoute un effet scintillant au contenu."
rotate: "Pivoter" rotate: "Pivoter"
fade: "Apparaître/Disparaître"
fadeDescription: "Fait apparaître et disparaître le contenu."
plainDescription: Désactiver les effets de tous les MFM contenus dans cet effet plainDescription: Désactiver les effets de tous les MFM contenus dans cet effet
MFM. MFM.
rotateDescription: Pivoter le contenu d'un angle spécifique. rotateDescription: Pivoter le contenu d'un angle spécifique.

View file

@ -910,6 +910,8 @@ _mfm:
fontDescription: "Puoi scegliere il tipo di carattere per il contenuto." fontDescription: "Puoi scegliere il tipo di carattere per il contenuto."
rainbow: "Arcobaleno" rainbow: "Arcobaleno"
rotate: "Ruota" rotate: "Ruota"
fade: "Dissolvenza"
fadeDescription: "Dissolvenza in entrata e in uscita del contenuto."
_instanceTicker: _instanceTicker:
none: "Nascondi" none: "Nascondi"
remote: "Mostra solo per gli/le utenti remotə" remote: "Mostra solo per gli/le utenti remotə"

View file

@ -13,7 +13,7 @@ password: "Wachtwoord"
forgotPassword: "Wachtwoord vergeten" forgotPassword: "Wachtwoord vergeten"
fetchingAsApObject: "Ophalen vanuit de Fediverse" fetchingAsApObject: "Ophalen vanuit de Fediverse"
ok: "Ok" ok: "Ok"
gotIt: "Begrepen" gotIt: "Begrepen!"
cancel: "Annuleren" cancel: "Annuleren"
enterUsername: "Voer een gebruikersnaam in" enterUsername: "Voer een gebruikersnaam in"
renotedBy: "Hergedeeld door {user}" renotedBy: "Hergedeeld door {user}"
@ -47,12 +47,12 @@ copyContent: "Kopiëren inhoud"
copyLink: "Kopiëren link" copyLink: "Kopiëren link"
delete: "Verwijderen" delete: "Verwijderen"
deleteAndEdit: "Verwijderen en bewerken" deleteAndEdit: "Verwijderen en bewerken"
deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? deleteAndEditConfirm: "Weet je zeker dat je deze post wilt verwijderen en dan bewerken?
Je verliest alle reacties, herdelingen en antwoorden erop." Je verliest alle reacties, boosts en antwoorden erop."
addToList: "Aan lijst toevoegen" addToList: "Aan lijst toevoegen"
sendMessage: "Verstuur bericht" sendMessage: "Verstuur bericht"
copyUsername: "Kopiëren gebruikersnaam " copyUsername: "Gebruikersnaam kopiëren"
searchUser: "Zoeken een gebruiker" searchUser: "Zoek een gebruiker"
reply: "Antwoord" reply: "Antwoord"
loadMore: "Laad meer" loadMore: "Laad meer"
showMore: "Toon meer" showMore: "Toon meer"
@ -68,7 +68,7 @@ export: "Export"
files: "Bestanden" files: "Bestanden"
download: "Downloaden" download: "Downloaden"
driveFileDeleteConfirm: "Weet je zeker dat je het bestand \"{name}\" wilt verwijderen? driveFileDeleteConfirm: "Weet je zeker dat je het bestand \"{name}\" wilt verwijderen?
Notities met dit bestand als bijlage worden ook verwijderd." Posts met dit bestand als bijlage worden ook verwijderd."
unfollowConfirm: "Weet je zeker dat je {name} wilt ontvolgen?" unfollowConfirm: "Weet je zeker dat je {name} wilt ontvolgen?"
exportRequested: "Je hebt een export aangevraagd. Dit kan een tijdje duren. Het wordt exportRequested: "Je hebt een export aangevraagd. Dit kan een tijdje duren. Het wordt
toegevoegd aan je Drive zodra het is voltooid." toegevoegd aan je Drive zodra het is voltooid."
@ -101,13 +101,13 @@ followRequests: "Volgverzoeken"
unfollow: "Ontvolgen" unfollow: "Ontvolgen"
followRequestPending: "Wachten op goedkeuring volgverzoek" followRequestPending: "Wachten op goedkeuring volgverzoek"
enterEmoji: "Voer een emoji in" enterEmoji: "Voer een emoji in"
renote: "Herdelen" renote: "Boost"
unrenote: "Stop herdelen" unrenote: "Stop herdelen"
renoted: "Herdeeld" renoted: "Boosted."
cantRenote: "Dit bericht kan niet worden herdeeld" cantRenote: "Dit bericht kan niet worden geboost."
cantReRenote: "Een herdeling kan niet worden herdeeld" cantReRenote: "Een boost kan niet worden geboost."
quote: "Quote" quote: "Quote"
pinnedNote: "Vastgemaakte notitie" pinnedNote: "Vastgemaakte post"
pinned: "Vastmaken aan profielpagina" pinned: "Vastmaken aan profielpagina"
you: "Jij" you: "Jij"
clickToShow: "Klik om te bekijken" clickToShow: "Klik om te bekijken"
@ -116,7 +116,7 @@ add: "Toevoegen"
reaction: "Reacties" reaction: "Reacties"
reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen,
Druk op \"+\" om toe te voegen" Druk op \"+\" om toe te voegen"
rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen" rememberNoteVisibility: "Onthoud post zichtbaarheidsinstellingen"
attachCancel: "Verwijder bijlage" attachCancel: "Verwijder bijlage"
markAsSensitive: "Markeren als NSFW" markAsSensitive: "Markeren als NSFW"
unmarkAsSensitive: "Geen NSFW" unmarkAsSensitive: "Geen NSFW"
@ -139,8 +139,8 @@ flagAsCat: "Markeer dit account als een kat."
flagAsCatDescription: "Zet deze vlag aan als je wilt aangeven dat dit account een flagAsCatDescription: "Zet deze vlag aan als je wilt aangeven dat dit account een
kat is." kat is."
flagShowTimelineReplies: "Toon antwoorden op de tijdlijn" flagShowTimelineReplies: "Toon antwoorden op de tijdlijn"
flagShowTimelineRepliesDescription: "Als je dit vlag aanzet, toont de tijdlijn ook flagShowTimelineRepliesDescription: "Als je deze vlag aanzet, toont de tijdlijn ook
antwoorden op andere en niet alleen jouw eigen notities." antwoorden op andere en niet alleen jouw eigen post."
autoAcceptFollowed: "Accepteer verzoeken om jezelf te volgen vanzelf als je de verzoeker autoAcceptFollowed: "Accepteer verzoeken om jezelf te volgen vanzelf als je de verzoeker
al volgt" al volgt"
addAccount: "Account toevoegen" addAccount: "Account toevoegen"
@ -203,8 +203,8 @@ mutedUsers: "Gedempte gebruikers"
blockedUsers: "Geblokkeerde gebruikers" blockedUsers: "Geblokkeerde gebruikers"
noUsers: "Er zijn geen gebruikers." noUsers: "Er zijn geen gebruikers."
editProfile: "Bewerk Profiel" editProfile: "Bewerk Profiel"
noteDeleteConfirm: "Ben je zeker dat je dit bericht wil verwijderen?" noteDeleteConfirm: "Ben je zeker dat je deze post wil verwijderen?"
pinLimitExceeded: "Je kunt geen berichten meer vastprikken" pinLimitExceeded: "Je kunt geen posts meer vastprikken"
intro: "Installatie van Calckey geëindigd! Maak nu een beheerder aan." intro: "Installatie van Calckey geëindigd! Maak nu een beheerder aan."
done: "Klaar" done: "Klaar"
processing: "Bezig met verwerken" processing: "Bezig met verwerken"
@ -499,3 +499,23 @@ manageGroups: Beheer groepen
subscribePushNotification: Pushmeldingen inschakelen subscribePushNotification: Pushmeldingen inschakelen
unsubscribePushNotification: Pushmeldingen uitschakelen unsubscribePushNotification: Pushmeldingen uitschakelen
pushNotificationAlreadySubscribed: Pushmeldingen zijn al ingeschakeld pushNotificationAlreadySubscribed: Pushmeldingen zijn al ingeschakeld
antennaSource: Antenne bron
antennaKeywords: Trefwoorden om naar te luisteren
antennaExcludeKeywords: Trefwoorden om te negeren
driveCapacityPerRemoteAccount: Schijfruimte per externe gebruiker
backgroundImageUrl: Achtergrondafbeelding URL
basicInfo: Basis informatie
pinnedUsers: Vastgezette gebruikers
pinnedPages: Vastgezette Pagina's
driveCapacityPerLocalAccount: Schijfruimte per lokale gebruiker
iconUrl: Icoon URL
bannerUrl: Banner afbeelding URL
manageAntennas: Beheer Antennes
name: Naam
notifyAntenna: Meld nieuwe posts
withFileAntenna: Alleen posts met bestanden
enableServiceworker: Schakel pushmeldingen voor je browser in
renoteUnmute: Ontdemp boosts
jumpToPrevious: Spring naar vorige
caseSensitive: Hoofdlettergevoelig
cw: Inhoudswaarschuwing

View file

@ -1,12 +1,12 @@
{ {
"name": "calckey", "name": "calckey",
"version": "14.0.0-dev14", "version": "14.0.0-dev18",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://codeberg.org/calckey/calckey.git" "url": "https://codeberg.org/calckey/calckey.git"
}, },
"packageManager": "pnpm@8.5.0", "packageManager": "pnpm@8.5.1",
"private": true, "private": true,
"scripts": { "scripts": {
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp", "rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",

View file

@ -85,7 +85,7 @@
"koa-send": "5.0.1", "koa-send": "5.0.1",
"koa-slow": "2.1.0", "koa-slow": "2.1.0",
"koa-views": "7.0.2", "koa-views": "7.0.2",
"mfm-js": "0.23.2", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"multer": "1.4.4-lts.1", "multer": "1.4.4-lts.1",
"native-utils": "link:native-utils", "native-utils": "link:native-utils",

View file

@ -9,6 +9,9 @@ export function nyaize(text: string): string {
.replace(/(?<=n)a/gi, (x) => (x === "A" ? "YA" : "ya")) .replace(/(?<=n)a/gi, (x) => (x === "A" ? "YA" : "ya"))
.replace(/(?<=morn)ing/gi, (x) => (x === "ING" ? "YAN" : "yan")) .replace(/(?<=morn)ing/gi, (x) => (x === "ING" ? "YAN" : "yan"))
.replace(/(?<=every)one/gi, (x) => (x === "ONE" ? "NYAN" : "nyan")) .replace(/(?<=every)one/gi, (x) => (x === "ONE" ? "NYAN" : "nyan"))
.replace(/non(?=[bcdfghjklmnpqrstvwxyz])/gi, (x) =>
x === "NON" ? "NYAN" : "nyan",
)
// ko-KR // ko-KR
.replace(/[나-낳]/g, (match) => .replace(/[나-낳]/g, (match) =>
String.fromCharCode( String.fromCharCode(

View file

@ -38,6 +38,7 @@ export const paramDef = {
type: "object", type: "object",
properties: { properties: {
noteId: { type: "string", format: "misskey:id" }, noteId: { type: "string", format: "misskey:id" },
userId: { type: "string", format: "misskey:id" },
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
sinceId: { type: "string", format: "misskey:id" }, sinceId: { type: "string", format: "misskey:id" },
untilId: { type: "string", format: "misskey:id" }, untilId: { type: "string", format: "misskey:id" },
@ -52,13 +53,19 @@ export default define(meta, paramDef, async (ps, user) => {
throw err; throw err;
}); });
const query = makePaginationQuery( let query = makePaginationQuery(
Notes.createQueryBuilder("note"), Notes.createQueryBuilder("note"),
ps.sinceId, ps.sinceId,
ps.untilId, ps.untilId,
) )
.andWhere("note.renoteId = :renoteId", { renoteId: note.id }) .andWhere("note.renoteId = :renoteId", { renoteId: note.id })
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user");
if (ps.userId) {
query.andWhere("user.id = :userId", { userId: ps.userId });
}
query
.leftJoinAndSelect("user.avatar", "avatar") .leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")

View file

@ -7,6 +7,7 @@ import * as fs from "node:fs";
import * as http from "node:http"; import * as http from "node:http";
import Koa from "koa"; import Koa from "koa";
import Router from "@koa/router"; import Router from "@koa/router";
import cors from "@koa/cors";
import mount from "koa-mount"; import mount from "koa-mount";
import koaLogger from "koa-logger"; import koaLogger from "koa-logger";
import * as slow from "koa-slow"; import * as slow from "koa-slow";
@ -41,6 +42,12 @@ app.proxy = true;
app.use(removeTrailingSlash()); app.use(removeTrailingSlash());
app.use(
cors({
origin: "*",
}),
);
if (!["production", "test"].includes(process.env.NODE_ENV || "")) { if (!["production", "test"].includes(process.env.NODE_ENV || "")) {
// Logger // Logger
app.use( app.use(

View file

@ -67,7 +67,7 @@
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"swiper": "^8.4.5", "swiper": "9.3.2",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.146.0", "three": "0.146.0",

View file

@ -111,11 +111,12 @@ function onMousedown(evt: MouseEvent): void {
z-index: 1; // box-shadow z-index: 1; // box-shadow
display: block; display: block;
min-width: 100px; min-width: 100px;
min-height: 35px;
width: max-content; width: max-content;
padding: 8px 16px; padding: 8px 16px;
text-align: center; text-align: center;
font-weight: normal; font-weight: normal;
font-size: 1em; font-size: max(12px, 1em);
box-shadow: none; box-shadow: none;
text-decoration: none; text-decoration: none;
background: var(--buttonBg); background: var(--buttonBg);
@ -193,7 +194,7 @@ function onMousedown(evt: MouseEvent): void {
&.mini { &.mini {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.9em; font-size: max(12px, 0.9em);
border-radius: 100px; border-radius: 100px;
} }

View file

@ -1,6 +1,7 @@
<template> <template>
<button <button
class="kpoogebi _button" v-if="$i != null && $i.id != user.id"
class="kpoogebi _button follow-button"
:class="{ :class="{
wait, wait,
active: isFollowing || hasPendingFollowRequestFromYou, active: isFollowing || hasPendingFollowRequestFromYou,
@ -10,40 +11,43 @@
}" }"
:disabled="wait" :disabled="wait"
@click="onClick" @click="onClick"
:aria-label="`${state} ${user.name || user.username}`"
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="isBlocking"> <template v-if="isBlocking">
<span v-if="full">{{ i18n.ts.blocked }}</span <span v-if="full">{{ (state = i18n.ts.blocked) }}</span
><i class="ph-prohibit ph-bold ph-lg"></i> ><i class="ph-prohibit ph-bold ph-lg"></i>
</template> </template>
<template <template
v-else-if="hasPendingFollowRequestFromYou && user.isLocked" v-else-if="hasPendingFollowRequestFromYou && user.isLocked"
> >
<span v-if="full">{{ i18n.ts.followRequestPending }}</span <span v-if="full">{{
(state = i18n.ts.followRequestPending)
}}</span
><i class="ph-hourglass-medium ph-bold ph-lg"></i> ><i class="ph-hourglass-medium ph-bold ph-lg"></i>
</template> </template>
<template <template
v-else-if="hasPendingFollowRequestFromYou && !user.isLocked" v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"
> >
<!-- つまりリモートフォローの場合 --> <!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ i18n.ts.processing }}</span <span v-if="full">{{ (state = i18n.ts.processing) }}</span
><i class="ph-circle-notch ph-bold ph-lg fa-pulse"></i> ><i class="ph-circle-notch ph-bold ph-lg fa-pulse"></i>
</template> </template>
<template v-else-if="isFollowing"> <template v-else-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span <span v-if="full">{{ (state = i18n.ts.unfollow) }}</span
><i class="ph-minus ph-bold ph-lg"></i> ><i class="ph-minus ph-bold ph-lg"></i>
</template> </template>
<template v-else-if="!isFollowing && user.isLocked"> <template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequest }}</span <span v-if="full">{{ (state = i18n.ts.followRequest) }}</span
><i class="ph-plus ph-bold ph-lg"></i> ><i class="ph-plus ph-bold ph-lg"></i>
</template> </template>
<template v-else-if="!isFollowing && !user.isLocked"> <template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ i18n.ts.follow }}</span <span v-if="full">{{ (state = i18n.ts.follow) }}</span
><i class="ph-plus ph-bold ph-lg"></i> ><i class="ph-plus ph-bold ph-lg"></i>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span <span v-if="full">{{ (state = i18n.ts.processing) }}</span
><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i> ><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
</template> </template>
</button> </button>
@ -55,6 +59,7 @@ import type * as Misskey from "calckey-js";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { $i } from "@/account";
const emit = defineEmits(["refresh"]); const emit = defineEmits(["refresh"]);
const props = withDefaults( const props = withDefaults(
@ -71,6 +76,8 @@ const props = withDefaults(
const isBlocking = computed(() => props.user.isBlocking); const isBlocking = computed(() => props.user.isBlocking);
let state = $ref(i18n.ts.processing);
let isFollowing = $ref(props.user.isFollowing); let isFollowing = $ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = $ref( let hasPendingFollowRequestFromYou = $ref(
props.user.hasPendingFollowRequestFromYou props.user.hasPendingFollowRequestFromYou
@ -155,7 +162,7 @@ onBeforeUnmount(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.kpoogebi { .follow-button {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -164,13 +171,15 @@ onBeforeUnmount(() => {
color: var(--accent); color: var(--accent);
border: solid 1px var(--accent); border: solid 1px var(--accent);
padding: 0; padding: 0;
height: 31px;
font-size: 16px; font-size: 16px;
border-radius: 32px; width: 2em;
height: 2em;
border-radius: 100px;
background: var(--bg); background: var(--bg);
&.full { &.full {
padding: 0 8px 0 12px; padding: 0.2em 0.7em;
width: auto;
font-size: 14px; font-size: 14px;
} }
@ -207,7 +216,7 @@ onBeforeUnmount(() => {
} }
&.active { &.active {
color: #fff; color: var(--fgOnAccent);
background: var(--accent); background: var(--accent);
&:hover { &:hover {

View file

@ -1,5 +1,6 @@
<template> <template>
<div <div
:aria-label="accessibleLabel"
v-if="!muted.muted" v-if="!muted.muted"
v-show="!isDeleted" v-show="!isDeleted"
ref="el" ref="el"
@ -85,6 +86,7 @@
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
@push="(e) => router.push(notePage(e))" @push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()" @focusfooter="footerEl.focus()"
@expanded="(e) => setPostExpanded(e)"
></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 />
@ -472,6 +474,39 @@ function readPromo() {
isDeleted.value = true; isDeleted.value = true;
} }
let postIsExpanded = ref(false);
function setPostExpanded(val: boolean) {
postIsExpanded.value = val;
}
const accessibleLabel = computed(() => {
let label = `${props.note.user.username}; `;
if (props.note.renote) {
label += `${i18n.t("renoted")} ${props.note.renote.user.username}; `;
if (props.note.renote.cw) {
label += `${i18n.t("cw")}: ${props.note.renote.cw}; `;
if (postIsExpanded.value) {
label += `${props.note.renote.text}; `;
}
} else {
label += `${props.note.renote.text}; `;
}
} else {
if (props.note.cw) {
label += `${i18n.t("cw")}: ${props.note.cw}; `;
if (postIsExpanded.value) {
label += `${props.note.text}; `;
}
} else {
label += `${props.note.text}; `;
}
}
const date = new Date(props.note.createdAt);
label += `${date.toLocaleTimeString()}`;
return label;
});
defineExpose({ defineExpose({
focus, focus,
blur, blur,

View file

@ -35,38 +35,30 @@
<MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab"> <MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab">
<option value="replies"> <option value="replies">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <!-- <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> -->
<template v-if="appearNote.repliesCount > 0"> <span v-if="appearNote.repliesCount > 0" class="count">{{
<span class="count">{{ appearNote.repliesCount }}</span> appearNote.repliesCount
</template> }}</span>
{{ i18n.ts._notification._types.reply }} {{ i18n.ts._notification._types.reply }}
</option> </option>
<option value="renotes"> <option value="renotes" v-if="appearNote.renoteCount > 0">
<i class="ph-repeat ph-bold ph-lg"></i> <!-- <i class="ph-repeat ph-bold ph-lg"></i> -->
<template v-if="appearNote.renoteCount > 0"> <span class="count">{{ appearNote.renoteCount }}</span>
<span class="count">{{ appearNote.renoteCount }}</span>
</template>
{{ i18n.ts._notification._types.renote }} {{ i18n.ts._notification._types.renote }}
</option> </option>
<option value="quotes"> <option value="reactions" v-if="reactionsCount > 0">
<i class="ph-quotes ph-bold ph-lg"></i> <!-- <i class="ph-smiley ph-bold ph-lg"></i> -->
<template v-if="directQuotes?.length > 0"> <span class="count">{{ reactionsCount }}</span>
<span class="count">{{ directQuotes.length }}</span>
</template>
{{ i18n.ts._notification._types.quote }}
</option>
<option value="reactions">
<i class="ph-smiley ph-bold ph-lg"></i>
<template v-if="reactionsCount > 0">
<span class="count">{{ reactionsCount }}</span>
</template>
{{ i18n.ts.reaction }} {{ i18n.ts.reaction }}
</option> </option>
<option value="clips"> <option value="quotes" v-if="directQuotes?.length > 0">
<i class="ph-paperclip ph-bold ph-lg"></i> <!-- <i class="ph-quotes ph-bold ph-lg"></i> -->
<template v-if="clips?.length > 0"> <span class="count">{{ directQuotes.length }}</span>
<span class="count">{{ clips.length }}</span> {{ i18n.ts._notification._types.quote }}
</template> </option>
<option value="clips" v-if="clips?.length > 0">
<!-- <i class="ph-paperclip ph-bold ph-lg"></i> -->
<span class="count">{{ clips.length }}</span>
{{ i18n.ts.clips }} {{ i18n.ts.clips }}
</option> </option>
</MkTab> </MkTab>
@ -518,11 +510,15 @@ onUnmounted(() => {
overflow: clip; overflow: clip;
outline: none; outline: none;
scroll-margin-top: calc(var(--stickyTop) + 20vh); scroll-margin-top: calc(var(--stickyTop) + 20vh);
&:not(:last-child) {
border-bottom: 1px solid var(--divider);
margin-bottom: 4px;
}
.article { .article {
cursor: unset; cursor: unset;
padding-bottom: 0; padding-bottom: 0;
} }
&:first-of-type { &:first-child {
padding-top: 28px; padding-top: 28px;
} }
} }
@ -632,7 +628,7 @@ onUnmounted(() => {
} }
> :deep(.note-container) { > :deep(.note-container) {
padding: 6px 0 0 0; padding: 12px 0 0 0;
> .header > .body { > .header > .body {
padding-left: 10px; padding-left: 10px;
} }
@ -642,7 +638,7 @@ onUnmounted(() => {
> :deep(.reacted-users > *) { > :deep(.reacted-users > *) {
padding-inline: 16px !important; padding-inline: 16px !important;
} }
> .chips { > :deep(.underline) {
padding-left: 16px !important; padding-left: 16px !important;
} }
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<div <article
v-if="!muted.muted || muted.what === 'reply'" v-if="!muted.muted || muted.what === 'reply'"
ref="el" ref="el"
v-size="{ max: [450, 500] }" v-size="{ max: [450, 500] }"
@ -150,7 +150,7 @@
></MkA> ></MkA>
</div> </div>
</template> </template>
</div> </article>
<div v-else class="muted" @click="muted.muted = false"> <div v-else class="muted" @click="muted.muted = false">
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small"> <I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
<template #name> <template #name>

View file

@ -69,11 +69,11 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
const renotes = await os.api("notes/renotes", { const renotes = await os.api("notes/renotes", {
noteId: props.note.id, noteId: props.note.id,
limit: 11, userId: $i.id,
limit: 1,
}); });
const users = renotes.map((x) => x.user.id); const hasRenotedBefore = renotes.length > 0;
const hasRenotedBefore = users.includes($i.id);
let buttonActions: Array<MenuItem> = []; let buttonActions: Array<MenuItem> = [];

View file

@ -17,7 +17,7 @@
!note.replyId !note.replyId
" "
:to="`/notes/${note.renoteId}`" :to="`/notes/${note.renoteId}`"
v-tooltip="i18n.ts.jumpToReply" v-tooltip="i18n.ts.jumpToPrevious"
class="reply-icon" class="reply-icon"
@click.stop @click.stop
> >
@ -54,11 +54,12 @@
v-model="showContent" v-model="showContent"
:note="note" :note="note"
v-on:keydown="focusFooter" v-on:keydown="focusFooter"
v-on:update:model-value="(val) => emit('expanded', val)"
/> />
<div <div
class="body" class="body"
v-bind="{ v-bind="{
'aria-hidden': !showContent ? 'true' : null, 'aria-hidden': note.cw && !showContent ? 'true' : null,
tabindex: !showContent ? '-1' : null, tabindex: !showContent ? '-1' : null,
}" }"
> >
@ -70,7 +71,7 @@
v-if="!detailed && note.replyId" v-if="!detailed && note.replyId"
:to="`#${note.replyId}`" :to="`#${note.replyId}`"
behavior="browser" behavior="browser"
v-tooltip="i18n.ts.jumpToReply" v-tooltip="i18n.ts.jumpToPrevious"
class="reply-icon" class="reply-icon"
@click.stop @click.stop
> >
@ -190,6 +191,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "push", v): void; (ev: "push", v): void;
(ev: "focusfooter"): void; (ev: "focusfooter"): void;
(ev: "expanded", v): void;
}>(); }>();
const cwButton = ref<HTMLElement>(); const cwButton = ref<HTMLElement>();

View file

@ -16,9 +16,10 @@ export default defineComponent({
return h( return h(
"div", "div",
{ {
class: ["pxhvhrfw", class: [
"pxhvhrfw",
{ chips: this.style === "chips" }, { chips: this.style === "chips" },
{ underline: this.style === "underline" } { underline: this.style === "underline" },
], ],
role: "tablist", role: "tablist",
}, },
@ -86,9 +87,14 @@ export default defineComponent({
> .icon { > .icon {
margin-right: 6px; margin-right: 6px;
} }
&:empty {
display: none !important;
}
} }
&.chips, &.underline { &.chips,
&.underline {
padding: 12px 32px; padding: 12px 32px;
font-size: 0.85em; font-size: 0.85em;
overflow-x: auto; overflow-x: auto;
@ -124,6 +130,7 @@ export default defineComponent({
&.underline { &.underline {
padding-block: 0 !important; padding-block: 0 !important;
margin-bottom: -1px; margin-bottom: -1px;
border-radius: 0;
button { button {
background: none !important; background: none !important;
border-radius: 0 !important; border-radius: 0 !important;

View file

@ -299,6 +299,15 @@ const props = withDefaults(
filter: hue-rotate(360deg) contrast(150%) saturate(150%); filter: hue-rotate(360deg) contrast(150%) saturate(150%);
} }
} }
@keyframes mfm-fade {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -3,31 +3,32 @@
v-if="show" v-if="show"
ref="el" ref="el"
class="fdidabkb" class="fdidabkb"
:class="{ slim: narrow, thin: thin_ }" :class="{ thin: thin_, tabs: tabs?.length > 0 }"
:style="{ background: bg }" :style="{ background: bg }"
@click="onClick" @click="onClick"
> >
<button <div class="left">
v-if="props.displayBackButton" <div class="buttons">
class="_button button icon backButton" <button
@click.stop="goBack()" v-if="props.displayBackButton"
@touchstart="preventDrag" class="_button button icon backButton"
v-tooltip.noDelay="i18n.ts.goBack" @click.stop="goBack()"
> @touchstart="preventDrag"
<i class="ph-caret-left ph-bold ph-lg"></i> v-tooltip.noDelay="i18n.ts.goBack"
</button> >
<div v-if="narrow" class="buttons left" @click="openAccountMenu"> <i class="ph-caret-left ph-bold ph-lg"></i>
<MkAvatar </button>
v-if="props.displayMyAvatar && $i" <MkAvatar
class="avatar" v-if="narrow && props.displayMyAvatar && $i"
:user="$i" class="avatar button"
:disable-preview="true" :user="$i"
disableLink :disable-preview="true"
/> disableLink
</div> @click.stop="openAccountMenu"
<template v-if="metadata"> />
</div>
<div <div
v-if="!hideTitle" v-if="!hideTitle && metadata"
class="titleContainer" class="titleContainer"
@click="showTabsPopup" @click="showTabsPopup"
> >
@ -66,7 +67,14 @@
</div> </div>
</div> </div>
</div> </div>
<nav ref="tabsEl" v-if="hasTabs" class="tabs"> </div>
<template v-if="metadata">
<nav
ref="tabsEl"
v-if="hasTabs"
class="tabs"
:class="{ collapse: hasTabs && tabs.length > 3 }"
>
<button <button
v-for="tab in tabs" v-for="tab in tabs"
:ref="(el) => (tabRefs[tab.key] = el)" :ref="(el) => (tabRefs[tab.key] = el)"
@ -85,6 +93,20 @@
</nav> </nav>
</template> </template>
<div class="buttons right"> <div class="buttons right">
<template v-if="metadata.avatar">
<MkFollowButton
v-if="narrow"
:user="metadata.avatar"
:full="false"
class="fullButton"
></MkFollowButton>
<MkFollowButton
v-else
:user="metadata.avatar"
:full="true"
class="fullButton"
></MkFollowButton>
</template>
<template v-for="action in actions"> <template v-for="action in actions">
<button <button
v-tooltip.noDelay="action.text" v-tooltip.noDelay="action.text"
@ -112,7 +134,7 @@ import {
nextTick, nextTick,
reactive, reactive,
} from "vue"; } from "vue";
import tinycolor from "tinycolor2"; import MkFollowButton from "@/components/MkFollowButton.vue";
import { popupMenu } from "@/os"; import { popupMenu } from "@/os";
import { scrollToTop } from "@/scripts/scroll"; import { scrollToTop } from "@/scripts/scroll";
import { globalEvents } from "@/events"; import { globalEvents } from "@/events";
@ -223,25 +245,9 @@ function goBack(): void {
window.history.back(); window.history.back();
} }
const calcBg = () => {
const rawBg = metadata?.bg || "var(--bg)";
const tinyBg = tinycolor(
rawBg.startsWith("var(")
? getComputedStyle(document.documentElement).getPropertyValue(
rawBg.slice(4, -1)
)
: rawBg
);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
};
let ro: ResizeObserver | null; let ro: ResizeObserver | null;
onMounted(() => { onMounted(() => {
calcBg();
globalEvents.on("themeChanged", calcBg);
watch( watch(
() => [props.tab, props.tabs], () => [props.tab, props.tabs],
() => { () => {
@ -251,17 +257,15 @@ onMounted(() => {
// offsetWidth offsetLeft getBoundingClientRect 使 // offsetWidth offsetLeft getBoundingClientRect 使
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 // https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
const tabSizeX = tabEl.scrollWidth + 20; // + the tab's padding const tabSizeX = tabEl.scrollWidth + 20; // + the tab's padding
tabEl.style = `--width: ${tabSizeX}px`; if (props.tabs.length > 3) {
tabEl.style = `--width: ${tabSizeX}px`;
}
setTimeout(() => { setTimeout(() => {
const parentRect = tabsEl.getBoundingClientRect();
const rect = tabEl.getBoundingClientRect();
const left =
rect.left - parentRect.left + tabsEl?.scrollLeft;
tabHighlightEl.style.width = tabSizeX + "px"; tabHighlightEl.style.width = tabSizeX + "px";
tabHighlightEl.style.transform = `translateX(${left}px)`; tabHighlightEl.style.transform = `translateX(${tabEl.offsetLeft}px)`;
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
tabsEl?.scrollTo({ tabsEl?.scrollTo({
left: left - 60, left: tabEl.offsetLeft - 60,
behavior: "smooth", behavior: "smooth",
}); });
}); });
@ -286,7 +290,6 @@ onMounted(() => {
}); });
onUnmounted(() => { onUnmounted(() => {
globalEvents.off("themeChanged", calcBg);
if (ro) ro.disconnect(); if (ro) ro.disconnect();
}); });
</script> </script>
@ -295,94 +298,96 @@ onUnmounted(() => {
.fdidabkb { .fdidabkb {
--height: 55px; --height: 55px;
display: flex; display: flex;
justify-content: space-between;
width: 100%; width: 100%;
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
border-bottom: solid 0.5px var(--divider);
height: var(--height); height: var(--height);
padding-inline: 24px;
box-sizing: border-box;
overflow: hidden;
@media (max-width: 500px) {
padding-inline: 12p;
}
@media (max-width: 700px) {
> .left {
min-width: unset !important;
max-width: 40%;
}
> .left,
> .right {
flex: unset !important;
}
&:not(.tabs) {
> .left {
width: 0 !important;
flex-grow: 1 !important;
max-width: unset !important;
}
}
&.tabs {
> .left {
flex-shrink: 0 !important;
}
.buttons ~ .titleContainer > .title {
display: none;
}
}
}
&::before {
content: "";
position: absolute;
inset: 0;
border-bottom: solid 0.5px var(--divider);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
z-index: -1;
}
&::after {
content: "";
position: absolute;
inset: 0;
background: var(--bg);
opacity: 0.85;
z-index: -2;
}
&.thin { &.thin {
--height: 45px; --height: 45px;
> .buttons { .buttons {
> .button { > .button {
font-size: 0.9em; font-size: 0.9em;
} }
} }
} }
&.slim { > .left {
> .titleContainer { display: flex;
flex: 1; > .buttons {
margin: 0 auto; &:not(:empty) {
margin-left: calc(0px - var(--margin));
> *:first-child {
margin-left: auto;
} }
> .avatar {
> *:last-child { width: 32px;
margin-right: auto; height: 32px;
} margin-left: var(--margin);
}
> .tabs {
padding-inline: 12px;
mask: linear-gradient(
to right,
transparent,
black 10px 80%,
transparent
);
-webkit-mask: linear-gradient(
to right,
transparent,
black 10px 80%,
transparent
);
margin-left: -10px;
padding-left: 22px;
scrollbar-width: none;
&::before {
content: unset;
}
&::-webkit-scrollbar {
display: none;
}
&::after {
// Force right padding
content: "";
display: inline-block;
min-width: 20%;
} }
} }
} }
> .buttons { .buttons {
--margin: 8px; --margin: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
height: var(--height); height: var(--height);
margin: 0 var(--margin);
&.left {
margin-right: auto;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
}
&.right { &.right {
margin-left: auto; justify-content: flex-end;
} // margin-right: calc(0px - var(--margin));
// margin-left: var(--margin);
&:empty { > .button:last-child {
display: none; margin-right: calc(0px - var(--margin));
}
} }
> .button/*, @at-root .backButton*/ { > .button/*, @at-root .backButton*/ {
@ -412,85 +417,117 @@ onUnmounted(() => {
} }
} }
> .backButton { > .left {
display: flex; > .backButton {
align-items: center; display: flex;
justify-content: center; align-items: center;
margin-left: 1rem; justify-content: center;
}
> .titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
margin-right: 1rem;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
} }
> .titleContainer {
> .icon { display: flex;
margin-right: 8px; align-items: center;
width: 16px; max-width: 400px;
text-align: center; overflow: auto;
transform: translate(0em);
}
> .title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
line-height: 1.1; text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-right: 1rem;
> .subtitle { > .avatar {
opacity: 0.6; $size: 32px;
font-size: 0.8em; display: inline-block;
font-weight: normal; width: $size;
white-space: nowrap; height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .icon {
margin-right: 8px;
width: 16px;
text-align: center;
}
> .title {
min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
&.activeTab { > .subtitle {
text-align: center; opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> .chevron { &.activeTab {
display: inline-block; text-align: center;
margin-left: 6px;
> .chevron {
display: inline-block;
margin-left: 6px;
}
} }
} }
} }
} }
} }
> .left,
> .right {
flex-basis: 100%;
flex-shrink: 9999;
overflow: hidden;
}
> .left {
min-width: 20%;
margin-left: -10px;
padding-left: 10px;
}
> .right {
// margin-left: auto;
min-width: max-content;
margin-right: -10px;
padding-right: 10px;
}
> .tabs { > .tabs {
position: relative; position: relative;
width: 100%;
font-size: 1em; font-size: 1em;
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
contain: strict; contain: content;
display: flex;
padding-inline: 20px;
margin-inline: -20px;
mask: linear-gradient(
to right,
transparent,
black 20px calc(100% - 20px),
transparent
);
-webkit-mask: linear-gradient(
to right,
transparent,
black 20px calc(100% - 20px),
transparent
);
scrollbar-width: none;
&::before { &.collapse {
content: ""; --width: 38px;
display: inline-block; > .tab {
height: 40%; width: 38px;
border-left: 1px solid var(--divider); min-width: 38px !important;
margin-right: 1em; &:not(.active) > .title {
margin-left: 10px; opacity: 0;
vertical-align: -1px; }
}
} }
> .tab { > .tab {
@ -499,12 +536,12 @@ onUnmounted(() => {
position: relative; position: relative;
border-inline: 10px solid transparent; border-inline: 10px solid transparent;
height: 100%; height: 100%;
min-width: max-content;
font-weight: normal; font-weight: normal;
opacity: 0.7; opacity: 0.7;
width: 38px;
--width: 38px;
overflow: hidden; overflow: hidden;
transition: color 0.2s, opacity 0.2s, width 0.2s; transition: color 0.2s, opacity 0.2s, width 0.2s, min-width 0.2s;
--width: max-content;
&:hover { &:hover {
opacity: 1; opacity: 1;
@ -515,9 +552,7 @@ onUnmounted(() => {
color: var(--accent); color: var(--accent);
font-weight: 600; font-weight: 600;
width: var(--width); width: var(--width);
} min-width: var(--width) !important;
&:not(.active) > .title {
opacity: 0;
} }
> .icon + .title { > .icon + .title {
@ -527,7 +562,6 @@ onUnmounted(() => {
transition: opacity 0.2s; transition: opacity 0.2s;
} }
} }
> .highlight { > .highlight {
position: absolute; position: absolute;
bottom: 0; bottom: 0;

View file

@ -156,6 +156,14 @@ export default defineComponent({
} }
return h(MkSparkle, {}, genEl(token.children)); return h(MkSparkle, {}, genEl(token.children));
} }
case "fade": {
const direction = token.props.args.out
? "alternate-reverse"
: "alternate";
const speed = validTime(token.props.args.speed) || "1.5s";
style = `animation: mfm-fade ${speed} linear infinite; animation-direction: ${direction};`;
break;
}
case "flip": { case "flip": {
const transform = const transform =
token.props.args.h && token.props.args.v token.props.args.h && token.props.args.v

View file

@ -353,6 +353,18 @@
</div> </div>
</div> </div>
</div> </div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.fade }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.fadeDescription }}</p>
<div class="preview">
<Mfm :text="preview_fade" />
<MkTextarea v-model="preview_fade"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block"> <div class="section _block">
<div class="title">{{ i18n.ts._mfm.position }}</div> <div class="title">{{ i18n.ts._mfm.position }}</div>
<div class="content"> <div class="content">
@ -479,6 +491,7 @@ 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>"
); );
let preview_fade = $ref("$[fade 🍮] $[fade.out 🍮] $[fade.speed=5s 🍮]");
definePageMetadata({ definePageMetadata({
title: i18n.ts._mfm.cheatSheet, title: i18n.ts._mfm.cheatSheet,

View file

@ -27,11 +27,10 @@
v-if="!showNext && hasNext" v-if="!showNext && hasNext"
class="load next" class="load next"
@click="showNext = true" @click="showNext = true"
v-tooltip=" >
`${i18n.ts.loadMore} (${i18n.ts.newer})` <i class="ph-caret-up ph-bold ph-lg"></i>
" {{ `${i18n.ts.loadMore} (${i18n.ts.newer})` }}
><i class="ph-caret-up ph-bold ph-lg"></i </MkButton>
></MkButton>
<div class="note _gap"> <div class="note _gap">
<MkRemoteCaution <MkRemoteCaution
v-if="note.user.host != null" v-if="note.user.host != null"
@ -47,11 +46,10 @@
v-if="!showPrev && hasPrev" v-if="!showPrev && hasPrev"
class="load prev" class="load prev"
@click="showPrev = true" @click="showPrev = true"
v-tooltip=" >
`${i18n.ts.loadMore} (${i18n.ts.older})` <i class="ph-caret-down ph-bold ph-lg"></i>
" {{ `${i18n.ts.loadMore} (${i18n.ts.older})` }}
><i class="ph-caret-down ph-bold ph-lg"></i </MkButton>
></MkButton>
</div> </div>
<div v-if="showPrev" class="_gap"> <div v-if="showPrev" class="_gap">

View file

@ -41,6 +41,7 @@
<MkInfo <MkInfo
v-if="user.username.includes('.')" v-if="user.username.includes('.')"
class="_formBlock" class="_formBlock"
warn
>{{ i18n.ts.isSystemAccount }}</MkInfo >{{ i18n.ts.isSystemAccount }}</MkInfo
> >

View file

@ -135,7 +135,6 @@
<div class="follow-container"> <div class="follow-container">
<div class="actions"> <div class="actions">
<MkFollowButton <MkFollowButton
v-if="$i != null && $i.id != user.id"
:user="user" :user="user"
@refresh="emit('refresh')" @refresh="emit('refresh')"
:inline="true" :inline="true"

View file

@ -199,6 +199,11 @@ export const routes = [
name: "api", name: "api",
component: page(() => import("./pages/settings/api.vue")), component: page(() => import("./pages/settings/api.vue")),
}, },
{
path: "/apps",
name: "apps",
component: page(() => import("./pages/settings/apps.vue")),
},
{ {
path: "/webhook/edit/:webhookId", path: "/webhook/edit/:webhookId",
name: "webhook", name: "webhook",

View file

@ -9,6 +9,7 @@ const animatedMfm = [
"jump", "jump",
"bounce", "bounce",
"rainbow", "rainbow",
"fade",
]; ];
export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] { export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] {

View file

@ -19,4 +19,5 @@ export const MFM_TAGS = [
"rainbow", "rainbow",
"sparkle", "sparkle",
"rotate", "rotate",
"fade",
]; ];

View file

@ -103,7 +103,7 @@ body::-webkit-scrollbar-thumb {
} }
html._themeChanging_ { html._themeChanging_ {
&, * { &, *, ::before, ::after {
transition: background 1s ease, border 1s ease !important; transition: background 1s ease, border 1s ease !important;
} }
} }

View file

@ -402,7 +402,6 @@ function more(ev: MouseEvent) {
position: relative; position: relative;
width: 32px; width: 32px;
margin-right: 8px; margin-right: 8px;
transform: translateY(0.15em);
} }
> .indicator { > .indicator {
@ -524,7 +523,6 @@ function more(ev: MouseEvent) {
> .icon { > .icon {
position: relative; position: relative;
color: var(--fgOnAccent); color: var(--fgOnAccent);
transform: translate(0.15em, 0em);
} }
> .text { > .text {

View file

@ -111,6 +111,7 @@
<div v-if="isMobile" class="buttons"> <div v-if="isMobile" class="buttons">
<button <button
:aria-label="i18n.t('menu')"
class="button nav _button" class="button nav _button"
@click="drawerMenuShowing = true" @click="drawerMenuShowing = true"
> >
@ -119,10 +120,15 @@
><i class="ph-circle ph-fill"></i ><i class="ph-circle ph-fill"></i
></span> ></span>
</button> </button>
<button class="button home _button" @click="mainRouter.push('/')"> <button
:aria-label="i18n.t('home')"
class="button home _button"
@click="mainRouter.push('/')"
>
<i class="ph-house ph-bold ph-lg"></i> <i class="ph-house ph-bold ph-lg"></i>
</button> </button>
<button <button
:aria-label="i18n.t('notifications')"
class="button notifications _button" class="button notifications _button"
@click="mainRouter.push('/my/notifications')" @click="mainRouter.push('/my/notifications')"
> >
@ -131,7 +137,11 @@
><i class="ph-circle ph-fill"></i ><i class="ph-circle ph-fill"></i
></span> ></span>
</button> </button>
<button class="button post _button" @click="os.post()"> <button
:aria-label="i18n.t('note')"
class="button post _button"
@click="os.post()"
>
<i class="ph-pencil ph-bold ph-lg"></i> <i class="ph-pencil ph-bold ph-lg"></i>
</button> </button>
</div> </div>

View file

@ -36,6 +36,7 @@
<div v-if="isMobile" class="buttons"> <div v-if="isMobile" class="buttons">
<button <button
:aria-label="i18n.t('menu')"
class="button nav _button" class="button nav _button"
@click="drawerMenuShowing = true" @click="drawerMenuShowing = true"
> >
@ -47,6 +48,7 @@
</div> </div>
</button> </button>
<button <button
:aria-label="i18n.t('home')"
class="button home _button" class="button home _button"
@click=" @click="
mainRouter.currentRoute.value.name === 'index' mainRouter.currentRoute.value.name === 'index'
@ -63,6 +65,7 @@
</div> </div>
</button> </button>
<button <button
:aria-label="i18n.t('notifications')"
class="button notifications _button" class="button notifications _button"
@click=" @click="
mainRouter.push('/my/notifications'); mainRouter.push('/my/notifications');
@ -80,6 +83,7 @@
</div> </div>
</button> </button>
<button <button
:aria-label="i18n.t('messaging')"
class="button messaging _button" class="button messaging _button"
@click=" @click="
mainRouter.push('/my/messaging'); mainRouter.push('/my/messaging');
@ -99,6 +103,7 @@
</div> </div>
</button> </button>
<button <button
:aria-label="i18n.t('_deck._columns.widgets')"
class="button widget _button" class="button widget _button"
@click="widgetsShowing = true" @click="widgetsShowing = true"
> >
@ -111,6 +116,7 @@
<button <button
v-if="isMobile && mainRouter.currentRoute.value.name === 'index'" v-if="isMobile && mainRouter.currentRoute.value.name === 'index'"
ref="postButton" ref="postButton"
:aria-label="i18n.t('note')"
class="postButton button post _button" class="postButton button post _button"
@click="os.post()" @click="os.post()"
> >
@ -122,6 +128,7 @@
" "
ref="postButton" ref="postButton"
class="postButton button post _button" class="postButton button post _button"
:aria-label="i18n.t('startMessaging')"
@click="messagingStart" @click="messagingStart"
> >
<i class="ph-user-plus ph-bold ph-lg"></i> <i class="ph-user-plus ph-bold ph-lg"></i>

View file

@ -264,8 +264,8 @@ importers:
specifier: 7.0.2 specifier: 7.0.2
version: 7.0.2(@types/koa@2.13.5)(ejs@3.1.8)(pug@3.0.2) version: 7.0.2(@types/koa@2.13.5)(ejs@3.1.8)(pug@3.0.2)
mfm-js: mfm-js:
specifier: 0.23.2 specifier: 0.23.3
version: 0.23.2 version: 0.23.3
mime-types: mime-types:
specifier: 2.1.35 specifier: 2.1.35
version: 2.1.35 version: 2.1.35
@ -840,8 +840,8 @@ importers:
specifier: 2.1.0 specifier: 2.1.0
version: 2.1.0 version: 2.1.0
swiper: swiper:
specifier: ^8.4.5 specifier: 9.3.2
version: 8.4.5 version: 9.3.2
syuilo-password-strength: syuilo-password-strength:
specifier: 0.0.1 specifier: 0.0.1
version: 0.0.1 version: 0.0.1
@ -6411,12 +6411,6 @@ packages:
domhandler: 5.0.3 domhandler: 5.0.3
entities: 4.4.0 entities: 4.4.0
/dom7@4.0.4:
resolution: {integrity: sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==}
dependencies:
ssr-window: 4.0.2
dev: true
/domelementtype@1.3.1: /domelementtype@1.3.1:
resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==}
dev: false dev: false
@ -10424,17 +10418,10 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: false dev: false
/mfm-js@0.23.2:
resolution: {integrity: sha512-lfYvsMr6FIYbt0ZDL+nY+GWWqmcXpe9jrYLBLy5vvQHwGfPALpx43uNHj8hZsakgM82hPMo/zdx0e9tj+4Z4IA==}
dependencies:
twemoji-parser: 14.0.0
dev: false
/mfm-js@0.23.3: /mfm-js@0.23.3:
resolution: {integrity: sha512-o8scYmbey6rMUmWAlT3k3ntt6khaCLdxlmHhAWV5wTTMj2OK1atQvZfRUq0SIVm1Jig08qlZg/ps71xUqrScNA==} resolution: {integrity: sha512-o8scYmbey6rMUmWAlT3k3ntt6khaCLdxlmHhAWV5wTTMj2OK1atQvZfRUq0SIVm1Jig08qlZg/ps71xUqrScNA==}
dependencies: dependencies:
twemoji-parser: 14.0.0 twemoji-parser: 14.0.0
dev: true
/micromatch@3.1.10: /micromatch@3.1.10:
resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==}
@ -13594,12 +13581,10 @@ packages:
webpack: 5.75.0(@swc/core@1.3.50)(webpack-cli@5.0.1) webpack: 5.75.0(@swc/core@1.3.50)(webpack-cli@5.0.1)
dev: true dev: true
/swiper@8.4.5: /swiper@9.3.2:
resolution: {integrity: sha512-zveyEFBBv4q1sVkbJHnuH4xCtarKieavJ4SxP0QEHvdpPLJRuD7j/Xg38IVVLbp7Db6qrPsLUePvxohYx39Agw==} resolution: {integrity: sha512-Kj9Z4kXRmJR3YT/Wj+XLWj8P6IcRt+WG38uL8M3/Wny7+6sV0TlP9vnE1X+Co9c7VzNooojWGnFa+Wf/9+CUMA==}
engines: {node: '>= 4.7.0'} engines: {node: '>= 4.7.0'}
requiresBuild: true
dependencies: dependencies:
dom7: 4.0.4
ssr-window: 4.0.2 ssr-window: 4.0.2
dev: true dev: true