diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b1a955a..f5cca07ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,40 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [3.5.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.4.0...3.5.0) + +- feat(webapp): user teaser popover [`#8450`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8450) +- feat(backend): signup email localized [`#8459`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8459) +- lint json [`#8472`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8472) +- refactor(other): cypress: simplify cucumber preprocessor imports and some linting [`#8489`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8489) +- fix backend node23 [`#8488`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8488) +- refactor(workflow): parallelize e2e preparation [`#8481`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8481) +- refactor(backend): types for context + `slug` [`#8486`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8486) +- feat(backend): emails for notifications [`#8435`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8435) +- remove some dependabot groups & no alpine version to allow update [`#8475`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8475) +- build(deps-dev): bump the babel group with 3 updates [`#8478`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8478) +- build(deps-dev): bump @types/node from 22.15.2 to 22.15.3 in /backend [`#8479`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8479) +- refactor(backend): refactor context [`#8434`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8434) +- build(deps): bump amannn/action-semantic-pull-request [`#8480`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8480) +- build(deps-dev): bump eslint-plugin-prettier in /webapp [`#8332`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8332) +- remove all helpers on src/helpers [`#8469`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8469) +- move models into database folder [`#8471`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8471) +- also lint cjs files [`#8467`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8467) +- refactor(backend): refactor badges [`#8465`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8465) +- refactor(backend): move resolvers into graphql folder [`#8470`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8470) +- refactor(webapp): remove unused packages [`#8468`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8468) +- refactor(backend): remove unused packages [`#8466`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8466) +- fix(backend): fix backend dev and dev:debug command [`#8439`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8439) +- move distanceToMe onto Location [`#8464`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8464) +- feat(backend): distanceToMe [`#8462`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8462) +- fix(webapp): fixed padding for mobile in basic layout [`#8455`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8455) +- Fix ocelot.social link for imprint and donation [`#8461`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8461) + #### [3.4.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.3.0...3.4.0) +> 28 April 2025 + +- v3.4.0 [`#8454`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8454) - fix(webapp): fix badge focus [`#8452`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8452) - feat(backend): branding middlewares [`#8429`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8429) - refactor(webapp): make login, registration, password-reset layout brandable [`#8440`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8440) diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs index 3e8e942ba..9883fae83 100644 --- a/backend/.eslintrc.cjs +++ b/backend/.eslintrc.cjs @@ -223,5 +223,10 @@ module.exports = { 'jest/unbound-method': 'error', }, }, + { + extends: ['plugin:jsonc/recommended-with-jsonc'], + files: ['*.json', '*.json5', '*.jsonc'], + parser: 'jsonc-eslint-parser', + }, ], } diff --git a/backend/package.json b/backend/package.json index 5dc8ca81f..c53a7d48c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-backend", - "version": "3.4.0", + "version": "3.5.0", "description": "GraphQL Backend for ocelot.social", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", @@ -12,7 +12,7 @@ "build": "tsc && tsc-alias && ./scripts/build.copy.files.sh", "dev": "nodemon --exec ts-node --require tsconfig-paths/register src/index.ts -e js,ts,gql", "dev:debug": "nodemon --exec node --inspect=0.0.0.0:9229 build/src/index.js -e js,ts,gql", - "lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts,.cjs .", + "lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts,.cjs,.json,.json5,.jsonc .", "test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles", "db:reset": "ts-node --require tsconfig-paths/register src/db/reset.ts", "db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts", @@ -106,6 +106,7 @@ "eslint-import-resolver-typescript": "^4.3.4", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", + "eslint-plugin-jsonc": "^2.20.0", "eslint-plugin-n": "^17.17.0", "eslint-plugin-no-catch-all": "^1.1.0", "eslint-plugin-prettier": "^5.2.6", diff --git a/backend/public/providers.json b/backend/public/providers.json index ef9f04bff..28b10de5b 100644 --- a/backend/public/providers.json +++ b/backend/public/providers.json @@ -1,257 +1,234 @@ [ - { - "provider_name": "Codepen", - "provider_url": "https:\/\/codepen.io", - "endpoints": [ - { - "schemes": [ - "http:\/\/codepen.io\/*", - "https:\/\/codepen.io\/*" - ], - "url": "http:\/\/codepen.io\/api\/oembed" - } - ] - }, - { - "provider_name": "DTube", - "provider_url": "https:\/\/d.tube\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/d.tube\/v\/*" - ], - "url": "https:\/\/api.d.tube\/oembed", - "discovery": true - } - ] - }, - { - "provider_name": "Facebook (Post)", - "provider_url": "https:\/\/www.facebook.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/www.facebook.com\/*\/posts\/*", - "https:\/\/www.facebook.com\/photos\/*", - "https:\/\/www.facebook.com\/*\/photos\/*", - "https:\/\/www.facebook.com\/photo.php*", - "https:\/\/www.facebook.com\/photo.php", - "https:\/\/www.facebook.com\/*\/activity\/*", - "https:\/\/www.facebook.com\/permalink.php", - "https:\/\/www.facebook.com\/media\/set?set=*", - "https:\/\/www.facebook.com\/questions\/*", - "https:\/\/www.facebook.com\/notes\/*\/*\/*" - ], - "url": "https:\/\/www.facebook.com\/plugins\/post\/oembed.json", - "discovery": true - } - ] - }, - { - "provider_name": "Facebook (Video)", - "provider_url": "https:\/\/www.facebook.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/www.facebook.com\/*\/videos\/*", - "https:\/\/www.facebook.com\/video.php" - ], - "url": "https:\/\/www.facebook.com\/plugins\/video\/oembed.json", - "discovery": true - } - ] - }, - { - "provider_name": "Flickr", - "provider_url": "https:\/\/www.flickr.com\/", - "endpoints": [ - { - "schemes": [ - "http:\/\/*.flickr.com\/photos\/*", - "http:\/\/flic.kr\/p\/*", - "https:\/\/*.flickr.com\/photos\/*", - "https:\/\/flic.kr\/p\/*" - ], - "url": "https:\/\/www.flickr.com\/services\/oembed\/", - "discovery": true - } - ] - }, - { - "provider_name": "GIPHY", - "provider_url": "https:\/\/giphy.com", - "endpoints": [ - { - "schemes": [ - "https:\/\/giphy.com\/gifs\/*", - "http:\/\/gph.is\/*", - "https:\/\/media.giphy.com\/media\/*\/giphy.gif" - ], - "url": "https:\/\/giphy.com\/services\/oembed", - "discovery": true - } - ] - }, - { - "provider_name": "Instagram", - "provider_url": "https:\/\/instagram.com", - "endpoints": [ - { - "schemes": [ - "http:\/\/instagram.com\/p\/*", - "http:\/\/instagr.am\/p\/*", - "http:\/\/www.instagram.com\/p\/*", - "http:\/\/www.instagr.am\/p\/*", - "https:\/\/instagram.com\/p\/*", - "https:\/\/instagr.am\/p\/*", - "https:\/\/www.instagram.com\/p\/*", - "https:\/\/www.instagr.am\/p\/*" - ], - "url": "https:\/\/api.instagram.com\/oembed", - "formats": [ - "json" - ] - } - ] - }, - { - "provider_name": "Meetup", - "provider_url": "http:\/\/www.meetup.com", - "endpoints": [ - { - "schemes": [ - "http:\/\/meetup.com\/*", - "https:\/\/www.meetup.com\/*", - "https:\/\/meetup.com\/*", - "http:\/\/meetu.ps\/*" - ], - "url": "https:\/\/api.meetup.com\/oembed", - "formats": [ - "json" - ] - } - ] - }, - { - "provider_name": "MixCloud", - "provider_url": "https:\/\/mixcloud.com\/", - "endpoints": [ - { - "schemes": [ - "http:\/\/www.mixcloud.com\/*\/*\/", - "https:\/\/www.mixcloud.com\/*\/*\/" - ], - "url": "https:\/\/www.mixcloud.com\/oembed\/" - } - ] - }, - { - "provider_name": "Reddit", - "provider_url": "https:\/\/reddit.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/reddit.com\/r\/*\/comments\/*\/*", - "https:\/\/www.reddit.com\/r\/*\/comments\/*\/*" - ], - "url": "https:\/\/www.reddit.com\/oembed" - } - ] - }, - { - "provider_name": "SlideShare", - "provider_url": "http:\/\/www.slideshare.net\/", - "endpoints": [ - { - "schemes": [ - "http:\/\/www.slideshare.net\/*\/*", - "http:\/\/fr.slideshare.net\/*\/*", - "http:\/\/de.slideshare.net\/*\/*", - "http:\/\/es.slideshare.net\/*\/*", - "http:\/\/pt.slideshare.net\/*\/*" - ], - "url": "http:\/\/www.slideshare.net\/api\/oembed\/2", - "discovery": true - } - ] - }, - { - "provider_name": "SoundCloud", - "provider_url": "http:\/\/soundcloud.com\/", - "endpoints": [ - { - "schemes": [ - "http:\/\/soundcloud.com\/*", - "https:\/\/soundcloud.com\/*" - ], - "url": "https:\/\/soundcloud.com\/oembed" - } - ] - }, - { - "provider_name": "Twitch", - "provider_url": "https:\/\/www.twitch.tv", - "endpoints": [ - { - "schemes": [ - "http:\/\/clips.twitch.tv\/*", - "https:\/\/clips.twitch.tv\/*", - "http:\/\/www.twitch.tv\/*", - "https:\/\/www.twitch.tv\/*", - "http:\/\/twitch.tv\/*", - "https:\/\/twitch.tv\/*" - ], - "url": "https:\/\/api.twitch.tv\/v4\/oembed", - "formats": [ - "json" - ] - } - ] - }, - { - "provider_name": "Twitter", - "provider_url": "http:\/\/www.twitter.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/twitter.com\/*\/status\/*", - "https:\/\/*.twitter.com\/*\/status\/*" - ], - "url": "https:\/\/publish.twitter.com\/oembed" - } - ] - }, - { - "provider_name": "Vimeo", - "provider_url": "https:\/\/vimeo.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/vimeo.com\/*", - "https:\/\/vimeo.com\/album\/*\/video\/*", - "https:\/\/vimeo.com\/channels\/*\/*", - "https:\/\/vimeo.com\/groups\/*\/videos\/*", - "https:\/\/vimeo.com\/ondemand\/*\/*", - "https:\/\/player.vimeo.com\/video\/*" - ], - "url": "https:\/\/vimeo.com\/api\/oembed.{format}", - "discovery": true - } - ] - }, - { - "provider_name": "YouTube", - "provider_url": "https:\/\/www.youtube.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/*.youtube.com\/watch*", - "https:\/\/*.youtube.com\/v\/*", - "https:\/\/youtu.be\/*" - ], - "url": "https:\/\/www.youtube.com\/oembed", - "discovery": true - } - ] - } -] \ No newline at end of file + { + "provider_name": "Codepen", + "provider_url": "https://codepen.io", + "endpoints": [ + { + "schemes": ["http://codepen.io/*", "https://codepen.io/*"], + "url": "http://codepen.io/api/oembed" + } + ] + }, + { + "provider_name": "DTube", + "provider_url": "https://d.tube/", + "endpoints": [ + { + "schemes": ["https://d.tube/v/*"], + "url": "https://api.d.tube/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Facebook (Post)", + "provider_url": "https://www.facebook.com/", + "endpoints": [ + { + "schemes": [ + "https://www.facebook.com/*/posts/*", + "https://www.facebook.com/photos/*", + "https://www.facebook.com/*/photos/*", + "https://www.facebook.com/photo.php*", + "https://www.facebook.com/photo.php", + "https://www.facebook.com/*/activity/*", + "https://www.facebook.com/permalink.php", + "https://www.facebook.com/media/set?set=*", + "https://www.facebook.com/questions/*", + "https://www.facebook.com/notes/*/*/*" + ], + "url": "https://www.facebook.com/plugins/post/oembed.json", + "discovery": true + } + ] + }, + { + "provider_name": "Facebook (Video)", + "provider_url": "https://www.facebook.com/", + "endpoints": [ + { + "schemes": ["https://www.facebook.com/*/videos/*", "https://www.facebook.com/video.php"], + "url": "https://www.facebook.com/plugins/video/oembed.json", + "discovery": true + } + ] + }, + { + "provider_name": "Flickr", + "provider_url": "https://www.flickr.com/", + "endpoints": [ + { + "schemes": [ + "http://*.flickr.com/photos/*", + "http://flic.kr/p/*", + "https://*.flickr.com/photos/*", + "https://flic.kr/p/*" + ], + "url": "https://www.flickr.com/services/oembed/", + "discovery": true + } + ] + }, + { + "provider_name": "GIPHY", + "provider_url": "https://giphy.com", + "endpoints": [ + { + "schemes": [ + "https://giphy.com/gifs/*", + "http://gph.is/*", + "https://media.giphy.com/media/*/giphy.gif" + ], + "url": "https://giphy.com/services/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Instagram", + "provider_url": "https://instagram.com", + "endpoints": [ + { + "schemes": [ + "http://instagram.com/p/*", + "http://instagr.am/p/*", + "http://www.instagram.com/p/*", + "http://www.instagr.am/p/*", + "https://instagram.com/p/*", + "https://instagr.am/p/*", + "https://www.instagram.com/p/*", + "https://www.instagr.am/p/*" + ], + "url": "https://api.instagram.com/oembed", + "formats": ["json"] + } + ] + }, + { + "provider_name": "Meetup", + "provider_url": "http://www.meetup.com", + "endpoints": [ + { + "schemes": [ + "http://meetup.com/*", + "https://www.meetup.com/*", + "https://meetup.com/*", + "http://meetu.ps/*" + ], + "url": "https://api.meetup.com/oembed", + "formats": ["json"] + } + ] + }, + { + "provider_name": "MixCloud", + "provider_url": "https://mixcloud.com/", + "endpoints": [ + { + "schemes": ["http://www.mixcloud.com/*/*/", "https://www.mixcloud.com/*/*/"], + "url": "https://www.mixcloud.com/oembed/" + } + ] + }, + { + "provider_name": "Reddit", + "provider_url": "https://reddit.com/", + "endpoints": [ + { + "schemes": [ + "https://reddit.com/r/*/comments/*/*", + "https://www.reddit.com/r/*/comments/*/*" + ], + "url": "https://www.reddit.com/oembed" + } + ] + }, + { + "provider_name": "SlideShare", + "provider_url": "http://www.slideshare.net/", + "endpoints": [ + { + "schemes": [ + "http://www.slideshare.net/*/*", + "http://fr.slideshare.net/*/*", + "http://de.slideshare.net/*/*", + "http://es.slideshare.net/*/*", + "http://pt.slideshare.net/*/*" + ], + "url": "http://www.slideshare.net/api/oembed/2", + "discovery": true + } + ] + }, + { + "provider_name": "SoundCloud", + "provider_url": "http://soundcloud.com/", + "endpoints": [ + { + "schemes": ["http://soundcloud.com/*", "https://soundcloud.com/*"], + "url": "https://soundcloud.com/oembed" + } + ] + }, + { + "provider_name": "Twitch", + "provider_url": "https://www.twitch.tv", + "endpoints": [ + { + "schemes": [ + "http://clips.twitch.tv/*", + "https://clips.twitch.tv/*", + "http://www.twitch.tv/*", + "https://www.twitch.tv/*", + "http://twitch.tv/*", + "https://twitch.tv/*" + ], + "url": "https://api.twitch.tv/v4/oembed", + "formats": ["json"] + } + ] + }, + { + "provider_name": "Twitter", + "provider_url": "http://www.twitter.com/", + "endpoints": [ + { + "schemes": ["https://twitter.com/*/status/*", "https://*.twitter.com/*/status/*"], + "url": "https://publish.twitter.com/oembed" + } + ] + }, + { + "provider_name": "Vimeo", + "provider_url": "https://vimeo.com/", + "endpoints": [ + { + "schemes": [ + "https://vimeo.com/*", + "https://vimeo.com/album/*/video/*", + "https://vimeo.com/channels/*/*", + "https://vimeo.com/groups/*/videos/*", + "https://vimeo.com/ondemand/*/*", + "https://player.vimeo.com/video/*" + ], + "url": "https://vimeo.com/api/oembed.{format}", + "discovery": true + } + ] + }, + { + "provider_name": "YouTube", + "provider_url": "https://www.youtube.com/", + "endpoints": [ + { + "schemes": [ + "https://*.youtube.com/watch*", + "https://*.youtube.com/v/*", + "https://youtu.be/*" + ], + "url": "https://www.youtube.com/oembed", + "discovery": true + } + ] + } +] diff --git a/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap index fd7b90395..57b256a12 100644 --- a/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap +++ b/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap @@ -62,6 +62,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -185,6 +189,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; diff --git a/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap b/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap new file mode 100644 index 000000000..34c945d65 --- /dev/null +++ b/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap @@ -0,0 +1,261 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendEmailVerification English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Hello User,

