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)
- Web Proxy (one of the following)
- 🍀 Nginx (recommended)
- 🪶 Apache
- 🦦 Caddy
- 🪶 Apache
### 😗 Optional dependencies
@ -107,7 +107,8 @@ git clone --depth 1 https://codeberg.org/calckey/calckey.git
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
@ -128,11 +129,18 @@ npm i -g pm2
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
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
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)
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.
@ -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 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
- Add the following block to your `Caddyfile`, replacing `example.tld` with your own domain:
@ -194,6 +196,15 @@ example.tld {
```
- 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!
### 🐢 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
useSystemFont: Fes servir la font per defecte del sistema
contact: Contacte
clips: Clips
clips: Retalls
experimentalFeatures: Característiques experimentals
developer: Desenvolupador
makeExplorableDescription: Si desactives aquesta funció el teu compte no sortirà a
@ -1496,7 +1496,7 @@ gallery: Galeria
popularPosts: Pàgines populars
shareWithNote: Comparteix amb una publicació
expiration: Data límit
memo: Memo
memo: Recordatori
priority: Prioritat
high: Alta
middle: Mitjana
@ -1537,7 +1537,7 @@ incorrectPassword: Contrasenya incorrecta.
clickToFinishEmailVerification: Feu clic a [{ok}] per completar la verificació del
correu electrònic.
overridedDeviceKind: Tipus de dispositiu
smartphone: Smartphone
smartphone: Telèfon intel·ligent
tablet: Tauleta
auto: Automàtic
recentNHours: Últimes {n} hores
@ -1625,7 +1625,7 @@ customKaTeXMacroDescription: "Configura macros per escriure expressions matemàt
objectStorageRegion: Regió
objectStoragePrefix: Prefix
objectStoragePrefixDesc: Els fitxers es guardaran dins de carpetes amb aquest prefix.
objectStorageEndpoint: Endpoint
objectStorageEndpoint: Extrem
newNoteRecived: Hi han notes noves
sounds: Sons
listen: Escoltar
@ -1704,7 +1704,9 @@ oneWeek: Una setmana
reflectMayTakeTime: Pot trigar una mica a reflectir-se.
thereIsUnresolvedAbuseReportWarning: Hi ha informes sense resoldre.
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
deleteAccount: Suprimeix el compte
document: Documentació
@ -1746,7 +1748,7 @@ reverse: Revés
objectStorageBucket: Cubell
objectStorageBucketDesc: Si us plau específica el nom del cubell que faràs servir
al teu proveïdor.
clip: Clip
clip: Retall
createNew: Crear una nova
optional: Opcional
jumpToSpecifiedDate: Vés a una data concreta
@ -2056,3 +2058,5 @@ newer: Més nou
older: Més antic
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.
jumpToPrevious: Vés a l'anterior
cw: Avís de contingut

View File

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

View File

@ -57,7 +57,7 @@ sendMessage: "Send a message"
copyUsername: "Copy username"
searchUser: "Search for a user"
reply: "Reply"
jumpToReply: "Jump to Reply"
jumpToPrevious: "Jump to previous"
loadMore: "Load more"
showMore: "Show more"
newer: "newer"
@ -69,6 +69,7 @@ followRequestAccepted: "Follow request accepted"
mention: "Mention"
mentions: "Mentions"
directNotes: "Direct messages"
cw: "Content warning"
importAndExport: "Import/Export Data"
import: "Import"
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\
\ or lower."
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"
deleteAccount: "Delete account"
document: "Documentation"
@ -1266,6 +1267,8 @@ _mfm:
sparkleDescription: "Gives content a sparkling particle effect."
rotate: "Rotate"
rotateDescription: "Turns content by a specified angle."
fade: "Fade"
fadeDescription: "Fades content in and out."
position: "Position"
positionDescription: "Move content by a specified amount."
scale: "Scale"

View File

@ -1077,6 +1077,8 @@ _mfm:
sparkle: "Paillettes"
sparkleDescription: "Ajoute un effet scintillant au contenu."
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
MFM.
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."
rainbow: "Arcobaleno"
rotate: "Ruota"
fade: "Dissolvenza"
fadeDescription: "Dissolvenza in entrata e in uscita del contenuto."
_instanceTicker:
none: "Nascondi"
remote: "Mostra solo per gli/le utenti remotə"

View File

@ -13,7 +13,7 @@ password: "Wachtwoord"
forgotPassword: "Wachtwoord vergeten"
fetchingAsApObject: "Ophalen vanuit de Fediverse"
ok: "Ok"
gotIt: "Begrepen"
gotIt: "Begrepen!"
cancel: "Annuleren"
enterUsername: "Voer een gebruikersnaam in"
renotedBy: "Hergedeeld door {user}"
@ -47,12 +47,12 @@ copyContent: "Kopiëren inhoud"
copyLink: "Kopiëren link"
delete: "Verwijderen"
deleteAndEdit: "Verwijderen en bewerken"
deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken?
Je verliest alle reacties, herdelingen en antwoorden erop."
deleteAndEditConfirm: "Weet je zeker dat je deze post wilt verwijderen en dan bewerken?
Je verliest alle reacties, boosts en antwoorden erop."
addToList: "Aan lijst toevoegen"
sendMessage: "Verstuur bericht"
copyUsername: "Kopiëren gebruikersnaam "
searchUser: "Zoeken een gebruiker"
copyUsername: "Gebruikersnaam kopiëren"
searchUser: "Zoek een gebruiker"
reply: "Antwoord"
loadMore: "Laad meer"
showMore: "Toon meer"
@ -68,7 +68,7 @@ export: "Export"
files: "Bestanden"
download: "Downloaden"
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?"
exportRequested: "Je hebt een export aangevraagd. Dit kan een tijdje duren. Het wordt
toegevoegd aan je Drive zodra het is voltooid."
@ -101,13 +101,13 @@ followRequests: "Volgverzoeken"
unfollow: "Ontvolgen"
followRequestPending: "Wachten op goedkeuring volgverzoek"
enterEmoji: "Voer een emoji in"
renote: "Herdelen"
renote: "Boost"
unrenote: "Stop herdelen"
renoted: "Herdeeld"
cantRenote: "Dit bericht kan niet worden herdeeld"
cantReRenote: "Een herdeling kan niet worden herdeeld"
renoted: "Boosted."
cantRenote: "Dit bericht kan niet worden geboost."
cantReRenote: "Een boost kan niet worden geboost."
quote: "Quote"
pinnedNote: "Vastgemaakte notitie"
pinnedNote: "Vastgemaakte post"
pinned: "Vastmaken aan profielpagina"
you: "Jij"
clickToShow: "Klik om te bekijken"
@ -116,7 +116,7 @@ add: "Toevoegen"
reaction: "Reacties"
reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen,
Druk op \"+\" om toe te voegen"
rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen"
rememberNoteVisibility: "Onthoud post zichtbaarheidsinstellingen"
attachCancel: "Verwijder bijlage"
markAsSensitive: "Markeren als 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
kat is."
flagShowTimelineReplies: "Toon antwoorden op de tijdlijn"
flagShowTimelineRepliesDescription: "Als je dit vlag aanzet, toont de tijdlijn ook
antwoorden op andere en niet alleen jouw eigen notities."
flagShowTimelineRepliesDescription: "Als je deze vlag aanzet, toont de tijdlijn ook
antwoorden op andere en niet alleen jouw eigen post."
autoAcceptFollowed: "Accepteer verzoeken om jezelf te volgen vanzelf als je de verzoeker
al volgt"
addAccount: "Account toevoegen"
@ -203,8 +203,8 @@ mutedUsers: "Gedempte gebruikers"
blockedUsers: "Geblokkeerde gebruikers"
noUsers: "Er zijn geen gebruikers."
editProfile: "Bewerk Profiel"
noteDeleteConfirm: "Ben je zeker dat je dit bericht wil verwijderen?"
pinLimitExceeded: "Je kunt geen berichten meer vastprikken"
noteDeleteConfirm: "Ben je zeker dat je deze post wil verwijderen?"
pinLimitExceeded: "Je kunt geen posts meer vastprikken"
intro: "Installatie van Calckey geëindigd! Maak nu een beheerder aan."
done: "Klaar"
processing: "Bezig met verwerken"
@ -499,3 +499,23 @@ manageGroups: Beheer groepen
subscribePushNotification: Pushmeldingen inschakelen
unsubscribePushNotification: Pushmeldingen uitschakelen
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",
"version": "14.0.0-dev14",
"version": "14.0.0-dev18",
"codename": "aqua",
"repository": {
"type": "git",
"url": "https://codeberg.org/calckey/calckey.git"
},
"packageManager": "pnpm@8.5.0",
"packageManager": "pnpm@8.5.1",
"private": true,
"scripts": {
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",

View File

@ -85,7 +85,7 @@
"koa-send": "5.0.1",
"koa-slow": "2.1.0",
"koa-views": "7.0.2",
"mfm-js": "0.23.2",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"multer": "1.4.4-lts.1",
"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(/(?<=morn)ing/gi, (x) => (x === "ING" ? "YAN" : "yan"))
.replace(/(?<=every)one/gi, (x) => (x === "ONE" ? "NYAN" : "nyan"))
.replace(/non(?=[bcdfghjklmnpqrstvwxyz])/gi, (x) =>
x === "NON" ? "NYAN" : "nyan",
)
// ko-KR
.replace(/[나-낳]/g, (match) =>
String.fromCharCode(

View File

@ -38,6 +38,7 @@ export const paramDef = {
type: "object",
properties: {
noteId: { type: "string", format: "misskey:id" },
userId: { type: "string", format: "misskey:id" },
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
sinceId: { 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;
});
const query = makePaginationQuery(
let query = makePaginationQuery(
Notes.createQueryBuilder("note"),
ps.sinceId,
ps.untilId,
)
.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.banner", "banner")
.leftJoinAndSelect("note.reply", "reply")

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
<template>
<button
class="kpoogebi _button"
v-if="$i != null && $i.id != user.id"
class="kpoogebi _button follow-button"
:class="{
wait,
active: isFollowing || hasPendingFollowRequestFromYou,
@ -10,40 +11,43 @@
}"
:disabled="wait"
@click="onClick"
:aria-label="`${state} ${user.name || user.username}`"
>
<template v-if="!wait">
<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>
</template>
<template
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>
</template>
<template
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>
</template>
<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>
</template>
<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>
</template>
<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>
</template>
</template>
<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>
</template>
</button>
@ -55,6 +59,7 @@ import type * as Misskey from "calckey-js";
import * as os from "@/os";
import { stream } from "@/stream";
import { i18n } from "@/i18n";
import { $i } from "@/account";
const emit = defineEmits(["refresh"]);
const props = withDefaults(
@ -71,6 +76,8 @@ const props = withDefaults(
const isBlocking = computed(() => props.user.isBlocking);
let state = $ref(i18n.ts.processing);
let isFollowing = $ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = $ref(
props.user.hasPendingFollowRequestFromYou
@ -155,7 +162,7 @@ onBeforeUnmount(() => {
</script>
<style lang="scss" scoped>
.kpoogebi {
.follow-button {
position: relative;
display: inline-flex;
align-items: center;
@ -164,13 +171,15 @@ onBeforeUnmount(() => {
color: var(--accent);
border: solid 1px var(--accent);
padding: 0;
height: 31px;
font-size: 16px;
border-radius: 32px;
width: 2em;
height: 2em;
border-radius: 100px;
background: var(--bg);
&.full {
padding: 0 8px 0 12px;
padding: 0.2em 0.7em;
width: auto;
font-size: 14px;
}
@ -207,7 +216,7 @@ onBeforeUnmount(() => {
}
&.active {
color: #fff;
color: var(--fgOnAccent);
background: var(--accent);
&:hover {

View File

@ -1,5 +1,6 @@
<template>
<div
:aria-label="accessibleLabel"
v-if="!muted.muted"
v-show="!isDeleted"
ref="el"
@ -85,6 +86,7 @@
:parentId="appearNote.parentId"
@push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()"
@expanded="(e) => setPostExpanded(e)"
></MkSubNoteContent>
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini />
@ -472,6 +474,39 @@ function readPromo() {
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({
focus,
blur,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,10 +3,12 @@
v-if="show"
ref="el"
class="fdidabkb"
:class="{ slim: narrow, thin: thin_ }"
:class="{ thin: thin_, tabs: tabs?.length > 0 }"
:style="{ background: bg }"
@click="onClick"
>
<div class="left">
<div class="buttons">
<button
v-if="props.displayBackButton"
class="_button button icon backButton"
@ -16,18 +18,17 @@
>
<i class="ph-caret-left ph-bold ph-lg"></i>
</button>
<div v-if="narrow" class="buttons left" @click="openAccountMenu">
<MkAvatar
v-if="props.displayMyAvatar && $i"
class="avatar"
v-if="narrow && props.displayMyAvatar && $i"
class="avatar button"
:user="$i"
:disable-preview="true"
disableLink
@click.stop="openAccountMenu"
/>
</div>
<template v-if="metadata">
<div
v-if="!hideTitle"
v-if="!hideTitle && metadata"
class="titleContainer"
@click="showTabsPopup"
>
@ -66,7 +67,14 @@
</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
v-for="tab in tabs"
:ref="(el) => (tabRefs[tab.key] = el)"
@ -85,6 +93,20 @@
</nav>
</template>
<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">
<button
v-tooltip.noDelay="action.text"
@ -112,7 +134,7 @@ import {
nextTick,
reactive,
} from "vue";
import tinycolor from "tinycolor2";
import MkFollowButton from "@/components/MkFollowButton.vue";
import { popupMenu } from "@/os";
import { scrollToTop } from "@/scripts/scroll";
import { globalEvents } from "@/events";
@ -223,25 +245,9 @@ function goBack(): void {
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;
onMounted(() => {
calcBg();
globalEvents.on("themeChanged", calcBg);
watch(
() => [props.tab, props.tabs],
() => {
@ -251,17 +257,15 @@ onMounted(() => {
// offsetWidth offsetLeft getBoundingClientRect 使
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
const tabSizeX = tabEl.scrollWidth + 20; // + the tab's padding
if (props.tabs.length > 3) {
tabEl.style = `--width: ${tabSizeX}px`;
}
setTimeout(() => {
const parentRect = tabsEl.getBoundingClientRect();
const rect = tabEl.getBoundingClientRect();
const left =
rect.left - parentRect.left + tabsEl?.scrollLeft;
tabHighlightEl.style.width = tabSizeX + "px";
tabHighlightEl.style.transform = `translateX(${left}px)`;
tabHighlightEl.style.transform = `translateX(${tabEl.offsetLeft}px)`;
window.requestAnimationFrame(() => {
tabsEl?.scrollTo({
left: left - 60,
left: tabEl.offsetLeft - 60,
behavior: "smooth",
});
});
@ -286,7 +290,6 @@ onMounted(() => {
});
onUnmounted(() => {
globalEvents.off("themeChanged", calcBg);
if (ro) ro.disconnect();
});
</script>
@ -295,94 +298,96 @@ onUnmounted(() => {
.fdidabkb {
--height: 55px;
display: flex;
justify-content: space-between;
width: 100%;
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));
border-bottom: solid 0.5px var(--divider);
height: var(--height);
z-index: -1;
}
&::after {
content: "";
position: absolute;
inset: 0;
background: var(--bg);
opacity: 0.85;
z-index: -2;
}
&.thin {
--height: 45px;
> .buttons {
.buttons {
> .button {
font-size: 0.9em;
}
}
}
&.slim {
> .titleContainer {
flex: 1;
margin: 0 auto;
> *:first-child {
margin-left: auto;
}
> *:last-child {
margin-right: auto;
}
}
> .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%;
}
}
}
> .left {
display: flex;
> .buttons {
&:not(:empty) {
margin-left: calc(0px - var(--margin));
}
> .avatar {
width: 32px;
height: 32px;
margin-left: var(--margin);
}
}
}
.buttons {
--margin: 8px;
display: flex;
align-items: center;
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 {
margin-left: auto;
justify-content: flex-end;
// margin-right: calc(0px - var(--margin));
// margin-left: var(--margin);
> .button:last-child {
margin-right: calc(0px - var(--margin));
}
&:empty {
display: none;
}
> .button/*, @at-root .backButton*/ {
@ -412,13 +417,12 @@ onUnmounted(() => {
}
}
> .left {
> .backButton {
display: flex;
align-items: center;
justify-content: center;
margin-left: 1rem;
}
> .titleContainer {
display: flex;
align-items: center;
@ -428,7 +432,6 @@ onUnmounted(() => {
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
margin-right: 1rem;
> .avatar {
@ -445,7 +448,6 @@ onUnmounted(() => {
margin-right: 8px;
width: 16px;
text-align: center;
transform: translate(0em);
}
> .title {
@ -474,23 +476,58 @@ onUnmounted(() => {
}
}
}
}
> .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 {
position: relative;
width: 100%;
font-size: 1em;
overflow-x: auto;
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 {
content: "";
display: inline-block;
height: 40%;
border-left: 1px solid var(--divider);
margin-right: 1em;
margin-left: 10px;
vertical-align: -1px;
&.collapse {
--width: 38px;
> .tab {
width: 38px;
min-width: 38px !important;
&:not(.active) > .title {
opacity: 0;
}
}
}
> .tab {
@ -499,12 +536,12 @@ onUnmounted(() => {
position: relative;
border-inline: 10px solid transparent;
height: 100%;
min-width: max-content;
font-weight: normal;
opacity: 0.7;
width: 38px;
--width: 38px;
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 {
opacity: 1;
@ -515,9 +552,7 @@ onUnmounted(() => {
color: var(--accent);
font-weight: 600;
width: var(--width);
}
&:not(.active) > .title {
opacity: 0;
min-width: var(--width) !important;
}
> .icon + .title {
@ -527,7 +562,6 @@ onUnmounted(() => {
transition: opacity 0.2s;
}
}
> .highlight {
position: absolute;
bottom: 0;

View File

@ -156,6 +156,14 @@ export default defineComponent({
}
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": {
const transform =
token.props.args.h && token.props.args.v

View File

@ -353,6 +353,18 @@
</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="title">{{ i18n.ts._mfm.position }}</div>
<div class="content">
@ -479,6 +491,7 @@ let preview_bg = $ref("$[bg.color=ff0000 Background color]");
let preview_plain = $ref(
"<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>"
);
let preview_fade = $ref("$[fade 🍮] $[fade.out 🍮] $[fade.speed=5s 🍮]");
definePageMetadata({
title: i18n.ts._mfm.cheatSheet,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -264,8 +264,8 @@ importers:
specifier: 7.0.2
version: 7.0.2(@types/koa@2.13.5)(ejs@3.1.8)(pug@3.0.2)
mfm-js:
specifier: 0.23.2
version: 0.23.2
specifier: 0.23.3
version: 0.23.3
mime-types:
specifier: 2.1.35
version: 2.1.35
@ -840,8 +840,8 @@ importers:
specifier: 2.1.0
version: 2.1.0
swiper:
specifier: ^8.4.5
version: 8.4.5
specifier: 9.3.2
version: 9.3.2
syuilo-password-strength:
specifier: 0.0.1
version: 0.0.1
@ -6411,12 +6411,6 @@ packages:
domhandler: 5.0.3
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:
resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==}
dev: false
@ -10424,17 +10418,10 @@ packages:
engines: {node: '>= 0.6'}
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:
resolution: {integrity: sha512-o8scYmbey6rMUmWAlT3k3ntt6khaCLdxlmHhAWV5wTTMj2OK1atQvZfRUq0SIVm1Jig08qlZg/ps71xUqrScNA==}
dependencies:
twemoji-parser: 14.0.0
dev: true
/micromatch@3.1.10:
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)
dev: true
/swiper@8.4.5:
resolution: {integrity: sha512-zveyEFBBv4q1sVkbJHnuH4xCtarKieavJ4SxP0QEHvdpPLJRuD7j/Xg38IVVLbp7Db6qrPsLUePvxohYx39Agw==}
/swiper@9.3.2:
resolution: {integrity: sha512-Kj9Z4kXRmJR3YT/Wj+XLWj8P6IcRt+WG38uL8M3/Wny7+6sV0TlP9vnE1X+Co9c7VzNooojWGnFa+Wf/9+CUMA==}
engines: {node: '>= 4.7.0'}
requiresBuild: true
dependencies:
dom7: 4.0.4
ssr-window: 4.0.2
dev: true