Merge branch 'master' into chat-notify-via-email

This commit is contained in:
mahula 2025-04-07 09:58:12 +02:00 committed by GitHub
commit 15a669ecb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1496 additions and 427 deletions

View File

@ -38,7 +38,7 @@ jobs:
run: npm install && npm run docs:build
- name: Deploy Vuepress to Github Pages
uses: crazy-max/ghaction-github-pages@fbf0a4fa4e00f45accd6cf3232368436ec06ed59 # v4.0.0
uses: crazy-max/ghaction-github-pages@df5cc2bfa78282ded844b354faee141f06b41865 # v4.0.0
with:
target_branch: gh-pages
build_dir: .vuepress/dist

View File

@ -59,16 +59,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.1.7
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Log in to the Container registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@70b2cdc6480c1a8b86edf1777157f8f437de2166
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@ -81,7 +81,7 @@ jobs:
type=sha
- name: Build and push Docker images
id: push
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4
with:
context: ${{ matrix.app.context }}
target: ${{ matrix.app.target }}

View File

@ -64,7 +64,7 @@ jobs:
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
#- name: Repository Dispatch
# uses: peter-evans/repository-dispatch@b0b38f73c8333be75d585a92b2c630a10d2a78f5 # v3.0.0
# uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0
# with:
# token: ${{ github.token }}
# event-type: trigger-ocelot-build-success
@ -72,7 +72,7 @@ jobs:
# client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}'
- name: Repository Dispatch stage.ocelot.social
uses: peter-evans/repository-dispatch@b0b38f73c8333be75d585a92b2c630a10d2a78f5 # v3.0.0
uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0
with:
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
event-type: trigger-ocelot-build-success
@ -80,7 +80,7 @@ jobs:
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "GITHUB_RUN_NUMBER": "${{ env.GITHUB_RUN_NUMBER }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}'
- name: Repository Dispatch stage.yunite.me
uses: peter-evans/repository-dispatch@b0b38f73c8333be75d585a92b2c630a10d2a78f5 # v3.0.0
uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0
with:
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
event-type: trigger-ocelot-build-success

View File

@ -37,7 +37,7 @@ jobs:
- name: Cache docker images
id: cache-neo4j
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-backend-neo4j-cache
@ -58,7 +58,7 @@ jobs:
- name: Cache docker images
id: cache-backend
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/backend.tar
key: ${{ github.run_id }}-backend-cache
@ -87,14 +87,14 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Restore Neo4J cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-backend-neo4j-cache
fail-on-cache-miss: true
- name: Restore Backend cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/backend.tar
key: ${{ github.run_id }}-backend-cache

View File

@ -37,7 +37,7 @@ jobs:
- name: Cache docker images
id: cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: |
/opt/cucumber-json-formatter
@ -59,7 +59,7 @@ jobs:
job: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- name: Restore cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
id: cache
with:
path: |

View File

@ -50,7 +50,7 @@ jobs:
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
- name: Cache docker image
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/webapp.tar
key: ${{ github.run_id }}-webapp-cache
@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Restore webapp cache
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/webapp.tar
key: ${{ github.run_id }}-webapp-cache

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ node_modules/
cypress/videos
cypress/screenshots/
cypress.env.json
deployment/configurations/
.vuepress/.cache/
.vuepress/.temp/

View File