+
+
+

So, you want to change your e-mail? No problem! Just click the button below to verify your new address:

Verify e-mail address +

If you don't want to change your e-mail address feel free to ignore this message.

+

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "New E-Mail Address ocelot.social", + "text": "HELLO USER, + +So, you want to change your e-mail? No problem! Just click the button below to +verify your new address: + +Verify e-mail address +[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456] + +If you don't want to change your e-mail address feel free to ignore this +message. + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendEmailVerification German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo User,

+
+
+

Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:

E-Mail Adresse bestätigen +

Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren.

+

Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: 123456

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Neue E-Mail Addresse ocelot.social", + "text": "HALLO USER, + +Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button +kannst Du Deine neue E-Mail Adresse bestätigen: + +E-Mail Adresse bestätigen +[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456] + +Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese +Nachricht einfach ignorieren. + +Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in +Dein Browserfenster kopieren: 123456 + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap index 698ae9082..0fec27b7c 100644 --- a/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap +++ b/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap @@ -62,6 +62,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -184,6 +188,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -308,6 +316,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -432,6 +444,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -556,6 +572,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -679,6 +699,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -801,6 +825,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -920,6 +948,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1043,6 +1075,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1166,6 +1202,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1288,6 +1328,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1412,6 +1456,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1536,6 +1584,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1660,6 +1712,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1783,6 +1839,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1905,6 +1965,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -2024,6 +2088,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -2147,6 +2215,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; diff --git a/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap new file mode 100644 index 000000000..3b8d1c077 --- /dev/null +++ b/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap @@ -0,0 +1,559 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendRegistrationMail with invite code English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:

Confirm your e-mail address +

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+

However, this only works if you have registered through our website.

+

If you didn't sign up for ocelot.social we recommend you to check it out! It's a social network from people for people who want to connect and change the world together. +

+

PS: If you ignore this e-mail we will not create an account for you. ;)

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Welcome to ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +Thank you for joining our cause – it's awesome to have you on board. There's +just one tiny step missing before we can start shaping the world together … +Please confirm your e-mail address by clicking the button below: + +Confirm your e-mail address +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code] + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +However, this only works if you have registered through our website. + +If you didn't sign up for ocelot.social we recommend you to check it out! It's a +social network from people for people who want to connect and change the world +together. + +PS: If you ignore this e-mail we will not create an account for you. ;) + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail with invite code German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:

Bestätige Deine E-Mail Adresse +

Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: 123456

+

Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.

+

Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. +

+

PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Willkommen bei ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt +fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können +… Bitte bestätige Deine E-Mail Adresse: + +Bestätige Deine E-Mail Adresse +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code] + +Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in +Dein Browserfenster kopieren: 123456 + +Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert +hast. + +Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal +vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. + +PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach +ignorieren. ;) + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail without invite code English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:

Confirm your e-mail address +

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+

However, this only works if you have registered through our website.

+

If you didn't sign up for ocelot.social we recommend you to check it out! It's a social network from people for people who want to connect and change the world together. +

+

PS: If you ignore this e-mail we will not create an account for you. ;)

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Welcome to ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +Thank you for joining our cause – it's awesome to have you on board. There's +just one tiny step missing before we can start shaping the world together … +Please confirm your e-mail address by clicking the button below: + +Confirm your e-mail address +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail] + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +However, this only works if you have registered through our website. + +If you didn't sign up for ocelot.social we recommend you to check it out! It's a +social network from people for people who want to connect and change the world +together. + +PS: If you ignore this e-mail we will not create an account for you. ;) + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail without invite code German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:

Bestätige Deine E-Mail Adresse +

Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: 123456

+

Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.

+

Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. +

+

PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Willkommen bei ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt +fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können +… Bitte bestätige Deine E-Mail Adresse: + +Bestätige Deine E-Mail Adresse +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail] + +Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in +Dein Browserfenster kopieren: 123456 + +Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert +hast. + +Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal +vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. + +PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach +ignorieren. ;) + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap new file mode 100644 index 000000000..3d8c6ac27 --- /dev/null +++ b/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap @@ -0,0 +1,260 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendResetPasswordMail English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:

Confirm your e-mail address +

If you didn't request a new password feel free to ignore this e-mail.

+

If the above button doesn't work you can also copy the following code into your browser window: 123456

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Reset Password ocelot.social", + "text": "HELLO JENNY ROSTOCK, + +So, you forgot your password? No problem! Just click the button below to reset +it within the next 24 hours: + +Confirm your e-mail address +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +If you didn't request a new password feel free to ignore this e-mail. + +If the above button doesn't work you can also copy the following code into your +browser window: 123456 + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendResetPasswordMail German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:

Bestätige Deine E-Mail Adresse +

Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.

+

Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: 123456

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Neues Passwort ocelot.social", + "text": "HALLO JENNY ROSTOCK, + +Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button +kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen: + +Bestätige Deine E-Mail Adresse +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach +ignorieren. + +Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in +Dein Browserfenster kopieren: 123456 + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap b/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap new file mode 100644 index 000000000..72acc52cd --- /dev/null +++ b/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap @@ -0,0 +1,255 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendWrongEmail English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address. Did you maybe use another one when you signed up?

Try a different e-mail +

If you don't have an account at ocelot.social yet or if you didn't want to reset your password, please ignore this e-mail. +

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Wrong E-mail? ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +You requested a password reset but unfortunately we couldn't find an account +associated with your e-mail address. Did you maybe use another one when you +signed up? + +Try a different e-mail [http://webapp:3000/password-reset/request] + +If you don't have an account at ocelot.social yet or if you didn't want to reset +your password, please ignore this e-mail. + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendWrongEmail German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer anderen Adresse bei uns angemeldet bist?

Versuch' es mit einer anderen E-Mail +

