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 @@
+
+
+
+
+ {{ locationData.name }}
+
+
{{ distance }}
+
+
+
+
+
+
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`] = `
+
+
+
+
+
+
+
+ TS
+
+
+
+
+
+
+
+
+
+
+
+
+ @tilda-swinton
+
+
+
+ Tilda Swinton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`UserTeaser given an user with linkToProfile, on touch screen when clicking the user name renders the popover 1`] = `
+
+
+
+
+
+
+
+ TS
+
+
+
+
+
+
+
+
+
+
+
+
+ @tilda-swinton
+
+
+
+ Tilda Swinton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`UserTeaser given an user without linkToProfile, on desktop renders 1`] = `
+
+
+
+
+
+
+
+ TS
+
+
+
+
+
+
+
+
+
+
+
+
+ @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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`UserTeaser given an user without linkToProfile, on desktop when hovering the user name renders the popover 1`] = `
+
+
+
+
+
+
+
+ TS
+
+
+
+
+
+
+
+
+
+
+
+
+ @tilda-swinton
+
+
+
+ Tilda Swinton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`UserTeaser given an user without linkToProfile, on touch screen renders 1`] = `
+
+
+
+
+
+
+
+ TS
+
+
+
+
+
+
+
+
+
+
+
+
+ @tilda-swinton
+
+
+
+ Tilda Swinton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`UserTeaser given an user without linkToProfile, on touch screen when clicking the user avatar renders the popover 1`] = `
+
+
+
+
+
+
+
+ TS
+
+
+
+
+
+
+
+
+
+
+
+
+ @tilda-swinton
+
+
+
+ Tilda Swinton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`UserTeaser given an user without linkToProfile, on touch screen when clicking the user name renders the popover 1`] = `
+
+
+
+
+
+
+
+ TS
+
+
+
+
+
+
+
+
+
+
+
+
+ @tilda-swinton
+
+
+
+ Tilda Swinton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+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
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+