@ -23,8 +23,8 @@
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create"
},
"dependencies": {
"@babel/cli": "~7.26.4",
"@babel/core": "^7.26.9",
"@babel/cli": "~7.27.0",
"@babel/core": "^7.26.10",
"@babel/node": "~7.26.0",
"@babel/plugin-proposal-throw-expressions": "^7.25.9",
"@babel/preset-env": "~7.26.9",
@ -53,7 +53,7 @@
"graphql-redis-subscriptions": "^2.7.0",
"graphql-shield": "~7.2.2",
"graphql-tag": "~2.10.3",
"helmet": "~8.0.0",
"helmet": "~8.1.0",
"ioredis": "^4.16.1",
"jsonwebtoken": "~8.5.1",
"languagedetect": "^2.0.0",
@ -61,20 +61,20 @@
"linkifyjs": "^4.2.0",
"lodash": "~4.17.21",
"merge-graphql-schemas": "^1.7.8",
"metascraper": "^5.46.7",
"metascraper-author": "^5.46.5",
"metascraper-date": "^5.46.5",
"metascraper-description": "^5.46.5",
"metascraper-image": "^5.46.5",
"metascraper-lang": "^5.46.5",
"metascraper": "^5.46.11",
"metascraper-author": "^5.46.11",
"metascraper-date": "^5.46.11",
"metascraper-description": "^5.46.11",
"metascraper-image": "^5.46.11",
"metascraper-lang": "^5.46.11",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.46.5",
"metascraper-publisher": "^5.46.5",
"metascraper-logo": "^5.46.11",
"metascraper-publisher": "^5.46.11",
"metascraper-soundcloud": "^5.34.4",
"metascraper-title": "^5.46.5",
"metascraper-url": "^5.46.5",
"metascraper-video": "^5.46.5",
"metascraper-youtube": "^5.46.5",
"metascraper-title": "^5.46.11",
"metascraper-url": "^5.46.11",
"metascraper-video": "^5.46.11",
"metascraper-youtube": "^5.46.11",
"migrate": "^2.1.0",
"mime-types": "^2.1.35",
"minimatch": "^9.0.4",
@ -86,25 +86,25 @@
"nodemailer": "^6.10.0",
"nodemailer-html-to-text": "^3.2.0",
"request": "~2.88.2",
"sanitize-html": "~2.14.0",
"sanitize-html": "~2.15.0",
"slug": "~9.1.0",
"subscriptions-transport-ws": "^0.9.19",
"trunc-html": "~1.1.2",
"uuid": "~9.0.1",
"validator": "^13.12.0",
"validator": "^13.15.0",
"xregexp": "^5.1.2"
},
"devDependencies": {
"@faker-js/faker": "9.5.0",
"@faker-js/faker": "9.6.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.5",
"@types/node": "^22.14.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"apollo-server-testing": "~2.11.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.1",
"eslint-config-standard": "^17.1.0",
"eslint-import-resolver-typescript": "^3.8.3",
"eslint-import-resolver-typescript": "^4.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-n": "^16.6.2",
@ -113,11 +113,11 @@
"eslint-plugin-security": "^3.0.1",
"jest": "^29.7.0",
"nodemon": "~3.1.9",
"prettier": "^3.5.2",
"prettier": "^3.5.3",
"rosie": "^2.1.1",
"ts-jest": "^29.2.5",
"ts-jest": "^29.3.1",
"ts-node": "^10.9.2",
"typescript": "^5.7.3"
"typescript": "^5.8.3"
},
"resolutions": {
"**/**/fs-capacitor": "^6.2.0",

View File

@ -255,7 +255,6 @@ describe('notifications', () => {
})
it('sends me no notification', async () => {
await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: { notifications: [] },

View File

@ -109,13 +109,19 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
const { content } = args
let idsOfUsers = extractMentionedUsers(content)
let idsOfMentionedUsers = extractMentionedUsers(content)
const comment = await resolve(root, args, context, resolveInfo)
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
idsOfUsers = idsOfUsers.filter((id) => id !== postAuthor.id)
idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id)
await publishNotifications(context, [
notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context),
notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context),
notifyUsersOfMention(
'Comment',
comment.id,
idsOfMentionedUsers,
'mentioned_in_comment',
context,
),
notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context),
])
return comment
}
@ -270,29 +276,34 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
}
}
const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => {
if (context.user.id === postAuthorId) return []
const notifyUsersOfComment = async (label, commentId, reason, context) => {
await validateNotifyUsers(label, reason)
const session = context.driver.session()
const writeTxResultPromise = await session.writeTransaction(async (transaction) => {
const notificationTransactionResponse = await transaction.run(
`
MATCH (postAuthor:User {id: $postAuthorId})-[:WROTE]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
WHERE NOT (postAuthor)-[:BLOCKED]-(commenter)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(postAuthor)
MATCH (observingUser:User)-[:OBSERVES { active: true }]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
WHERE NOT (observingUser)-[:BLOCKED]-(commenter) AND NOT observingUser.id = $userId
WITH observingUser, post, comment, commenter
MATCH (postAuthor:User)-[:WROTE]->(post)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser)
SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime())
WITH notification, postAuthor, post, commenter,
WITH notification, observingUser, post, commenter, postAuthor,
comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource
RETURN notification {
.*,
from: finalResource,
to: properties(postAuthor),
to: properties(observingUser),
relatedUser: properties(commenter)
}
`,
{ commentId, postAuthorId, reason },
{
commentId,
reason,
userId: context.user.id,
},
)
return notificationTransactionResponse.records.map((record) => record.get('notification'))
})

View File

@ -0,0 +1,377 @@
import gql from 'graphql-tag'
import { cleanDatabase } from '../../db/factories'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '../../config'
CONFIG.CATEGORIES_ACTIVE = false
let server, query, mutate, authenticatedUser
let postAuthor, firstCommenter, secondCommenter
const driver = getDriver()
const neode = getNeode()
const createPostMutation = gql`
mutation ($id: ID, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
id
title
content
}
}
`
const createCommentMutation = gql`
mutation ($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
content
}
}
`
const notificationQuery = gql`
query ($read: Boolean) {
notifications(read: $read, orderBy: updatedAt_desc) {
read
reason
createdAt
relatedUser {
id
}
from {
__typename
... on Post {
id
content
}
... on Comment {
id
content
}
... on Group {
id
}
}
}
}
`
const toggleObservePostMutation = gql`
mutation ($id: ID!, $value: Boolean!) {
toggleObservePost(id: $id, value: $value) {
isObservedByMe
observingUsersCount
}
}
`
beforeAll(async () => {
await cleanDatabase()
const createServerResult = createServer({
context: () => {
return {
user: authenticatedUser,
neode,
driver,
cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null,
},
}
},
})
server = createServerResult.server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
})
afterAll(async () => {
await cleanDatabase()
driver.close()
})
describe('notifications for users that observe a post', () => {
beforeAll(async () => {
postAuthor = await neode.create(
'User',
{
id: 'post-author',
name: 'Post Author',
slug: 'post-author',
},
{
email: 'test@example.org',
password: '1234',
},
)
firstCommenter = await neode.create(
'User',
{
id: 'first-commenter',
name: 'First Commenter',
slug: 'first-commenter',
},
{
email: 'test2@example.org',
password: '1234',
},
)
secondCommenter = await neode.create(
'User',
{
id: 'second-commenter',
name: 'Second Commenter',
slug: 'second-commenter',
},
{
email: 'test3@example.org',
password: '1234',
},
)
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: 'This is the content of the post',
},
})
})
describe('first comment on the post', () => {
beforeAll(async () => {
authenticatedUser = await firstCommenter.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
postId: 'post',
id: 'c-1',
content: 'first comment of first commenter',
},
})
})
it('sends NO notification to the commenter', async () => {
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [],
},
errors: undefined,
})
})
it('sends notification to the author', async () => {
authenticatedUser = await postAuthor.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-1',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
describe('second comment on post', () => {
beforeAll(async () => {
authenticatedUser = await secondCommenter.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
postId: 'post',
id: 'c-2',
content: 'first comment of second commenter',
},
})
})
it('sends NO notification to the commenter', async () => {
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [],
},
errors: undefined,
})
})
it('sends notification to the author', async () => {
authenticatedUser = await postAuthor.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-2',
},
read: false,
reason: 'commented_on_post',
},
{
from: {
__typename: 'Comment',
id: 'c-1',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
it('sends notification to first commenter', async () => {
authenticatedUser = await firstCommenter.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-2',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
})
describe('first commenter unfollows the post and post author comments post', () => {
beforeAll(async () => {
authenticatedUser = await firstCommenter.toJson()
await mutate({
mutation: toggleObservePostMutation,
variables: {
id: 'post',
value: false,
},
})
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
postId: 'post',
id: 'c-3',
content: 'first comment of post author',
},
})
})
it('sends no new notification to the post author', async () => {
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-2',
},
read: false,
reason: 'commented_on_post',
},
{
from: {
__typename: 'Comment',
id: 'c-1',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
it('sends no new notification to first commenter', async () => {
authenticatedUser = await firstCommenter.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-2',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
it('sends notification to second commenter', async () => {
authenticatedUser = await secondCommenter.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-3',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,73 @@
# Deployment Values
For each deployment, you need to set the environment variables and configurations.
Here is some specific information on how to set the values.
## Webapp
We have several configuration possibilities just in the frontend.
### Date Time
In file `branding/constants/dateTime.js`.
- `RELATIVE_DATETIME`
- `true` (default) or `false`
- `ABSOLUT_DATETIME_FORMAT`
- definition see [date-fns, format](https://date-fns.org/v3.3.1/docs/format):
- `P`: just localized date
- `Pp`: just localized date and time
## E-Mails
You need to set environment variables to send registration and invitation information or notifications to users, for example.
### SPF and DKIM
More and more e-mail providers require settings for authorization and verification of e-mail senders.
### SPF
Sometimes it is enough to create an SPF record in your DNS.
### DKIM
However, if you need DKIM authorization and verification, you must set the appropriate environment variables in: `.env`, `docker-compose.yml` or Helm script `values.yaml`:
```bash
SMTP_DKIM_DOMAINNAME=<your e-mail sender domain>
SMTP_DKIM_KEYSELECTOR=ocelot # "free" name used in DNS as selector. we recommend this
SMTP_DKIM_PRIVATKEY="-----BEGIN RSA PRIVATE KEY-----\\n<your base64 encoded private key data>\\n-----END RSA PRIVATE KEY-----\\n"
```
You can find out how DKIM works here:
<https://www.ionos.com/digitalguide/e-mail/e-mail-security/dkim-domainkeys/>
To create the private and public DKIM key as DNS records with selector, see here:
<https://knowledge.ondmarc.redsift.com/en/articles/2141592-generating-2048-bits-dkim-public-and-private-keys-using-openssl-on-a-mac>
Information about the required PEM format can be found here:
<https://docs.progress.com/bundle/datadirect-hybrid-data-pipeline-installation-46/page/PEM-file-format.html>
## Neo4j Database
We have several configuration options for our Neo4j database.
### DBMS_DEFAULT_DATABASE Default Database Name to be Used
If you need to set the default database name in Neo4j to be used for all operations and terminal commands like our backup scripts, you must set the appropriate environment variable in: `.env`, `docker-compose.yml` or Helm script `values.yaml`:
```yaml
DBMS_DEFAULT_DATABASE: "graph.db"
```
The default value is `neo4j` if it is not set.
As example see files:
- `neo4j/.env.template`
- `deployment/docker-compose.yml`
- `deployment/configurations/stage.ocelot.social/kubernetes/values.yaml.template`

View File

@ -43,3 +43,4 @@ services:
container_name: mailserver
ports:
- 1080:80
- 25:25

View File

@ -40,3 +40,4 @@ services:
image: djfarrelly/maildev
ports:
- 1080:80
- 25:25

84
package-lock.json generated
View File

@ -9,13 +9,13 @@
"version": "3.2.1",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/register": "^7.25.9",
"@badeball/cypress-cucumber-preprocessor": "^22.0.1",
"@cucumber/cucumber": "11.2.0",
"@cypress/browserify-preprocessor": "^3.0.2",
"@faker-js/faker": "9.5.0",
"@faker-js/faker": "9.6.0",
"auto-changelog": "^2.5.0",
"bcryptjs": "^2.4.3",
"cross-env": "^7.0.3",
@ -127,22 +127,22 @@
}
},
"node_modules/@babel/core": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz",
"integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.9",
"@babel/generator": "^7.26.10",
"@babel/helper-compilation-targets": "^7.26.5",
"@babel/helper-module-transforms": "^7.26.0",
"@babel/helpers": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/helpers": "^7.26.10",
"@babel/parser": "^7.26.10",
"@babel/template": "^7.26.9",
"@babel/traverse": "^7.26.9",
"@babel/types": "^7.26.9",
"@babel/traverse": "^7.26.10",
"@babel/types": "^7.26.10",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@ -158,14 +158,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz",
"integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9",
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
@ -411,27 +411,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9"
"@babel/template": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.9"
"@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -1714,32 +1714,32 @@
}
},
"node_modules/@babel/template": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9"
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz",
"integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9",
"@babel/generator": "^7.27.0",
"@babel/parser": "^7.27.0",
"@babel/template": "^7.27.0",
"@babel/types": "^7.27.0",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@ -1748,9 +1748,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@ -2786,9 +2786,9 @@
}
},
"node_modules/@faker-js/faker": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.0.tgz",
"integrity": "sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw==",
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.6.0.tgz",
"integrity": "sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==",
"dev": true,
"funding": [
{

View File

@ -33,13 +33,13 @@
"release": "./scripts/release.sh"
},
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/register": "^7.25.9",
"@badeball/cypress-cucumber-preprocessor": "^22.0.1",
"@cucumber/cucumber": "11.2.0",
"@cypress/browserify-preprocessor": "^3.0.2",
"@faker-js/faker": "9.5.0",
"@faker-js/faker": "9.6.0",
"auto-changelog": "^2.5.0",
"bcryptjs": "^2.4.3",
"cross-env": "^7.0.3",

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="1080" height="1080"><rect width="100%" height="100%" fill="transparent"/><path d="M15 3a2 2 0 0 1 2 2c0 .085-.021.168-.031.25C20.49 6.174 23 9.523 23 13.281V22c0 .565.435 1 1 1h1v2h-7.188c.114.316.188.647.188 1 0 1.645-1.355 3-3 3s-3-1.355-3-3c0-.353.073-.684.188-1H5v-2h1c.565 0 1-.435 1-1v-9c0-3.726 2.574-6.866 6.031-7.75C13.021 5.168 13 5.085 13 5a2 2 0 0 1 2-2zm-.437 4A6.004 6.004 0 0 0 9 13v9c0 .353-.073.684-.188 1h12.375a2.925 2.925 0 0 1-.188-1v-8.719c0-3.319-2.546-6.183-5.813-6.281-.064-.002-.124 0-.188 0-.148 0-.292-.011-.438 0zM15 25c-.564 0-1 .436-1 1 0 .564.436 1 1 1 .564 0 1-.436 1-1 0-.564-.436-1-1-1z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:1" transform="translate(33.75) scale(33.75)"/><rect width="74.334" height="74.334" x="-37.167" y="-37.167" rx="0" ry="0" style="stroke:#000;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:1" transform="matrix(9.42 -12.59 .8 .6 538.54 541.95)" vector-effect="non-scaling-stroke"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -500,6 +500,44 @@ describe('ContentMenu.vue', () => {
],
])
})
it('can observe posts', async () => {
const wrapper = await openContentMenu({
isOwner: false,
resourceType: 'contribution',
resource: {
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
isObservedByMe: false,
},
})
wrapper
.findAll('.ds-menu-item')
.filter((item) => item.text() === 'post.menu.observe')
.at(0)
.trigger('click')
expect(wrapper.emitted('toggleObservePost')).toEqual([
['d23a4265-f5f7-4e17-9f86-85f714b4b9f8', true],
])
})
it('can unobserve posts', async () => {
const wrapper = await openContentMenu({
isOwner: false,
resourceType: 'contribution',
resource: {
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
isObservedByMe: true,
},
})
wrapper
.findAll('.ds-menu-item')
.filter((item) => item.text() === 'post.menu.unobserve')
.at(0)
.trigger('click')
expect(wrapper.emitted('toggleObservePost')).toEqual([
['d23a4265-f5f7-4e17-9f86-85f714b4b9f8', false],
])
})
})
})
})