Wenn du noch keinen Account bei ocelot.social hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren! +

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Falsche Mailaddresse? ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen +Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer +anderen Adresse bei uns angemeldet bist? + +Versuch' es mit einer anderen E-Mail [http://webapp:3000/password-reset/request] + +Wenn du noch keinen Account bei ocelot.social hast oder dein Password gar nicht +ändern willst, kannst du diese E-Mail einfach ignorieren! + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/locales/de.json b/backend/src/emails/locales/de.json index d09991262..9e0ce843a 100644 --- a/backend/src/emails/locales/de.json +++ b/backend/src/emails/locales/de.json @@ -7,12 +7,32 @@ "followedUserPosted": "Neuer Beitrag von gefolgtem Nutzer", "mentionedInComment": "Erwähnung in Kommentar", "mentionedInPost": "Erwähnung in Beitrag", + "newEmail": "Neue E-Mail Addresse", "removedUserFromGroup": "Aus Gruppe entfernt", "postInGroup": "Neuer Beitrag in Gruppe", + "resetPassword": "Neues Passwort", "userJoinedGroup": "Nutzer tritt Gruppe bei", - "userLeftGroup": "Nutzer verlässt Gruppe" + "userLeftGroup": "Nutzer verlässt Gruppe", + "wrongEmail": "Falsche Mailaddresse?" + }, + "registration": { + "introduction": "Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:", + "codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ", + "codeHintException": "Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.", + "notYouStart": "Falls Du Dich nicht selbst bei ", + "notYouEnd": " angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.", + "ps": "PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)" + }, + "emailVerification": { + "codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ", + "introduction": "Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:", + "doNotChange": "Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. " }, "buttons": { + "confirmEmail": "Bestätige Deine E-Mail Adresse", + "resetPassword": "Passwort zurücksetzen", + "tryAgain": "Versuch' es mit einer anderen E-Mail", + "verifyEmail": "E-Mail Adresse bestätigen", "viewChat": "Chat anzeigen", "viewComment": "Kommentar ansehen", "viewGroup": "Gruppe ansehen", @@ -23,7 +43,19 @@ "seeYou": "Bis bald bei ", "yourTeam": "– Dein {team} Team", "settingsHint": "PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine ", - "settingsName": "Benachrichtigungseinstellungen" + "settingsName": "Benachrichtigungseinstellungen", + "welcome": "Willkommen bei" + }, + "resetPassword": { + "codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: ", + "ignore": "Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.", + "introduction": "Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:" + }, + "wrongEmail": { + "codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ", + "ignoreEnd": " hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren!", + "ignoreStart": "Wenn du noch keinen Account bei ", + "introduction": "Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer anderen Adresse bei uns angemeldet bist?" }, "changedGroupMemberRole": "deine Rolle in der Gruppe „{groupName}“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:", "chatMessageStart": "du hast eine neue Chat-Nachricht von ", diff --git a/backend/src/emails/locales/en.json b/backend/src/emails/locales/en.json index f14f469ae..30ca64655 100644 --- a/backend/src/emails/locales/en.json +++ b/backend/src/emails/locales/en.json @@ -7,12 +7,32 @@ "followedUserPosted": "New post by followd user", "mentionedInComment": "Mentioned in comment", "mentionedInPost": "Mentioned in post", + "newEmail": "New E-Mail Address", "removedUserFromGroup": "Removed from group", "postInGroup": "New post in group", + "resetPassword": "Reset Password", "userJoinedGroup": "User joined group", - "userLeftGroup": "User left group" + "userLeftGroup": "User left group", + "wrongEmail": "Wrong E-mail?" + }, + "registration": { + "introduction": "Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:", + "codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ", + "codeHintException": "However, this only works if you have registered through our website.", + "notYouStart": "If you didn't sign up for ", + "notYouEnd": " we recommend you to check it out! It's a social network from people for people who want to connect and change the world together.", + "ps": "PS: If you ignore this e-mail we will not create an account for you. ;)" + }, + "emailVerification": { + "codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ", + "introduction": "So, you want to change your e-mail? No problem! Just click the button below to verify your new address:", + "doNotChange": "If you don't want to change your e-mail address feel free to ignore this message. " }, "buttons": { + "confirmEmail": "Confirm your e-mail address", + "resetPassword": "Reset password", + "tryAgain": "Try a different e-mail", + "verifyEmail": "Verify e-mail address", "viewChat": "Show Chat", "viewComment": "View comment", "viewGroup": "View group", @@ -23,7 +43,18 @@ "seeYou": "See you soon on ", "yourTeam": "– The {team} Team", "settingsHint": "PS: If you don't want to receive e-mails anymore, change your ", - "settingsName": "notification settings" + "settingsName": "notification settings", + "welcome": "Welcome to" + }, + "resetPassword": { + "codeHint": "If the above button doesn't work you can also copy the following code into your browser window: ", + "ignore": "If you didn't request a new password feel free to ignore this e-mail.", + "introduction": "So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:" + }, + "wrongEmail": { + "ignoreEnd": " yet or if you didn't want to reset your password, please ignore this e-mail.", + "ignoreStart": "If you don't have an account at ", + "introduction": "You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address. Did you maybe use another one when you signed up?" }, "changedGroupMemberRole": "your role in the group “{groupName}” has been changed. Click on the button to view this group:", "chatMessageStart": "you have received a new chat message from ", diff --git a/backend/src/emails/sendEmail.ts b/backend/src/emails/sendEmail.ts index 460a3984a..7b7ea76b3 100644 --- a/backend/src/emails/sendEmail.ts +++ b/backend/src/emails/sendEmail.ts @@ -28,6 +28,7 @@ const defaultParams = { ORGANIZATION_URL: CONFIG.ORGANIZATION_URL, supportUrl: CONFIG.SUPPORT_URL, settingsUrl, + renderSettingsUrl: true, } export const transport = createTransport({ @@ -202,3 +203,137 @@ export const sendChatMessageMail = async ( throw new Error(error) } } + +interface VerifyMailInput { + email: string + nonce: string + locale: string +} + +interface RegistrationMailInput extends VerifyMailInput { + inviteCode?: string +} + +export const sendRegistrationMail = async ( + data: RegistrationMailInput, +): Promise => { + const { nonce, locale, inviteCode } = data + const to = data.email + const actionUrl = new URL('/registration', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + if (inviteCode) { + actionUrl.searchParams.set('inviteCode', inviteCode) + actionUrl.searchParams.set('method', 'invite-code') + } else { + actionUrl.searchParams.set('method', 'invite-mail') + } + + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'registration'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +interface EmailVerificationInput extends VerifyMailInput { + name: string +} + +export const sendEmailVerification = async ( + data: EmailVerificationInput, +): Promise => { + const { nonce, locale, name } = data + const to = data.email + const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'emailVerification'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + name, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +export const sendResetPasswordMail = async ( + data: EmailVerificationInput, +): Promise => { + const { nonce, locale, name } = data + const to = data.email + const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'resetPassword'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + name, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +export const sendWrongEmail = async (data: { + locale: string + email: string +}): Promise => { + const { locale } = data + const to = data.email + const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI) + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'wrongEmail'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} diff --git a/backend/src/emails/sendEmailVerification.spec.ts b/backend/src/emails/sendEmailVerification.spec.ts new file mode 100644 index 000000000..0863dd9db --- /dev/null +++ b/backend/src/emails/sendEmailVerification.spec.ts @@ -0,0 +1,35 @@ +import { sendEmailVerification } from './sendEmail' + +describe('sendEmailVerification', () => { + const data: { + email: string + nonce: string + locale: string + name: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + name: 'User', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendEmailVerification(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendEmailVerification(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/sendRegistrationMail.spec.ts b/backend/src/emails/sendRegistrationMail.spec.ts new file mode 100644 index 000000000..ea66771c2 --- /dev/null +++ b/backend/src/emails/sendRegistrationMail.spec.ts @@ -0,0 +1,63 @@ +import { sendRegistrationMail } from './sendEmail' + +describe('sendRegistrationMail', () => { + const data: { + email: string + nonce: string + locale: string + inviteCode?: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + inviteCode: 'welcome', + } + + describe('with invite code', () => { + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + data.inviteCode = 'welcome' + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + data.inviteCode = 'welcome' + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + }) + + describe('without invite code', () => { + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + delete data.inviteCode + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + delete data.inviteCode + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + }) +}) diff --git a/backend/src/emails/sendResetPasswordMail.spec.ts b/backend/src/emails/sendResetPasswordMail.spec.ts new file mode 100644 index 000000000..e37af2e7b --- /dev/null +++ b/backend/src/emails/sendResetPasswordMail.spec.ts @@ -0,0 +1,35 @@ +import { sendResetPasswordMail } from './sendEmail' + +describe('sendResetPasswordMail', () => { + const data: { + email: string + nonce: string + locale: string + name: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + name: 'Jenny Rostock', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/sendWrongEmail.spec.ts b/backend/src/emails/sendWrongEmail.spec.ts new file mode 100644 index 000000000..854d935f9 --- /dev/null +++ b/backend/src/emails/sendWrongEmail.spec.ts @@ -0,0 +1,31 @@ +import { sendWrongEmail } from './sendEmail' + +describe('sendWrongEmail', () => { + const data: { + email: string + locale: string + } = { + email: 'user@example.org', + locale: 'en', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendWrongEmail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendWrongEmail(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/templates/emailVerification/html.pug b/backend/src/emails/templates/emailVerification/html.pug new file mode 100644 index 000000000..7483106e4 --- /dev/null +++ b/backend/src/emails/templates/emailVerification/html.pug @@ -0,0 +1,10 @@ +extend ../layout.pug + +block content + .content + p= t('emailVerification.introduction') + a.button(href=actionUrl)= t('buttons.verifyEmail') + p= t('emailVerification.doNotChange') + + p= t('emailVerification.codeHint') + span= nonce diff --git a/backend/src/emails/templates/emailVerification/subject.pug b/backend/src/emails/templates/emailVerification/subject.pug new file mode 100644 index 000000000..5fc98a7b9 --- /dev/null +++ b/backend/src/emails/templates/emailVerification/subject.pug @@ -0,0 +1 @@ += `${t('subjects.newEmail')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/includes/greeting.pug b/backend/src/emails/templates/includes/greeting.pug index 26ae259c5..6b682fc2d 100644 --- a/backend/src/emails/templates/includes/greeting.pug +++ b/backend/src/emails/templates/includes/greeting.pug @@ -3,12 +3,15 @@ - var organizationUrl = ORGANIZATION_URL - var team = APPLICATION_NAME - var settingsUrl = settingsUrl + - var renderSettingsUrl = renderSettingsUrl p= t('general.seeYou') a.organization(href=organizationUrl)= team | ! p= t('general.yourTeam', { team }) - br - p= t('general.settingsHint') - a.settings(href=settingsUrl)= t('general.settingsName') - | ! + + if renderSettingsUrl + br + p= t('general.settingsHint') + a.settings(href=settingsUrl)= t('general.settingsName') + | ! diff --git a/backend/src/emails/templates/includes/webflow.css b/backend/src/emails/templates/includes/webflow.css index c7ea12921..1dc1f0b24 100644 --- a/backend/src/emails/templates/includes/webflow.css +++ b/backend/src/emails/templates/includes/webflow.css @@ -50,6 +50,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; diff --git a/backend/src/emails/templates/includes/welcome.pug b/backend/src/emails/templates/includes/welcome.pug new file mode 100644 index 000000000..f4ec6f8bd --- /dev/null +++ b/backend/src/emails/templates/includes/welcome.pug @@ -0,0 +1 @@ +h2= `${t('general.welcome')} ${APPLICATION_NAME}!` \ No newline at end of file diff --git a/backend/src/emails/templates/layout.pug b/backend/src/emails/templates/layout.pug index 898776323..faaadb5d3 100644 --- a/backend/src/emails/templates/layout.pug +++ b/backend/src/emails/templates/layout.pug @@ -13,11 +13,15 @@ html(lang=locale) .wf-force-outline-none[tabindex="-1"]:focus{outline:none;} style include includes/webflow.css - + + - var name = name body div.container include includes/header.pug - include includes/salutation.pug + if name + include includes/salutation.pug + else + include includes/welcome.pug .wrapper block content diff --git a/backend/src/emails/templates/registration/html.pug b/backend/src/emails/templates/registration/html.pug new file mode 100644 index 000000000..b50aaca31 --- /dev/null +++ b/backend/src/emails/templates/registration/html.pug @@ -0,0 +1,15 @@ +extend ../layout.pug + +block content + .content + p= t('registration.introduction') + a.button(href=actionUrl)= t('buttons.confirmEmail') + p= t('registration.codeHint') + span= nonce + p= t('registration.codeHintException') + + p= t('registration.notYouStart') + a(href=ORGANIZATION_LINK)= APPLICATION_NAME + = t('registration.notYouEnd') + + p= t('registration.ps') \ No newline at end of file diff --git a/backend/src/emails/templates/registration/subject.pug b/backend/src/emails/templates/registration/subject.pug new file mode 100644 index 000000000..7e9dbec7f --- /dev/null +++ b/backend/src/emails/templates/registration/subject.pug @@ -0,0 +1 @@ += `${t('general.welcome')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/resetPassword/html.pug b/backend/src/emails/templates/resetPassword/html.pug new file mode 100644 index 000000000..f10ee01c2 --- /dev/null +++ b/backend/src/emails/templates/resetPassword/html.pug @@ -0,0 +1,9 @@ +extend ../layout.pug + +block content + .content + p= t('resetPassword.introduction') + a.button(href=actionUrl)= t('buttons.confirmEmail') + p= t('resetPassword.ignore') + p= t('resetPassword.codeHint') + span= nonce diff --git a/backend/src/emails/templates/resetPassword/subject.pug b/backend/src/emails/templates/resetPassword/subject.pug new file mode 100644 index 000000000..047af2052 --- /dev/null +++ b/backend/src/emails/templates/resetPassword/subject.pug @@ -0,0 +1 @@ += `${t('subjects.resetPassword')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/wrongEmail/html.pug b/backend/src/emails/templates/wrongEmail/html.pug new file mode 100644 index 000000000..79f97833f --- /dev/null +++ b/backend/src/emails/templates/wrongEmail/html.pug @@ -0,0 +1,10 @@ +extend ../layout.pug + +block content + .content + p= t('wrongEmail.introduction') + a.button(href=actionUrl)= t('buttons.tryAgain') + + p= t('wrongEmail.ignoreStart') + a(href=ORGANIZATION_LINK)= APPLICATION_NAME + = t('wrongEmail.ignoreEnd') diff --git a/backend/src/emails/templates/wrongEmail/subject.pug b/backend/src/emails/templates/wrongEmail/subject.pug new file mode 100644 index 000000000..b6bc2d01c --- /dev/null +++ b/backend/src/emails/templates/wrongEmail/subject.pug @@ -0,0 +1 @@ += `${t('subjects.wrongEmail')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/graphql/resolvers/emails.ts b/backend/src/graphql/resolvers/emails.ts index be721dda5..0491c86ad 100644 --- a/backend/src/graphql/resolvers/emails.ts +++ b/backend/src/graphql/resolvers/emails.ts @@ -69,6 +69,7 @@ export default { ) return result.records.map((record) => ({ name: record.get('user').properties.name, + locale: record.get('user').properties.locale, ...record.get('email').properties, })) }) diff --git a/backend/src/graphql/resolvers/passwordReset.spec.ts b/backend/src/graphql/resolvers/passwordReset.spec.ts index d5d08265c..3bc4d53ba 100644 --- a/backend/src/graphql/resolvers/passwordReset.spec.ts +++ b/backend/src/graphql/resolvers/passwordReset.spec.ts @@ -71,14 +71,14 @@ describe('passwordReset', () => { describe('requestPasswordReset', () => { const mutation = gql` - mutation ($email: String!) { - requestPasswordReset(email: $email) + mutation ($email: String!, $locale: String!) { + requestPasswordReset(email: $email, locale: $locale) } ` describe('with invalid email', () => { beforeEach(() => { - variables = { ...variables, email: 'non-existent@example.org' } + variables = { ...variables, email: 'non-existent@example.org', locale: 'de' } }) it('resolves anyways', async () => { @@ -96,7 +96,7 @@ describe('passwordReset', () => { describe('with a valid email', () => { beforeEach(() => { - variables = { ...variables, email: 'user@example.org' } + variables = { ...variables, email: 'user@example.org', locale: 'de' } }) it('resolves', async () => { diff --git a/backend/src/graphql/resolvers/registration.spec.ts b/backend/src/graphql/resolvers/registration.spec.ts index ccf5a9e10..d959b348a 100644 --- a/backend/src/graphql/resolvers/registration.spec.ts +++ b/backend/src/graphql/resolvers/registration.spec.ts @@ -50,14 +50,14 @@ afterEach(async () => { describe('Signup', () => { const mutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } ` beforeEach(() => { - variables = { ...variables, email: 'someuser@example.org' } + variables = { ...variables, email: 'someuser@example.org', locale: 'de' } }) describe('unauthenticated', () => { diff --git a/backend/src/graphql/types/type/EmailAddress.gql b/backend/src/graphql/types/type/EmailAddress.gql index b2e65eafa..261b97207 100644 --- a/backend/src/graphql/types/type/EmailAddress.gql +++ b/backend/src/graphql/types/type/EmailAddress.gql @@ -9,7 +9,11 @@ type Query { } type Mutation { - Signup(email: String!, inviteCode: String = null): EmailAddress + Signup( + email: String! + locale: String! + inviteCode: String = null + ): EmailAddress SignupVerification( nonce: String! email: String! diff --git a/backend/src/graphql/types/type/User.gql b/backend/src/graphql/types/type/User.gql index 81dd9cf5b..83de35c37 100644 --- a/backend/src/graphql/types/type/User.gql +++ b/backend/src/graphql/types/type/User.gql @@ -245,7 +245,7 @@ type Mutation { updateOnlineStatus(status: OnlineStatus!): Boolean! - requestPasswordReset(email: String!): Boolean! + requestPasswordReset(email: String!, locale: String!): Boolean! resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean! changePassword(oldPassword: String!, newPassword: String!): String! diff --git a/backend/src/middleware/login/loginMiddleware.ts b/backend/src/middleware/login/loginMiddleware.ts index b67e5f60a..35f3df702 100644 --- a/backend/src/middleware/login/loginMiddleware.ts +++ b/backend/src/middleware/login/loginMiddleware.ts @@ -2,43 +2,41 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { sendMail } from '@middleware/helpers/email/sendMail' import { - signupTemplate, - resetPasswordTemplate, - wrongAccountTemplate, - emailVerificationTemplate, -} from '@middleware/helpers/email/templateBuilder' + sendRegistrationMail, + sendEmailVerification, + sendResetPasswordMail, +} from '@src/emails/sendEmail' const sendSignupMail = async (resolve, root, args, context, resolveInfo) => { - const { inviteCode } = args + const { inviteCode, locale } = args const response = await resolve(root, args, context, resolveInfo) const { email, nonce } = response if (nonce) { // emails that already exist do not have a nonce - if (inviteCode) { - await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } })) - } else { - await sendMail(signupTemplate({ email, variables: { nonce } })) - } + await sendRegistrationMail({ email, nonce, locale, inviteCode }) } delete response.nonce return response } const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo) => { - const { email } = args + const { email, locale } = args const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo) - const template = userFound ? resetPasswordTemplate : wrongAccountTemplate - await sendMail(template({ email, variables: { nonce, name } })) + if (userFound) { + await sendResetPasswordMail({ email, nonce, name, locale }) + } else { + // this is an antifeature allowing unauthenticated users to spam any email with wrong-email notifications + // await sendWrongEmail({ email, locale }) + } return true } const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => { const response = await resolve(root, args, context, resolveInfo) - const { email, nonce, name } = response + const { email, nonce, name, locale } = response if (nonce) { - await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } })) + await sendEmailVerification({ email, nonce, name, locale }) } delete response.nonce return response diff --git a/backend/src/middleware/permissionsMiddleware.spec.ts b/backend/src/middleware/permissionsMiddleware.spec.ts index ca45005fe..e8089b7f3 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.ts +++ b/backend/src/middleware/permissionsMiddleware.spec.ts @@ -177,8 +177,8 @@ describe('authorization', () => { describe('access Signup', () => { const signupMutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } @@ -189,6 +189,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } CONFIG.INVITE_REGISTRATION = false CONFIG.PUBLIC_REGISTRATION = false @@ -231,6 +232,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } CONFIG.INVITE_REGISTRATION = false CONFIG.PUBLIC_REGISTRATION = true @@ -269,6 +271,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } authenticatedUser = null }) @@ -288,6 +291,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'no valid invite code', + locale: 'de', } authenticatedUser = null }) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 7ef3f47b0..7da05a2f0 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -25,11 +25,12 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "commonjs" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + /* Specify a set of entries that re-map imports to additional lookup locations. */ + "paths": { "@config/*": ["./src/config/*"], "@constants/*": ["./src/constants/*"], "@context/*": ["./src/context/*"], @@ -66,7 +67,7 @@ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./build", /* Specify an output folder for all emitted files. */ + "outDir": "./build" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -88,19 +89,19 @@ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - "useUnknownInCatchVariables": false, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "useUnknownInCatchVariables": false /* Default catch clause variables as 'unknown' instead of 'any'. */, // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ @@ -115,6 +116,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } diff --git a/backend/yarn.lock b/backend/yarn.lock index 688f5faad..4d6b2e2d3 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -408,6 +408,13 @@ dependencies: eslint-visitor-keys "^3.4.3" +"@eslint-community/eslint-utils@^4.5.1": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + "@eslint-community/regexpp@^4.11.0": version "4.12.1" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" @@ -1841,6 +1848,11 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.14.0, acorn@^8.5.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + acorn@^8.4.1: version "8.9.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" @@ -4034,6 +4046,13 @@ eslint-compat-utils@^0.5.1: dependencies: semver "^7.5.4" +eslint-compat-utils@^0.6.4: + version "0.6.5" + resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.6.5.tgz#6b06350a1c947c4514cfa64a170a6bfdbadc7ec2" + integrity sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ== + dependencies: + semver "^7.5.4" + eslint-config-prettier@^10.1.2: version "10.1.2" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276" @@ -4065,6 +4084,13 @@ eslint-import-resolver-typescript@^4.3.4: tinyglobby "^0.2.13" unrs-resolver "^1.6.3" +eslint-json-compat-utils@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.1.tgz#32931d42c723da383712f25177a2c57b9ef5f079" + integrity sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg== + dependencies: + esquery "^1.6.0" + eslint-module-utils@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" @@ -4113,6 +4139,20 @@ eslint-plugin-jest@^28.11.0: dependencies: "@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0" +eslint-plugin-jsonc@^2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.20.0.tgz#7f3ae51abd38176487ba7324dee77578a92e15e0" + integrity sha512-FRgCn9Hzk5eKboCbVMrr9QrhM0eO4G+WKH8IFXoaeqhM/2kuWzbStJn4kkr0VWL8J5H8RYZF+Aoam1vlBaZVkw== + dependencies: + "@eslint-community/eslint-utils" "^4.5.1" + eslint-compat-utils "^0.6.4" + eslint-json-compat-utils "^0.2.1" + espree "^9.6.1 || ^10.3.0" + graphemer "^1.4.0" + jsonc-eslint-parser "^2.4.0" + natural-compare "^1.4.0" + synckit "^0.6.2 || ^0.7.3 || ^0.10.3" + eslint-plugin-n@^17.17.0: version "17.17.0" resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-17.17.0.tgz#6644433d395c2ecae0b2fe58018807e85d8e0724" @@ -4170,11 +4210,16 @@ eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + eslint@^8.57.1: version "8.57.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" @@ -4229,7 +4274,7 @@ esniff@^2.0.1: event-emitter "^0.3.5" type "^2.7.2" -espree@^9.6.0, espree@^9.6.1: +espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -4238,6 +4283,15 @@ espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" +"espree@^9.6.1 || ^10.3.0": + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" + esprima@^1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.5.tgz#0993502feaf668138325756f30f9a51feeec11e9" @@ -4255,6 +4309,13 @@ esquery@^1.4.2: dependencies: estraverse "^5.1.0" +esquery@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -6529,6 +6590,16 @@ json5@^2.2.2, json5@^2.2.3, json5@^2.x: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-eslint-parser@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz#74ded53f9d716e8d0671bd167bf5391f452d5461" + integrity sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg== + dependencies: + acorn "^8.5.0" + eslint-visitor-keys "^3.0.0" + espree "^9.0.0" + semver "^7.3.5" + jsonwebtoken@^8.3.0, jsonwebtoken@~8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -9121,7 +9192,14 @@ string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9225,6 +9303,14 @@ synckit@^0.11.0: "@pkgr/core" "^0.2.0" tslib "^2.8.1" +"synckit@^0.6.2 || ^0.7.3 || ^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.10.3.tgz#940aea2c7b6d141a4f74dbdebc81e0958c331a4b" + integrity sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ== + dependencies: + "@pkgr/core" "^0.2.0" + tslib "^2.8.1" + tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -10087,7 +10173,16 @@ with@^7.0.0: assert-never "^1.2.1" babel-walk "3.0.0-canary-5" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@7.0.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== diff --git a/cypress/support/step_definitions/Post.Comment/I_should_see_my_comment.js b/cypress/support/step_definitions/Post.Comment/I_should_see_my_comment.js index 707a7397f..332379dcc 100644 --- a/cypress/support/step_definitions/Post.Comment/I_should_see_my_comment.js +++ b/cypress/support/step_definitions/Post.Comment/I_should_see_my_comment.js @@ -8,6 +8,6 @@ defineStep('I should see my comment', () => { .get('.profile-avatar img') .should('have.attr', 'src') .and('contain', 'https://') // some url - .get('.user-teaser > .info > .text') + .get('.user-teaser .info > .text') .should('contain', 'today at') }) diff --git a/deployment/helm/charts/ocelot-neo4j/Chart.yaml b/deployment/helm/charts/ocelot-neo4j/Chart.yaml index e9f8e2354..989e1a683 100644 --- a/deployment/helm/charts/ocelot-neo4j/Chart.yaml +++ b/deployment/helm/charts/ocelot-neo4j/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "3.4.0" +appVersion: "3.5.0" diff --git a/deployment/helm/charts/ocelot-social/Chart.yaml b/deployment/helm/charts/ocelot-social/Chart.yaml index 2a872480e..32793ee79 100644 --- a/deployment/helm/charts/ocelot-social/Chart.yaml +++ b/deployment/helm/charts/ocelot-social/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "3.4.0" +appVersion: "3.5.0" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4a8a0442c..58af310dd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocelot-social-frontend", - "version": "3.4.0", + "version": "3.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocelot-social-frontend", - "version": "3.4.0", + "version": "3.5.0", "license": "Apache-2.0", "dependencies": { "@intlify/unplugin-vue-i18n": "^2.0.0", diff --git a/frontend/package.json b/frontend/package.json index 5e2235be5..3278dfffd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-frontend", - "version": "3.4.0", + "version": "3.5.0", "description": "ocelot.social new Frontend (in development and not fully implemented) by IT4C Boilerplate for frontends", "main": "build/index.js", "type": "module", diff --git a/package.json b/package.json index 2754268fc..9c03f0323 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social", - "version": "3.4.0", + "version": "3.5.0", "description": "Free and open source software program code available to run social networks.", "author": "ocelot.social Community", "license": "MIT", diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index 578484355..42c01d3a8 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -150,7 +150,7 @@ $font-size-xx-large: 2rem; $font-size-x-large: 1.5rem; $font-size-large: 1.25rem; $font-size-base: 1rem; -$font-size-body: 15px; +$font-size-body: 0.938rem; $font-size-small: 0.8rem; $font-size-x-small: 0.7rem; $font-size-xx-small: 0.6rem; @@ -359,37 +359,37 @@ $media-query-medium: (min-width: 768px); $media-query-large: (min-width: 1024px); $media-query-x-large: (min-width: 1200px); -/** +/** * @tokens Background Images */ -/** +/** * @tokens Header Color */ $color-header-background: $color-neutral-100; -/** +/** * @tokens Footer Color */ $color-footer-background: $color-neutral-100; $color-footer-link: $color-primary; -/** +/** * @tokens Locale Menu Color */ $color-locale-menu: $text-color-soft; -/** +/** * @tokens Donation Bar Color */ $color-donation-bar: $color-primary; $color-donation-bar-light: $color-primary-light; -/** +/** * @tokens Toast Color */ diff --git a/webapp/assets/styles/main.scss b/webapp/assets/styles/main.scss index b726758c7..4fba0b5e0 100644 --- a/webapp/assets/styles/main.scss +++ b/webapp/assets/styles/main.scss @@ -52,11 +52,6 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1); background: #fff; } -body.dropdown-open { - height: 100vh; - overflow: hidden; -} - blockquote { display: block; padding: 15px 20px 15px 45px; @@ -140,6 +135,10 @@ hr { opacity: 1; transition-delay: 0; transition: opacity 80ms ease-out; + + @media(hover: none) { + pointer-events: all; + } } } @@ -155,7 +154,6 @@ hr { [class$='menu-popover'] { min-width: 130px; - a, button { display: flex; align-content: center; @@ -179,4 +177,4 @@ hr { .dropdown-arrow { font-size: $font-size-xx-small; -} \ No newline at end of file +} diff --git a/webapp/components/Dropdown.vue b/webapp/components/Dropdown.vue index 7e4d21223..dd2b4a822 100644 --- a/webapp/components/Dropdown.vue +++ b/webapp/components/Dropdown.vue @@ -43,32 +43,12 @@ export default { }, watch: { isPopoverOpen: { - immediate: true, handler(isOpen) { - try { - if (isOpen) { - this.$nextTick(() => { - setTimeout(() => { - const paddingRightStyle = `${ - window.innerWidth - document.documentElement.clientWidth - }px` - const navigationElement = document.querySelector('.main-navigation') - document.body.style.paddingRight = paddingRightStyle - document.body.classList.add('dropdown-open') - if (navigationElement) { - navigationElement.style.paddingRight = paddingRightStyle - } - }, 20) - }) - } else { - const navigationElement = document.querySelector('.main-navigation') - document.body.style.paddingRight = null - document.body.classList.remove('dropdown-open') - if (navigationElement) { - navigationElement.style.paddingRight = null - } - } - } catch (err) {} + if (isOpen) { + document.body.classList.add('dropdown-open') + } else { + document.body.classList.remove('dropdown-open') + } }, }, }, diff --git a/webapp/components/Notification/Notification.vue b/webapp/components/Notification/Notification.vue index a657b10ba..d83995b9b 100644 --- a/webapp/components/Notification/Notification.vue +++ b/webapp/components/Notification/Notification.vue @@ -4,6 +4,7 @@