View File

@ -99,6 +99,24 @@ export default {
})
}
}
if (this.resource.isObservedByMe) {
routes.push({
label: this.$t(`post.menu.unobserve`),
callback: () => {
this.$emit('toggleObservePost', this.resource.id, false)
},
icon: 'bell-slashed',
})
} else {
routes.push({
label: this.$t(`post.menu.observe`),
callback: () => {
this.$emit('toggleObservePost', this.resource.id, true)
},
icon: 'bell',
})
}
}
if (this.isOwner && this.resourceType === 'comment') {

View File

@ -0,0 +1,60 @@
import { mount } from '@vue/test-utils'
import ObserveButton from './ObserveButton.vue'
const localVue = global.localVue
describe('ObserveButton', () => {
let mocks
const Wrapper = (count = 1, postId = '123', isObserved = true) => {
return mount(ObserveButton, {
mocks,
localVue,
propsData: {
count,
postId,
isObserved,
},
})
}
let wrapper
beforeEach(() => {
mocks = {
$t: jest.fn(),
}
})
describe('observed', () => {
beforeEach(() => {
wrapper = Wrapper(1, '123', true)
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
it('emits toggleObservePost with false when clicked', () => {
const button = wrapper.find('.base-button')
button.trigger('click')
expect(wrapper.emitted('toggleObservePost')).toEqual([['123', false]])
})
})
describe('unobserved', () => {
beforeEach(() => {
wrapper = Wrapper(1, '123', false)
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
it('emits toggleObservePost with true when clicked', () => {
const button = wrapper.find('.base-button')
button.trigger('click')
expect(wrapper.emitted('toggleObservePost')).toEqual([['123', true]])
})
})
})

View File

@ -0,0 +1,39 @@
<template>
<ds-space margin="xx-small" class="text-align-center">
<base-button :loading="loading" :filled="isObserved" icon="bell" circle @click="toggle" />
<ds-space margin-bottom="xx-small" />
<ds-text color="soft" class="observe-button-text">
<ds-heading style="display: inline" tag="h3">{{ count }}x</ds-heading>
{{ $t('observeButton.observed') }}
</ds-text>
</ds-space>
</template>
<script>
export default {
props: {
count: { type: Number, default: 0 },
postId: { type: String, default: null },
isObserved: { type: Boolean, default: false },
},
data() {
return {
loading: false,
}
},
methods: {
toggle() {
this.$emit('toggleObservePost', this.postId, !this.isObserved)
},
},
}
</script>
<style lang="scss">
.observe-button-text {
user-select: none;
}
.text-align-center {
text-align: center;
}
</style>

View File

@ -59,7 +59,7 @@
:key="category.id"
v-tooltip="{
content: `
${$t(`contribution.category.name.${category.slug}`)}:
${$t(`contribution.category.name.${category.slug}`)}:
${$t(`contribution.category.description.${category.slug}`)}
`,
placement: 'bottom-start',
@ -97,6 +97,7 @@
:is-owner="isAuthor"
@pinPost="pinPost"
@unpinPost="unpinPost"
@toggleObservePost="toggleObservePost"
/>
</client-only>
</footer>
@ -212,6 +213,9 @@ export default {
unpinPost(post) {
this.$emit('unpinPost', post)
},
toggleObservePost(postId, value) {
this.$emit('toggleObservePost', postId, value)
},
visibilityChanged(isVisible, entry, id) {
if (!this.post.viewedTeaserByCurrentUser && isVisible) {
this.$apollo

View File

@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ObserveButton observed renders 1`] = `
<div
class="ds-space text-align-center"
style="margin-top: 4px; margin-bottom: 4px;"
>
<button
class="base-button --icon-only --circle --filled"
type="button"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</button>
<div
class="ds-space"
style="margin-bottom: 4px;"
/>
<p
class="ds-text observe-button-text ds-text-soft"
>
<h3
class="ds-heading ds-heading-h3"
style="display: inline;"
>
1x
</h3>
</p>
</div>
`;
exports[`ObserveButton unobserved renders 1`] = `
<div
class="ds-space text-align-center"
style="margin-top: 4px; margin-bottom: 4px;"
>
<button
class="base-button --icon-only --circle"
type="button"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</button>
<div
class="ds-space"
style="margin-bottom: 4px;"
/>
<p
class="ds-text observe-button-text ds-text-soft"
>
<h3
class="ds-heading ds-heading-h3"
style="display: inline;"
>
1x
</h3>
</p>
</div>
`;

View File

@ -48,6 +48,9 @@
@removePostFromList="posts = removePostFromList(post, posts)"
@pinPost="pinPost(post, refetchPostList)"
@unpinPost="unpinPost(post, refetchPostList)"
@toggleObservePost="
(postId, value) => toggleObservePost(postId, value, refetchPostList)
"
/>
</masonry-grid-item>
</template>

View File

@ -13,6 +13,8 @@ export default (i18n) => {
updatedAt
disabled
deleted
isPostObservedByMe
postObservingUsersCount
author {
id
slug

View File

@ -38,4 +38,5 @@ module.exports = {
},
moduleFileExtensions: ['js', 'json', 'vue'],
testEnvironment: 'jest-environment-jsdom',
snapshotSerializers: ['jest-serializer-vue'],
}

View File

@ -747,6 +747,9 @@
"title": "Benachrichtigungen",
"user": "Nutzer"
},
"observeButton": {
"observed": "beobachtet"
},
"post": {
"comment": {
"reply": "Antworten",
@ -778,8 +781,12 @@
"menu": {
"delete": "Beitrag löschen",
"edit": "Beitrag bearbeiten",
"observe": "Beitrag beobachten",
"observedSuccessfully": "Du beobachtest diesen Beitrag!",
"pin": "Beitrag anheften",
"pinnedSuccessfully": "Beitrag erfolgreich angeheftet!",
"unobserve": "Beitrag nicht mehr beobachten",
"unobservedSuccessfully": "Du beobachtest diesen Beitrag nicht mehr!",
"unpin": "Beitrag loslösen",
"unpinnedSuccessfully": "Angehefteten Beitrag erfolgreich losgelöst!"
},

View File

@ -747,6 +747,9 @@
"title": "Notifications",
"user": "User"
},
"observeButton": {
"observed": "observed"
},
"post": {
"comment": {
"reply": "Reply",
@ -778,8 +781,12 @@
"menu": {
"delete": "Delete post",
"edit": "Edit post",
"observe": "Observe post",
"observedSuccessfully": "You are now observing this post!",
"pin": "Pin post",
"pinnedSuccessfully": "Post pinned successfully!",
"unobserve": "Stop to observe post",
"unobservedSuccessfully": "You are no longer observing this post!",
"unpin": "Unpin post",
"unpinnedSuccessfully": "Post unpinned successfully!"
},

View File

@ -747,6 +747,9 @@
"title": "Notificaciones",
"user": "Usuario"
},
"observeButton": {
"observed": null
},
"post": {
"comment": {
"reply": "Contestar",
@ -778,8 +781,12 @@
"menu": {
"delete": "Borrar contribución",
"edit": "Editar contribución",
"observe": "Observar contribución",
"observedSuccessfully": null,
"pin": "Anclar contribución",
"pinnedSuccessfully": "¡Contribución anclado con éxito!",
"unobserve": "Dejar de observar contribución",
"unobservedSuccessfully": null,
"unpin": "Desanclar contribución",
"unpinnedSuccessfully": "¡Contribución desanclado con éxito!"
},

View File

@ -747,6 +747,9 @@
"title": "Notifications",
"user": "Utilisateur"
},
"observeButton": {
"observed": null
},
"post": {
"comment": {
"reply": null,
@ -778,8 +781,12 @@
"menu": {
"delete": "Supprimer le Post",
"edit": "Modifier le Post",
"observe": "Observer le Post",
"observedSuccessfully": null,
"pin": "Épingler le Post",
"pinnedSuccessfully": "Poste épinglé avec succès!",
"unobserve": "Ne plus observer le Post",
"unobservedSuccessfully": null,
"unpin": "Retirer l'épingle du poste",
"unpinnedSuccessfully": "Épingle retirer du Post avec succès!"
},

View File

@ -747,6 +747,9 @@
"title": null,
"user": null
},
"observeButton": {
"observed": null
},
"post": {
"comment": {
"reply": null,
@ -778,8 +781,12 @@
"menu": {
"delete": null,
"edit": null,
"observe": null,
"observedSuccessfully": null,
"pin": null,
"pinnedSuccessfully": null,
"unobserve": null,
"unobservedSuccessfully": null,
"unpin": null,
"unpinnedSuccessfully": null
},

View File

@ -747,6 +747,9 @@
"title": null,
"user": null
},
"observeButton": {
"observed": null
},
"post": {
"comment": {
"reply": null,
@ -778,8 +781,12 @@
"menu": {
"delete": null,
"edit": null,
"observe": null,
"observedSuccessfully": null,
"pin": null,
"pinnedSuccessfully": null,
"unobserve": null,
"unobservedSuccessfully": null,
"unpin": null,
"unpinnedSuccessfully": null
},

View File

@ -747,6 +747,9 @@
"title": null,
"user": null
},
"observeButton": {
"observed": null
},
"post": {
"comment": {
"reply": null,
@ -778,8 +781,12 @@
"menu": {
"delete": "Usuń wpis",
"edit": "Edytuj wpis",
"observe": null,
"observedSuccessfully": null,
"pin": null,
"pinnedSuccessfully": null,
"unobserve": null,
"unobservedSuccessfully": null,
"unpin": null,
"unpinnedSuccessfully": null
},

View File

@ -747,6 +747,9 @@
"title": "Notificações",
"user": "Usuário"
},
"observeButton": {
"observed": null
},
"post": {
"comment": {
"reply": null,
@ -778,8 +781,12 @@
"menu": {
"delete": "Excluir publicação",
"edit": "Editar publicação",
"observe": "Observar publicação",
"observedSuccessfully": null,
"pin": "Fixar publicação",
"pinnedSuccessfully": "Publicação fixada com sucesso!",
"unobserve": "Deixar de observar publicação",
"unobservedSuccessfully": null,
"unpin": "Desafixar publicação",
"unpinnedSuccessfully": "Publicação desafixada com sucesso!"
},

View File

@ -747,6 +747,9 @@
"title": "Уведомления",
"user": "Пользователь"
},
"observeButton": {
"observed": null
},
"post": {
"comment": {
"reply": "Ответ",
@ -778,8 +781,12 @@
"menu": {
"delete": "Удалить пост",
"edit": "Редактировать пост",
"observe": null,
"observedSuccessfully": null,
"pin": "Закрепить пост",
"pinnedSuccessfully": "Пост больше не закреплен!",
"unobserve": null,
"unobservedSuccessfully": null,
"unpin": "Открепить пост",
"unpinnedSuccessfully": "Пост успешно не закреплено!"
},

View File

@ -35,5 +35,23 @@ export default {
})
.catch((error) => this.$toast.error(error.message))
},
toggleObservePost(postId, value, refetchPostList = () => {}) {
this.$apollo
.mutate({
mutation: PostMutations().toggleObservePost,
variables: {
value,
id: postId,
},
})
.then(() => {
const message = this.$t(
`post.menu.${value ? 'observedSuccessfully' : 'unobservedSuccessfully'}`,
)
this.$toast.success(message)
refetchPostList()
})
.catch((error) => this.$toast.error(error.message))
},
},
}

View File

@ -40,6 +40,7 @@
"express": "~4.21.2",
"graphql": "~14.7.0",
"intersection-observer": "^0.12.0",
"jest-serializer-vue": "^3.1.0",
"jsonwebtoken": "~9.0.2",
"linkify-it": "~5.0.0",
"mapbox-gl": "1.13.2",
@ -47,7 +48,7 @@
"nuxt": "~2.12.1",
"nuxt-dropzone": "^1.0.4",
"nuxt-env": "~0.1.0",
"sass": "^1.85.0",
"sass": "^1.86.3",
"stack-utils": "^2.0.3",
"tippy.js": "^4.3.5",
"tiptap": "~1.26.6",
@ -55,7 +56,7 @@
"trunc-html": "^1.1.2",
"v-mapbox": "^1.11.2",
"v-tooltip": "~2.1.3",
"validator": "^13.12.0",
"validator": "^13.15.0",
"vue-advanced-chat": "^2.0.11",
"vue-count-to": "~1.0.13",
"vue-infinite-loading": "^2.4.5",
@ -72,7 +73,7 @@
"@babel/core": "^7.25.8",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.25.8",
"@faker-js/faker": "9.5.0",
"@faker-js/faker": "9.6.0",
"@storybook/addon-a11y": "^8.0.8",
"@storybook/addon-actions": "^5.3.21",
"@storybook/addon-notes": "^5.3.18",
@ -92,7 +93,7 @@
"core-js": "~2.6.10",
"css-loader": "~3.5.2",
"eslint": "^7.28.0",
"eslint-config-prettier": "~10.0.1",
"eslint-config-prettier": "~10.1.1",
"eslint-config-standard": "~15.0.1",
"eslint-loader": "~4.0.0",
"eslint-plugin-import": "~2.31.0",
@ -101,13 +102,13 @@
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-promise": "~7.2.1",
"eslint-plugin-standard": "~5.0.0",
"eslint-plugin-vue": "~9.32.0",
"eslint-plugin-vue": "~9.33.0",
"flush-promises": "^1.0.2",
"identity-obj-proxy": "^3.0.0",
"jest": "29.7",
"jest-environment-jsdom": "^29.7.0",
"mutation-observer": "^1.0.3",
"prettier": "~3.5.2",
"prettier": "~3.5.3",
"sass-loader": "~10.1.1",
"storybook-design-token": "^0.8.1",
"storybook-vue-router": "^1.0.7",

View File

@ -283,6 +283,9 @@
@removePostFromList="posts = removePostFromList(post, posts)"
@pinPost="pinPost(post, refetchPostList)"
@unpinPost="unpinPost(post, refetchPostList)"
@toggleObservePost="
(postId, value) => toggleObservePost(postId, value, refetchPostList)
"
/>
</masonry-grid-item>
</template>

View File

@ -116,6 +116,9 @@
@removePostFromList="posts = removePostFromList(post, posts)"
@pinPost="pinPost(post, refetchPostList)"
@unpinPost="unpinPost(post, refetchPostList)"
@toggleObservePost="
(postId, value) => toggleObservePost(postId, value, refetchPostList)
"
/>
</masonry-grid-item>
</template>

View File

@ -49,6 +49,7 @@
:is-owner="isAuthor"
@pinPost="pinPost"
@unpinPost="unpinPost"
@toggleObservePost="toggleObservePost"
/>
</client-only>
</section>
@ -111,6 +112,18 @@
:post-id="post.id"
/>
</ds-flex-item>
<!-- Follow Button -->
<ds-flex-item
:width="{ lg: '15%', md: '22%', sm: '22%', base: '100%' }"
class="shout-button"
>
<observe-button
:is-observed="post.isObservedByMe"
:count="post.observingUsersCount"
:post-id="post.id"
@toggleObservePost="toggleObservePost"
/>
</ds-flex-item>
</ds-flex>
</ds-space>
<!-- Comments -->
@ -156,6 +169,7 @@ import ContentMenu from '~/components/ContentMenu/ContentMenu'
import DateTimeRange from '~/components/DateTimeRange/DateTimeRange'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcShoutButton from '~/components/ShoutButton.vue'
import ObserveButton from '~/components/ObserveButton.vue'
import LocationTeaser from '~/components/LocationTeaser/LocationTeaser'
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
import {
@ -184,6 +198,7 @@ export default {
HcCategory,
HcHashtag,
HcShoutButton,
ObserveButton,
LocationTeaser,
PageParamsLink,
UserTeaser,
@ -302,6 +317,8 @@ export default {
},
async createComment(comment) {
this.post.comments.push(comment)
this.post.isObservedByMe = comment.isPostObservedByMe
this.post.observingUsersCount = comment.postObservingUsersCount
},
pinPost(post) {
this.$apollo
@ -325,6 +342,24 @@ export default {
})
.catch((error) => this.$toast.error(error.message))
},
toggleObservePost(postId, value) {
this.$apollo
.mutate({
mutation: PostMutations().toggleObservePost,
variables: {
value,
id: postId,
},
})
.then(() => {
const message = this.$t(
`post.menu.${value ? 'observedSuccessfully' : 'unobservedSuccessfully'}`,
)
this.$toast.success(message)
this.$apollo.queries.Post.refetch()
})
.catch((error) => this.$toast.error(error.message))
},
toggleNewCommentForm(showNewCommentForm) {
this.showNewCommentForm = showNewCommentForm
},
@ -379,7 +414,7 @@ export default {
position: relative;
/* The padding top makes sure the correct height is set (according to the
hero image aspect ratio) before the hero image loads so
the autoscroll works correctly when following a comment link.
the autoscroll works correctly when following a comment link.
*/
padding-top: calc(var(--hero-image-aspect-ratio) * (100% + 48px));

View File

@ -156,6 +156,9 @@
@removePostFromList="posts = removePostFromList(post, posts)"
@pinPost="pinPost(post, refetchPostList)"
@unpinPost="unpinPost(post, refetchPostList)"
@toggleObservePost="
(postId, value) => toggleObservePost(postId, value, refetchPostList)
"
/>
</masonry-grid-item>
</template>

View File

@ -2447,10 +2447,10 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@faker-js/faker@9.5.0":
version "9.5.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.5.0.tgz#ce254c83706250ca8a5a0e05683608160610dd84"
integrity sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw==
"@faker-js/faker@9.6.0":
version "9.6.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.6.0.tgz#64235d20330b142eef3d1d1638ba56c083b4bf1d"
integrity sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==
"@hapi/address@2.x.x":
version "2.0.0"
@ -9425,10 +9425,10 @@ eslint-config-prettier@^6.0.0:
dependencies:
get-stdin "^6.0.0"
eslint-config-prettier@~10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz#fbb03bfc8db0651df9ce4e8b7150d11c5fe3addf"
integrity sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==
eslint-config-prettier@~10.1.1:
version "10.1.1"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz#cf0ff6e5c4e7e15f129f1f1ce2a5ecba92dec132"
integrity sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==
eslint-config-standard@~15.0.1:
version "15.0.1"
@ -9534,10 +9534,10 @@ eslint-plugin-standard@~5.0.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz#c43f6925d669f177db46f095ea30be95476b1ee4"
integrity sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==
eslint-plugin-vue@~9.32.0:
version "9.32.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.32.0.tgz#2b558e827886b567dfaa156cc1cad0f596461fab"
integrity sha512-b/Y05HYmnB/32wqVcjxjHZzNpwxj1onBOvqW89W+V+XNG1dRuaFbNd3vT9CLbr2LXjEoq+3vn8DanWf7XU22Ug==
eslint-plugin-vue@~9.33.0:
version "9.33.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz#de33eba8f78e1d172c59c8ec7fbfd60c6ca35c39"
integrity sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
globals "^13.24.0"
@ -12609,6 +12609,13 @@ jest-runtime@^29.7.0:
slash "^3.0.0"
strip-bom "^4.0.0"
jest-serializer-vue@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jest-serializer-vue/-/jest-serializer-vue-3.1.0.tgz#af65817aa416d019f837b6cc53f121a3222846f4"
integrity sha512-vXz9/3IgBbLhsaVANYLG4ROCQd+Wg3qbB6ICofzFL+fbhSFPlqb0/MMGXcueVsjaovdWlYiRaLQLpdi1PTcoRQ==
dependencies:
pretty "2.0.0"
jest-snapshot@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5"
@ -16118,10 +16125,10 @@ prettier@^1.18.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
prettier@~3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.2.tgz#d066c6053200da0234bf8fa1ef45168abed8b914"
integrity sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==
prettier@~3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
pretty-bytes@^5.3.0:
version "5.3.0"
@ -16155,7 +16162,7 @@ pretty-time@^1.1.0:
resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e"
integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==
pretty@^2.0.0:
pretty@2.0.0, pretty@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pretty/-/pretty-2.0.0.tgz#adbc7960b7bbfe289a557dc5f737619a220d06a5"
integrity sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==
@ -17419,10 +17426,10 @@ sass-resources-loader@^2.2.1:
glob "^7.1.6"
loader-utils "^2.0.0"
sass@^1.85.0:
version "1.85.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.85.0.tgz#0127ef697d83144496401553f0a0e87be83df45d"
integrity sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==
sass@^1.86.3:
version "1.86.3"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.86.3.tgz#0a0d9ea97cb6665e73f409639f8533ce057464c9"
integrity sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"
@ -17816,12 +17823,7 @@ source-list-map@^2.0.0:
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
source-map-js@^1.0.2, source-map-js@^1.2.0:
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@ -19659,10 +19661,10 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
validator@^13.12.0:
version "13.12.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f"
integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==
validator@^13.15.0:
version "13.15.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.15.0.tgz#2dc7ce057e7513a55585109eec29b2c8e8c1aefd"
integrity sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==
vary@^1, vary@^1.1.2, vary@~1.1.2:
version "1.1.2"

View File

@ -52,34 +52,34 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==
"@babel/core@^7.16.0", "@babel/core@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.9.tgz#71838542a4b1e49dfed353d7acbc6eb89f4a76f2"
integrity sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==
"@babel/core@^7.16.0", "@babel/core@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9"
integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.9"
"@babel/generator" "^7.26.10"
"@babel/helper-compilation-targets" "^7.26.5"
"@babel/helper-module-transforms" "^7.26.0"
"@babel/helpers" "^7.26.9"
"@babel/parser" "^7.26.9"
"@babel/helpers" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/traverse" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/traverse" "^7.26.10"
"@babel/types" "^7.26.10"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.9.tgz#75a9482ad3d0cc7188a537aa4910bc59db67cbca"
integrity sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==
"@babel/generator@^7.26.10", "@babel/generator@^7.27.0":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.0.tgz#764382b5392e5b9aff93cadb190d0745866cbc2c"
integrity sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==
dependencies:
"@babel/parser" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/parser" "^7.27.0"
"@babel/types" "^7.27.0"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
@ -343,20 +343,20 @@
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helpers@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.9.tgz#28f3fb45252fc88ef2dc547c8a911c255fc9fef6"
integrity sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==
"@babel/helpers@^7.26.10":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808"
integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==
dependencies:
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/template" "^7.27.0"
"@babel/types" "^7.27.0"
"@babel/parser@^7.24.4", "@babel/parser@^7.24.7", "@babel/parser@^7.25.3", "@babel/parser@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5"
integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==
"@babel/parser@^7.24.4", "@babel/parser@^7.24.7", "@babel/parser@^7.25.3", "@babel/parser@^7.26.10", "@babel/parser@^7.27.0":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec"
integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==
dependencies:
"@babel/types" "^7.26.9"
"@babel/types" "^7.27.0"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9":
version "7.25.9"
@ -1015,32 +1015,32 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15", "@babel/template@^7.25.9", "@babel/template@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2"
integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==
"@babel/template@^7.22.15", "@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.0":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4"
integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/parser" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/parser" "^7.27.0"
"@babel/types" "^7.27.0"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.8", "@babel/traverse@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a"
integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.8":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.0.tgz#11d7e644779e166c0442f9a07274d02cd91d4a70"
integrity sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.9"
"@babel/parser" "^7.26.9"
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/generator" "^7.27.0"
"@babel/parser" "^7.27.0"
"@babel/template" "^7.27.0"
"@babel/types" "^7.27.0"
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.25.9", "@babel/types@^7.26.9", "@babel/types@^7.4.4":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce"
integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==
"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.4.4":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559"
integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
@ -1543,10 +1543,10 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699"
integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==
"@faker-js/faker@9.5.0":
version "9.5.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.5.0.tgz#ce254c83706250ca8a5a0e05683608160610dd84"
integrity sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw==
"@faker-js/faker@9.6.0":
version "9.6.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.6.0.tgz#64235d20330b142eef3d1d1638ba56c083b4bf1d"
integrity sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==
"@fastify/busboy@^2.0.0":
version "2.1.1"