{{ $t(`notifications.reason.${notification.reason}`) }}

diff --git a/webapp/components/NotificationMenu/NotificationMenu.vue b/webapp/components/NotificationMenu/NotificationMenu.vue index 276da8490..576abb213 100644 --- a/webapp/components/NotificationMenu/NotificationMenu.vue +++ b/webapp/components/NotificationMenu/NotificationMenu.vue @@ -127,7 +127,7 @@ export default { apollo: { notifications: { query() { - return notificationQuery(this.$i18n) + return notificationQuery() }, variables() { return { diff --git a/webapp/components/NotificationsTable/NotificationsTable.spec.js b/webapp/components/NotificationsTable/NotificationsTable.spec.js index 0d3560787..5fbfc338a 100644 --- a/webapp/components/NotificationsTable/NotificationsTable.spec.js +++ b/webapp/components/NotificationsTable/NotificationsTable.spec.js @@ -88,7 +88,7 @@ describe('NotificationsTable.vue', () => { }) it('renders the author', () => { - const userinfo = firstRowNotification.find('.user-teaser > .info') + const userinfo = firstRowNotification.find('.user-teaser .info') expect(userinfo.text()).toContain(postNotification.from.author.name) }) @@ -121,7 +121,7 @@ describe('NotificationsTable.vue', () => { }) it('renders the author', () => { - const userinfo = secondRowNotification.find('.user-teaser > .info') + const userinfo = secondRowNotification.find('.user-teaser .info') expect(userinfo.text()).toContain(commentNotification.from.author.name) }) diff --git a/webapp/components/PasswordReset/Request.spec.js b/webapp/components/PasswordReset/Request.spec.js index 50d6495bd..e2f082242 100644 --- a/webapp/components/PasswordReset/Request.spec.js +++ b/webapp/components/PasswordReset/Request.spec.js @@ -59,7 +59,12 @@ describe('Request', () => { }) it('delivers email to backend', () => { - const expected = expect.objectContaining({ variables: { email: 'mail@example.org' } }) + const expected = expect.objectContaining({ + variables: { + email: 'mail@example.org', + locale: 'en', + }, + }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) @@ -92,7 +97,12 @@ describe('Request', () => { }) it('normalizes email to lower case letters', () => { - const expected = expect.objectContaining({ variables: { email: 'mail@gmail.com' } }) + const expected = expect.objectContaining({ + variables: { + email: 'mail@gmail.com', + locale: 'en', + }, + }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) }) diff --git a/webapp/components/PasswordReset/Request.vue b/webapp/components/PasswordReset/Request.vue index 5398c13ed..3eebeba65 100644 --- a/webapp/components/PasswordReset/Request.vue +++ b/webapp/components/PasswordReset/Request.vue @@ -85,13 +85,13 @@ export default { }, async handleSubmit() { const mutation = gql` - mutation ($email: String!) { - requestPasswordReset(email: $email) + mutation ($email: String!, $locale: String!) { + requestPasswordReset(email: $email, locale: $locale) } ` try { const { email } = this - await this.$apollo.mutate({ mutation, variables: { email } }) + await this.$apollo.mutate({ mutation, variables: { email, locale: this.$i18n.locale() } }) this.submitted = true setTimeout(() => { diff --git a/webapp/components/Registration/RegistrationSlideEmail.vue b/webapp/components/Registration/RegistrationSlideEmail.vue index 6d6454ac9..96441dee8 100644 --- a/webapp/components/Registration/RegistrationSlideEmail.vue +++ b/webapp/components/Registration/RegistrationSlideEmail.vue @@ -36,8 +36,8 @@ import normalizeEmail from '~/components/utils/NormalizeEmail' import translateErrorMessage from '~/components/utils/TranslateErrorMessage' export const SignupMutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } @@ -140,7 +140,7 @@ export default { async onNextClick() { const { email } = this.formData const { inviteCode = null } = this.sliderData.collectedInputData - const variables = { email, inviteCode } + const variables = { email, inviteCode, locale: this.$i18n.locale() } if (this.sliderData.collectedInputData.emailSend && !this.sendEmailAgain) { return true diff --git a/webapp/components/Registration/Signup.spec.js b/webapp/components/Registration/Signup.spec.js index 7ef2dc994..2ee413b8b 100644 --- a/webapp/components/Registration/Signup.spec.js +++ b/webapp/components/Registration/Signup.spec.js @@ -25,6 +25,9 @@ describe('Signup', () => { loading: false, mutate: jest.fn().mockResolvedValue({ data: { Signup: { email: 'mail@example.org' } } }), }, + $i18n: { + locale: () => 'de', + }, } propsData = {} }) @@ -64,7 +67,7 @@ describe('Signup', () => { it('delivers email to backend', () => { const expected = expect.objectContaining({ mutation: SignupMutation, - variables: { email: 'mAIL@exAMPLE.org', inviteCode: null }, + variables: { email: 'mAIL@exAMPLE.org', locale: 'de', inviteCode: null }, }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) diff --git a/webapp/components/Registration/Signup.vue b/webapp/components/Registration/Signup.vue index 91b9ecd61..156c43d4e 100644 --- a/webapp/components/Registration/Signup.vue +++ b/webapp/components/Registration/Signup.vue @@ -70,8 +70,8 @@ import { SweetalertIcon } from 'vue-sweetalert-icons' import translateErrorMessage from '~/components/utils/TranslateErrorMessage' export const SignupMutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } @@ -121,7 +121,7 @@ export default { try { const response = await this.$apollo.mutate({ mutation: SignupMutation, - variables: { email, inviteCode: null }, + variables: { email, locale: this.$i18n.locale(), inviteCode: null }, }) this.data = response.data setTimeout(() => { diff --git a/webapp/components/UserTeaser/LocationInfo.spec.js b/webapp/components/UserTeaser/LocationInfo.spec.js new file mode 100644 index 000000000..2b100e66d --- /dev/null +++ b/webapp/components/UserTeaser/LocationInfo.spec.js @@ -0,0 +1,31 @@ +import { render } from '@testing-library/vue' +import LocationInfo from './LocationInfo.vue' + +const localVue = global.localVue + +describe('LocationInfo', () => { + const Wrapper = ({ withDistance }) => { + return render(LocationInfo, { + localVue, + propsData: { + locationData: { + name: 'Paris', + distanceToMe: withDistance ? 100 : null, + }, + }, + mocks: { + $t: jest.fn((t) => t), + }, + }) + } + + it('renders with distance', () => { + const wrapper = Wrapper({ withDistance: true }) + expect(wrapper.container).toMatchSnapshot() + }) + + it('renders without distance', () => { + const wrapper = Wrapper({ withDistance: false }) + expect(wrapper.container).toMatchSnapshot() + }) +}) diff --git a/webapp/components/UserTeaser/LocationInfo.vue b/webapp/components/UserTeaser/LocationInfo.vue new file mode 100644 index 000000000..67dc46c27 --- /dev/null +++ b/webapp/components/UserTeaser/LocationInfo.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/webapp/components/UserTeaser/UserTeaser.spec.js b/webapp/components/UserTeaser/UserTeaser.spec.js index 354308109..8a67285ac 100644 --- a/webapp/components/UserTeaser/UserTeaser.spec.js +++ b/webapp/components/UserTeaser/UserTeaser.spec.js @@ -1,113 +1,250 @@ -import { mount, RouterLinkStub } from '@vue/test-utils' +import { render, screen, fireEvent } from '@testing-library/vue' +import { RouterLinkStub } from '@vue/test-utils' import UserTeaser from './UserTeaser.vue' import Vuex from 'vuex' const localVue = global.localVue -const filter = jest.fn((str) => str) -localVue.filter('truncate', filter) +// Mock Math.random, used in Dropdown +Object.assign(Math, { + random: () => 0, +}) + +const waitForPopover = async () => await new Promise((resolve) => setTimeout(resolve, 1000)) + +let mockIsTouchDevice +jest.mock('../utils/isTouchDevice', () => ({ + isTouchDevice: jest.fn(() => mockIsTouchDevice), +})) + +const userTilda = { + name: 'Tilda Swinton', + slug: 'tilda-swinton', + id: 'user1', + avatar: '/avatars/tilda-swinton', + badgeVerification: { + id: 'bv1', + icon: '/icons/verified', + description: 'Verified', + isDefault: false, + }, + badgeTrophiesSelected: [ + { + id: 'trophy1', + icon: '/icons/trophy1', + description: 'Trophy 1', + isDefault: false, + }, + { + id: 'trophy2', + icon: '/icons/trophy2', + description: 'Trophy 2', + isDefault: false, + }, + { + id: 'empty', + icon: '/icons/empty', + description: 'Empty', + isDefault: true, + }, + ], +} describe('UserTeaser', () => { - let propsData - let mocks - let stubs - let getters + const Wrapper = ({ + isModerator = false, + withLinkToProfile = true, + onTouchScreen = false, + withAvatar = true, + user = userTilda, + withPopoverEnabled = true, + }) => { + mockIsTouchDevice = onTouchScreen - beforeEach(() => { - propsData = {} - - mocks = { - $t: jest.fn(), - } - stubs = { - NuxtLink: RouterLinkStub, - } - getters = { - 'auth/user': () => { - return {} + const store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return {} + }, + 'auth/isModerator': () => isModerator, }, - 'auth/isModerator': () => false, - } + }) + return render(UserTeaser, { + localVue, + store, + propsData: { + user, + linkToProfile: withLinkToProfile, + showAvatar: withAvatar, + showPopover: withPopoverEnabled, + }, + stubs: { + NuxtLink: RouterLinkStub, + 'user-teaser-popover': true, + 'v-popover': true, + 'client-only': true, + }, + mocks: { + $t: jest.fn((t) => t), + }, + }) + } + + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: null }) + expect(wrapper.container).toMatchSnapshot() }) - describe('mount', () => { - const Wrapper = () => { - const store = new Vuex.Store({ - getters, + describe('given an user', () => { + describe('without linkToProfile, on touch screen', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: true }) }) - return mount(UserTeaser, { store, propsData, mocks, stubs, localVue }) - } - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).toBe('') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when clicking the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.click(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('when clicking the user avatar', () => { + beforeEach(async () => { + const userAvatar = screen.getByAltText('Tilda Swinton') + await fireEvent.click(userAvatar) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) }) - describe('given an user', () => { + describe('with linkToProfile, on touch screen', () => { + let wrapper beforeEach(() => { - propsData.user = { - name: 'Tilda Swinton', - slug: 'tilda-swinton', - } + wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: true }) }) - it('renders user name', () => { - const wrapper = Wrapper() - expect(mocks.$t).not.toHaveBeenCalledWith('profile.userAnonym') - expect(wrapper.text()).toMatch('Tilda Swinton') + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() }) - describe('user is deleted', () => { - beforeEach(() => { - propsData.user.deleted = true + describe('when clicking the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.click(userName) }) + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('without linkToProfile, on desktop', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: false }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when hovering the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.mouseOver(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('when hovering the user avatar', () => { + beforeEach(async () => { + const userAvatar = screen.getByAltText('Tilda Swinton') + await fireEvent.mouseOver(userAvatar) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('with linkToProfile, on desktop', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: false }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when hovering the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.mouseOver(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('avatar is disabled', () => { + it('does not render the avatar', () => { + const wrapper = Wrapper({ withAvatar: false }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('user is deleted', () => { + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: { ...userTilda, deleted: true } }) + expect(wrapper.container).toMatchSnapshot() + }) + + describe('even if the current user is a moderator', () => { it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') - }) - - describe('even if the current user is a moderator', () => { - beforeEach(() => { - getters['auth/isModerator'] = () => true - }) - - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') + const wrapper = Wrapper({ + user: { ...userTilda, deleted: true }, + isModerator: true, }) + expect(wrapper.container).toMatchSnapshot() }) }) + }) - describe('user is disabled', () => { - beforeEach(() => { - propsData.user.disabled = true - }) + describe('user is disabled', () => { + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: { ...userTilda, disabled: true } }) + expect(wrapper.container).toMatchSnapshot() + }) - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') - }) - - describe('current user is a moderator', () => { - beforeEach(() => { - getters['auth/isModerator'] = () => true - }) - - it('renders user name', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Anonymous') - expect(wrapper.text()).toMatch('Tilda Swinton') - }) - - it('has "disabled-content" class', () => { - const wrapper = Wrapper() - expect(wrapper.classes()).toContain('disabled-content') - }) + describe('current user is a moderator', () => { + it('renders user name', () => { + const wrapper = Wrapper({ user: { ...userTilda, disabled: true }, isModerator: true }) + expect(wrapper.container).toMatchSnapshot() }) }) }) diff --git a/webapp/components/UserTeaser/UserTeaser.vue b/webapp/components/UserTeaser/UserTeaser.vue index a9e556bf4..4bc72576e 100644 --- a/webapp/components/UserTeaser/UserTeaser.vue +++ b/webapp/components/UserTeaser/UserTeaser.vue @@ -4,57 +4,34 @@ {{ $t('profile.userAnonym') }}
-
- - - - -
-
- - - {{ userSlug }} - {{ userName }} - - - - {{ userSlug }} - {{ userName }} - -   - - - {{ $t('group.in') }} - - - - {{ groupSlug }} - {{ groupName }} - - - -
- - - - - -
-
+ + + +
diff --git a/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap new file mode 100644 index 000000000..50ce23f9a --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocationInfo renders with distance 1`] = ` +
+
+
+ + + + + Paris + +
+ +
+ location.distance +
+
+
+`; + +exports[`LocationInfo renders without distance 1`] = ` +
+
+
+ + + + + Paris + +
+ + +
+
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap new file mode 100644 index 000000000..b8fc6cae9 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap @@ -0,0 +1,1136 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaser given an user avatar is disabled does not render the avatar 1`] = ` +
+
+ + + + + + +
+ + +
+
+`; + +exports[`UserTeaser given an user user is deleted even if the current user is a moderator renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user user is deleted renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user user is disabled current user is a moderator renders user name 1`] = ` +
+ +`; + +exports[`UserTeaser given an user user is disabled renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user with linkToProfile, on desktop renders 1`] = ` +
+ +`; + +exports[`UserTeaser given an user with linkToProfile, on desktop when hovering the user name renders the popover 1`] = ` + +`; + +exports[`UserTeaser given an user with linkToProfile, on touch screen renders 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user with linkToProfile, on touch screen when clicking the user name renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop renders 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop when hovering the user avatar renders the popover 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop when hovering the user name renders the popover 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen renders 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen when clicking the user avatar renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen when clicking the user name renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap new file mode 100644 index 000000000..2257e8a51 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaserHelper with linkToProfile and popover enabled, on touch screen renders button 1`] = ` +
+
+`; + +exports[`UserTeaserHelper with linkToProfile, on desktop renders link 1`] = ` +
+ +
+`; + +exports[`UserTeaserHelper without linkToProfile renders span 1`] = ` +
+ +
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap new file mode 100644 index 000000000..3eab03611 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap @@ -0,0 +1,547 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaserPopover does not show badges when disabled 1`] = ` +
+
+ + + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; + +exports[`UserTeaserPopover given a non-touch device does not show button when userLink is provided 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; + +exports[`UserTeaserPopover given a touch device does not show button when userLink is not provided 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; + +exports[`UserTeaserPopover given a touch device shows button when userLink is provided 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; + +exports[`UserTeaserPopover shows badges when enabled 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; diff --git a/webapp/components/utils/isTouchDevice.js b/webapp/components/utils/isTouchDevice.js new file mode 100644 index 000000000..a6bc17752 --- /dev/null +++ b/webapp/components/utils/isTouchDevice.js @@ -0,0 +1,2 @@ +export const isTouchDevice = () => + 'ontouchstart' in window || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js index 77af830e8..e1704923f 100644 --- a/webapp/graphql/Fragments.js +++ b/webapp/graphql/Fragments.js @@ -17,9 +17,11 @@ export const locationFragment = (lang) => gql` fragment location on User { locationName location { + id name: name${lang} lng lat + distanceToMe } } ` @@ -50,6 +52,19 @@ export const userCountsFragment = gql` } ` +export const userTeaserFragment = (lang) => gql` + ${badgesFragment} + ${locationFragment(lang)} + + fragment userTeaser on User { + followedByCount + contributionsCount + commentedCount + ...badges + ...location + } +` + export const postFragment = gql` fragment post on Post { id diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 75342ef2a..7440b5051 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -7,6 +7,7 @@ import { postFragment, commentFragment, groupFragment, + userTeaserFragment, } from './Fragments' export const profileUserQuery = (i18n) => { @@ -125,7 +126,7 @@ export const mapUserQuery = (i18n) => { ` } -export const notificationQuery = (_i18n) => { +export const notificationQuery = () => { return gql` ${userFragment} ${commentFragment} @@ -483,6 +484,18 @@ export const userDataQuery = (i18n) => { ` } +export const userTeaserQuery = (i18n) => { + const lang = i18n.locale().toUpperCase() + return gql` + ${userTeaserFragment(lang)} + query ($id: ID!) { + User(id: $id) { + ...userTeaser + } + } + ` +} + export const setTrophyBadgeSelected = gql` mutation ($slot: Int!, $badgeId: ID) { setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) { diff --git a/webapp/locales/de.json b/webapp/locales/de.json index df050b191..e2b641b08 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": "Sprache wählen" }, + "location": { + "distance": "{distance} km von mir entfernt" + }, "login": { "email": "Deine E-Mail", "failure": "Fehlerhafte E-Mail-Adresse oder Passwort.", @@ -1173,5 +1176,10 @@ "newTermsAndConditions": "Neue Nutzungsbedingungen", "termsAndConditionsNewConfirm": "Ich habe die neuen Nutzungsbedingungen durchgelesen und stimme zu.", "termsAndConditionsNewConfirmText": "Bitte lies Dir die neuen Nutzungsbedingungen jetzt durch!" + }, + "user-teaser": { + "popover": { + "open-profile": "Profil öffnen" + } } } diff --git a/webapp/locales/en.json b/webapp/locales/en.json index ecd0ec18d..ab34ba66a 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": "Choose language" }, + "location": { + "distance": "{distance} km away from me" + }, "login": { "email": "Your E-mail", "failure": "Incorrect email address or password.", @@ -1173,5 +1176,10 @@ "newTermsAndConditions": "New Terms and Conditions", "termsAndConditionsNewConfirm": "I have read and agree to the new terms of conditions.", "termsAndConditionsNewConfirmText": "Please read the new terms of use now!" + }, + "user-teaser": { + "popover": { + "open-profile": "Open profile" + } } } diff --git a/webapp/locales/es.json b/webapp/locales/es.json index 15096b9d8..b7d95d11c 100644 --- a/webapp/locales/es.json +++ b/webapp/locales/es.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": null }, + "location": { + "distance": null + }, "login": { "email": "Su correo electrónico", "failure": "Dirección de correo electrónico o contraseña incorrecta.", @@ -1173,5 +1176,10 @@ "newTermsAndConditions": "Nuevos términos de uso", "termsAndConditionsNewConfirm": "He leído y acepto los nuevos términos de uso.", "termsAndConditionsNewConfirmText": "¡Por favor, lea los nuevos términos de uso ahora!" + }, + "user-teaser": { + "popover": { + "open-profile": null + } } } diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json index 2da2a9801..37a182c28 100644 --- a/webapp/locales/fr.json +++ b/webapp/locales/fr.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": null }, + "location": { + "distance": null + }, "login": { "email": "Votre mail", "failure": "Adresse mail ou mot de passe incorrect.", @@ -1173,5 +1176,10 @@ "newTermsAndConditions": "Nouvelles conditions générales", "termsAndConditionsNewConfirm": "J'ai lu et accepté les nouvelles conditions générales.", "termsAndConditionsNewConfirmText": "Veuillez lire les nouvelles conditions d'utilisation dès maintenant !" + }, + "user-teaser": { + "popover": { + "open-profile": null + } } } diff --git a/webapp/locales/it.json b/webapp/locales/it.json index 485abff3a..6b686502c 100644 --- a/webapp/locales/it.json +++ b/webapp/locales/it.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": null }, + "location": { + "distance": null + }, "login": { "email": "La tua email", "failure": null, @@ -1173,5 +1176,10 @@ "newTermsAndConditions": "Nuovi Termini e Condizioni", "termsAndConditionsNewConfirm": "Ho letto e accetto le nuove condizioni generali di contratto.", "termsAndConditionsNewConfirmText": "Si prega di leggere le nuove condizioni d'uso ora!" + }, + "user-teaser": { + "popover": { + "open-profile": null + } } } diff --git a/webapp/locales/nl.json b/webapp/locales/nl.json index 40f9aca2e..714ed2b01 100644 --- a/webapp/locales/nl.json +++ b/webapp/locales/nl.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": null }, + "location": { + "distance": null + }, "login": { "email": "Uw E-mail", "failure": null, @@ -1173,5 +1176,10 @@ "newTermsAndConditions": null, "termsAndConditionsNewConfirm": null, "termsAndConditionsNewConfirmText": null + }, + "user-teaser": { + "popover": { + "open-profile": null + } } } diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json index ee332b84b..61a6acf24 100644 --- a/webapp/locales/pl.json +++ b/webapp/locales/pl.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": null }, + "location": { + "distance": null + }, "login": { "email": "Twój adres e-mail", "failure": null, @@ -1173,5 +1176,10 @@ "newTermsAndConditions": null, "termsAndConditionsNewConfirm": null, "termsAndConditionsNewConfirmText": null + }, + "user-teaser": { + "popover": { + "open-profile": null + } } } diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index 54f9b5d99..80172daa3 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": null }, + "location": { + "distance": null + }, "login": { "email": "Seu email", "failure": "Endereço de e-mail ou senha incorretos.", @@ -1173,5 +1176,10 @@ "newTermsAndConditions": "Novos Termos e Condições", "termsAndConditionsNewConfirm": "Eu li e concordo com os novos termos de condições.", "termsAndConditionsNewConfirmText": "Por favor, leia os novos termos de uso agora!" + }, + "user-teaser": { + "popover": { + "open-profile": null + } } } diff --git a/webapp/locales/ru.json b/webapp/locales/ru.json index 4d2e2a357..f7956755c 100644 --- a/webapp/locales/ru.json +++ b/webapp/locales/ru.json @@ -636,6 +636,9 @@ "localeSwitch": { "tooltip": null }, + "location": { + "distance": null + }, "login": { "email": "Электронная почта", "failure": "Неверный адрес электронной почты или пароль.", @@ -1173,5 +1176,10 @@ "newTermsAndConditions": "Новые условия и положения", "termsAndConditionsNewConfirm": "Я прочитал(а) и согласен(на) с новыми условиями.", "termsAndConditionsNewConfirmText": "Пожалуйста, ознакомьтесь с новыми условиями использования!" + }, + "user-teaser": { + "popover": { + "open-profile": null + } } } diff --git a/webapp/maintenance/source/package.json b/webapp/maintenance/source/package.json index df76a77ca..031729a40 100644 --- a/webapp/maintenance/source/package.json +++ b/webapp/maintenance/source/package.json @@ -1,6 +1,6 @@ { "name": "@ocelot-social/maintenance", - "version": "3.4.0", + "version": "3.5.0", "description": "Maintenance page for ocelot.social", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", diff --git a/webapp/package.json b/webapp/package.json index 7af701437..05f00b2ac 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-webapp", - "version": "3.4.0", + "version": "3.5.0", "description": "ocelot.social Frontend", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", diff --git a/webapp/pages/groups/_id/__snapshots__/_slug.spec.js.snap b/webapp/pages/groups/_id/__snapshots__/_slug.spec.js.snap index cb43b8526..68c2b50ba 100644 --- a/webapp/pages/groups/_id/__snapshots__/_slug.spec.js.snap +++ b/webapp/pages/groups/_id/__snapshots__/_slug.spec.js.snap @@ -493,39 +493,52 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a close class="" placement="top-start" > -
- + -
- - PL - - - - - -
-
- -
-
- + PL + + + + + +
+ + +
+
+ Peter Lustig - - - - + + + + + +
- -
-
+
+ +
  • -
    - + -
    - - JR - - - - - -
    -
    - -
    -
    - + JR + + + + + +
    + + +
    +
    + Jenny Rostock - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - BDB - - - - - -
    -
    - -
    -
    - + BDB + + + + + +
    + + +
    +
    + Bob der Baumeister - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - H - - - - - -
    -
    - -
    -
    - + H + + + + + +
    + + +
    +
    + Huey - - - - + + + + + +
    - -
    -
    +
    + +
  • @@ -2304,39 +2364,52 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a close class="" placement="top-start" > -
    - + -
    - - PL - - - - - -
    -
    - -
    -
    - + PL + + + + + +
    + + +
    +
    + Peter Lustig - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - JR - - - - - -
    -
    - -
    -
    - + JR + + + + + +
    + + +
    +
    + Jenny Rostock - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - BDB - - - - - -
    -
    - -
    -
    - + BDB + + + + + +
    + + +
    +
    + Bob der Baumeister - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - H - - - - - -
    -
    - -
    -
    - + H + + + + + +
    + + +
    +
    + Huey - - - - + + + + + +
    - -
    -
    +
    + +
  • @@ -3197,39 +3317,52 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre class="" placement="top-start" > -
    - + -
    - - PL - - - - - -
    -
    - -
    -
    - + PL + + + + + +
    + + +
    +
    + Peter Lustig - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - JR - - - - - -
    -
    - -
    -
    - + JR + + + + + +
    + + +
    +
    + Jenny Rostock - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - BDB - - - - - -
    -
    - -
    -
    - + BDB + + + + + +
    + + +
    +
    + Bob der Baumeister - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - H - - - - - -
    -
    - -
    -
    - + H + + + + + +
    + + +
    +
    + Huey - - - - + + + + + +
    - -
    -
    +
    + +
  • @@ -3956,39 +4136,52 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre class="" placement="top-start" > -
    - + -
    - - PL - - - - - -
    -
    - -
    -
    - + PL + + + + + +
    + + +
    +
    + Peter Lustig - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - JR - - - - - -
    -
    - -
    -
    - + JR + + + + + +
    + + +
    +
    + Jenny Rostock - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - BDB - - - - - -
    -
    - -
    -
    - + BDB + + + + + +
    + + +
    +
    + Bob der Baumeister - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - H - - - - - -
    -
    - -
    -
    - + H + + + + + +
    + + +
    +
    + Huey - - - - + + + + + +
    - -
    -
    +
    + +
  • @@ -4713,39 +4953,52 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre class="" placement="top-start" > -
    - + -
    - - PL - - - - - -
    -
    - -
    -
    - + PL + + + + + +
    + + +
    +
    + Peter Lustig - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - JR - - - - - -
    -
    - -
    -
    - + JR + + + + + +
    + + +
    +
    + Jenny Rostock - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - BDB - - - - - -
    -
    - -
    -
    - + BDB + + + + + +
    + + +
    +
    + Bob der Baumeister - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - H - - - - - -
    -
    - -
    -
    - + H + + + + + +
    + + +
    +
    + Huey - - - - + + + + + +
    - -
    -
    +
    + +
  • @@ -5539,39 +5839,52 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre class="" placement="top-start" > -
    - + -
    - - PL - - - - - -
    -
    - -
    -
    - + PL + + + + + +
    + + +
    +
    + Peter Lustig - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - JR - - - - - -
    -
    - -
    -
    - + JR + + + + + +
    + + +
    +
    + Jenny Rostock - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - BDB - - - - - -
    -
    - -
    -
    - + BDB + + + + + +
    + + +
    +
    + Bob der Baumeister - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - H - - - - - -
    -
    - -
    -
    - + H + + + + + +
    + + +
    +
    + Huey - - - - + + + + + +
    - -
    -
    +
    + +
  • @@ -6479,39 +6839,52 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a hidde class="" placement="top-start" > -
    - + -
    - - PL - - - - - -
    -
    - -
    -
    - + PL + + + + + +
    + + +
    +
    + Peter Lustig - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - JR - - - - - -
    -
    - -
    -
    - + JR + + + + + +
    + + +
    +
    + Jenny Rostock - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - BDB - - - - - -
    -
    - -
    -
    - + BDB + + + + + +
    + + +
    +
    + Bob der Baumeister - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - H - - - - - -
    -
    - -
    -
    - + H + + + + + +
    + + +
    +
    + Huey - - - - + + + + + +
    - -
    -
    +
    + +
  • @@ -7393,39 +7813,52 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a hidde class="" placement="top-start" > -
    - + -
    - - PL - - - - - -
    -
    - -
    -
    - + PL + + + + + +
    + + +
    +
    + Peter Lustig - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - JR - - - - - -
    -
    - -
    -
    - + JR + + + + + +
    + + +
    +
    + Jenny Rostock - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - BDB - - - - - -
    -
    - -
    -
    - + BDB + + + + + +
    + + +
    +
    + Bob der Baumeister - - - - + + + + + +
    - -
    -
    +
    + +
  • -
    - + -
    - - H - - - - - -
    -
    - -
    -
    - + H + + + + + +
    + + +
    +
    + Huey - - - - + + + + + +
    - -
    -
    +
    + +