From 33274e5b9a557b3031060fd54c5e48be4f40b7ee Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 6 May 2025 01:54:13 +0200 Subject: [PATCH] feat(webapp): user teaser popover (#8450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * calculate distance between current user and queried user * fix query for unset location * use database to calculate distance * rename distance to distance to me, 100% calculation done in DB * distanceToMe tests * lint fixes * remove comments * Show user teaser popover with badges, Desktop * Refactor UserTeaser and add mobile popover support * Avoid click propagation (WIP) * Prevent event propagation * Adjust alignment and font sizes * More spacing for statistics * Add distance, simplify user link * Refactor location info into own component * Add tests for UserTeaserPopup * Refactor and test LocationInfo * Query distanceToMe, rename distance to distanceToMe * Update test * Improve tests for UserTeaser, WIP * Fix tests * DistanceToMe on User instead of Location * Revert "DistanceToMe on User instead of Location" This reverts commit 96c9db00a44cd120e47bfe9534d3e066a194744c. * Fix notifications * Refactor UserTeaser and fix location info * Fix group member crash * Show 0 distance * Fit in popover on small screens * Allow access to profile on desktop * Revert backend changes * Load user teaser popover data only when needed * Fix type mismatch * Refactor for clarity and accessibility * Litte refactorings and improvements * Fix popover test * Adapt and fix tests * Fix tests and bugs * Add placeholder * cypress: adapt user teaser locator to changes * Remove delays and scrolling * Disable popovers in notification list and fix layout * Remove flickering * Make overlay catch all pointer events on touch devices * Re-add attribute for E2E test * Fix test, return to mouseover * fix snapshot --------- Co-authored-by: Ulf Gebhardt Co-authored-by: Wolfgang Huß Co-authored-by: mahula --- .../Post.Comment/I_should_see_my_comment.js | 2 +- webapp/assets/_new/styles/tokens.scss | 14 +- webapp/assets/styles/main.scss | 12 +- webapp/components/Dropdown.vue | 30 +- .../components/Notification/Notification.vue | 1 + .../NotificationMenu/NotificationMenu.vue | 2 +- .../NotificationsTable.spec.js | 4 +- .../UserTeaser/LocationInfo.spec.js | 31 + webapp/components/UserTeaser/LocationInfo.vue | 44 + .../components/UserTeaser/UserTeaser.spec.js | 301 +- webapp/components/UserTeaser/UserTeaser.vue | 118 +- .../UserTeaser/UserTeaserHelper.spec.js | 71 + .../UserTeaser/UserTeaserHelper.vue | 46 + .../UserTeaser/UserTeaserNonAnonymous.vue | 123 + .../UserTeaser/UserTeaserPopover.spec.js | 99 + .../UserTeaser/UserTeaserPopover.vue | 103 + .../__snapshots__/LocationInfo.spec.js.snap | 51 + .../__snapshots__/UserTeaser.spec.js.snap | 1136 +++++++ .../UserTeaserHelper.spec.js.snap | 19 + .../UserTeaserPopover.spec.js.snap | 547 ++++ webapp/components/utils/isTouchDevice.js | 2 + webapp/graphql/Fragments.js | 15 + webapp/graphql/User.js | 15 +- webapp/locales/de.json | 8 + webapp/locales/en.json | 8 + webapp/locales/es.json | 8 + webapp/locales/fr.json | 8 + webapp/locales/it.json | 8 + webapp/locales/nl.json | 8 + webapp/locales/pl.json | 8 + webapp/locales/pt.json | 8 + webapp/locales/ru.json | 8 + .../_id/__snapshots__/_slug.spec.js.snap | 2784 ++++++++++------- 33 files changed, 4277 insertions(+), 1365 deletions(-) create mode 100644 webapp/components/UserTeaser/LocationInfo.spec.js create mode 100644 webapp/components/UserTeaser/LocationInfo.vue create mode 100644 webapp/components/UserTeaser/UserTeaserHelper.spec.js create mode 100644 webapp/components/UserTeaser/UserTeaserHelper.vue create mode 100644 webapp/components/UserTeaser/UserTeaserNonAnonymous.vue create mode 100644 webapp/components/UserTeaser/UserTeaserPopover.spec.js create mode 100644 webapp/components/UserTeaser/UserTeaserPopover.vue create mode 100644 webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap create mode 100644 webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap create mode 100644 webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap create mode 100644 webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap create mode 100644 webapp/components/utils/isTouchDevice.js 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/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/UserTeaser/LocationInfo.spec.js b/webapp/components/UserTeaser/LocationInfo.spec.js new file mode 100644 index 000000000..2b100e66d --- /dev/null +++ b/webapp/components/UserTeaser/LocationInfo.spec.js @@ -0,0 +1,31 @@ +import { render } from '@testing-library/vue' +import LocationInfo from './LocationInfo.vue' + +const localVue = global.localVue + +describe('LocationInfo', () => { + const Wrapper = ({ withDistance }) => { + return render(LocationInfo, { + localVue, + propsData: { + locationData: { + name: 'Paris', + distanceToMe: withDistance ? 100 : null, + }, + }, + mocks: { + $t: jest.fn((t) => t), + }, + }) + } + + it('renders with distance', () => { + const wrapper = Wrapper({ withDistance: true }) + expect(wrapper.container).toMatchSnapshot() + }) + + it('renders without distance', () => { + const wrapper = Wrapper({ withDistance: false }) + expect(wrapper.container).toMatchSnapshot() + }) +}) diff --git a/webapp/components/UserTeaser/LocationInfo.vue b/webapp/components/UserTeaser/LocationInfo.vue new file mode 100644 index 000000000..67dc46c27 --- /dev/null +++ b/webapp/components/UserTeaser/LocationInfo.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/webapp/components/UserTeaser/UserTeaser.spec.js b/webapp/components/UserTeaser/UserTeaser.spec.js index 354308109..8a67285ac 100644 --- a/webapp/components/UserTeaser/UserTeaser.spec.js +++ b/webapp/components/UserTeaser/UserTeaser.spec.js @@ -1,113 +1,250 @@ -import { mount, RouterLinkStub } from '@vue/test-utils' +import { render, screen, fireEvent } from '@testing-library/vue' +import { RouterLinkStub } from '@vue/test-utils' import UserTeaser from './UserTeaser.vue' import Vuex from 'vuex' const localVue = global.localVue -const filter = jest.fn((str) => str) -localVue.filter('truncate', filter) +// Mock Math.random, used in Dropdown +Object.assign(Math, { + random: () => 0, +}) + +const waitForPopover = async () => await new Promise((resolve) => setTimeout(resolve, 1000)) + +let mockIsTouchDevice +jest.mock('../utils/isTouchDevice', () => ({ + isTouchDevice: jest.fn(() => mockIsTouchDevice), +})) + +const userTilda = { + name: 'Tilda Swinton', + slug: 'tilda-swinton', + id: 'user1', + avatar: '/avatars/tilda-swinton', + badgeVerification: { + id: 'bv1', + icon: '/icons/verified', + description: 'Verified', + isDefault: false, + }, + badgeTrophiesSelected: [ + { + id: 'trophy1', + icon: '/icons/trophy1', + description: 'Trophy 1', + isDefault: false, + }, + { + id: 'trophy2', + icon: '/icons/trophy2', + description: 'Trophy 2', + isDefault: false, + }, + { + id: 'empty', + icon: '/icons/empty', + description: 'Empty', + isDefault: true, + }, + ], +} describe('UserTeaser', () => { - let propsData - let mocks - let stubs - let getters + const Wrapper = ({ + isModerator = false, + withLinkToProfile = true, + onTouchScreen = false, + withAvatar = true, + user = userTilda, + withPopoverEnabled = true, + }) => { + mockIsTouchDevice = onTouchScreen - beforeEach(() => { - propsData = {} - - mocks = { - $t: jest.fn(), - } - stubs = { - NuxtLink: RouterLinkStub, - } - getters = { - 'auth/user': () => { - return {} + const store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return {} + }, + 'auth/isModerator': () => isModerator, }, - 'auth/isModerator': () => false, - } + }) + return render(UserTeaser, { + localVue, + store, + propsData: { + user, + linkToProfile: withLinkToProfile, + showAvatar: withAvatar, + showPopover: withPopoverEnabled, + }, + stubs: { + NuxtLink: RouterLinkStub, + 'user-teaser-popover': true, + 'v-popover': true, + 'client-only': true, + }, + mocks: { + $t: jest.fn((t) => t), + }, + }) + } + + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: null }) + expect(wrapper.container).toMatchSnapshot() }) - describe('mount', () => { - const Wrapper = () => { - const store = new Vuex.Store({ - getters, + describe('given an user', () => { + describe('without linkToProfile, on touch screen', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: true }) }) - return mount(UserTeaser, { store, propsData, mocks, stubs, localVue }) - } - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).toBe('') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when clicking the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.click(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('when clicking the user avatar', () => { + beforeEach(async () => { + const userAvatar = screen.getByAltText('Tilda Swinton') + await fireEvent.click(userAvatar) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) }) - describe('given an user', () => { + describe('with linkToProfile, on touch screen', () => { + let wrapper beforeEach(() => { - propsData.user = { - name: 'Tilda Swinton', - slug: 'tilda-swinton', - } + wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: true }) }) - it('renders user name', () => { - const wrapper = Wrapper() - expect(mocks.$t).not.toHaveBeenCalledWith('profile.userAnonym') - expect(wrapper.text()).toMatch('Tilda Swinton') + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() }) - describe('user is deleted', () => { - beforeEach(() => { - propsData.user.deleted = true + describe('when clicking the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.click(userName) }) + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('without linkToProfile, on desktop', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: false }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when hovering the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.mouseOver(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('when hovering the user avatar', () => { + beforeEach(async () => { + const userAvatar = screen.getByAltText('Tilda Swinton') + await fireEvent.mouseOver(userAvatar) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('with linkToProfile, on desktop', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: false }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when hovering the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.mouseOver(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('avatar is disabled', () => { + it('does not render the avatar', () => { + const wrapper = Wrapper({ withAvatar: false }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('user is deleted', () => { + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: { ...userTilda, deleted: true } }) + expect(wrapper.container).toMatchSnapshot() + }) + + describe('even if the current user is a moderator', () => { it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') - }) - - describe('even if the current user is a moderator', () => { - beforeEach(() => { - getters['auth/isModerator'] = () => true - }) - - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') + const wrapper = Wrapper({ + user: { ...userTilda, deleted: true }, + isModerator: true, }) + expect(wrapper.container).toMatchSnapshot() }) }) + }) - describe('user is disabled', () => { - beforeEach(() => { - propsData.user.disabled = true - }) + describe('user is disabled', () => { + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: { ...userTilda, disabled: true } }) + expect(wrapper.container).toMatchSnapshot() + }) - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') - }) - - describe('current user is a moderator', () => { - beforeEach(() => { - getters['auth/isModerator'] = () => true - }) - - it('renders user name', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Anonymous') - expect(wrapper.text()).toMatch('Tilda Swinton') - }) - - it('has "disabled-content" class', () => { - const wrapper = Wrapper() - expect(wrapper.classes()).toContain('disabled-content') - }) + describe('current user is a moderator', () => { + it('renders user name', () => { + const wrapper = Wrapper({ user: { ...userTilda, disabled: true }, isModerator: true }) + expect(wrapper.container).toMatchSnapshot() }) }) }) diff --git a/webapp/components/UserTeaser/UserTeaser.vue b/webapp/components/UserTeaser/UserTeaser.vue index a9e556bf4..4bc72576e 100644 --- a/webapp/components/UserTeaser/UserTeaser.vue +++ b/webapp/components/UserTeaser/UserTeaser.vue @@ -4,57 +4,34 @@ {{ $t('profile.userAnonym') }}
-
- - - - -
-
- - - {{ userSlug }} - {{ userName }} - - - - {{ userSlug }} - {{ userName }} - -   - - - {{ $t('group.in') }} - - - - {{ groupSlug }} - {{ groupName }} - - - -
- - - - - -
-
+ + + +
diff --git a/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap new file mode 100644 index 000000000..50ce23f9a --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocationInfo renders with distance 1`] = ` +
+
+
+ + + + + Paris + +
+ +
+ location.distance +
+
+
+`; + +exports[`LocationInfo renders without distance 1`] = ` +
+
+
+ + + + + Paris + +
+ + +
+
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap new file mode 100644 index 000000000..b8fc6cae9 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap @@ -0,0 +1,1136 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaser given an user avatar is disabled does not render the avatar 1`] = ` +
+
+ + + + + + +
+ + +
+
+`; + +exports[`UserTeaser given an user user is deleted even if the current user is a moderator renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user user is deleted renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user user is disabled current user is a moderator renders user name 1`] = ` +
+ +`; + +exports[`UserTeaser given an user user is disabled renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user with linkToProfile, on desktop renders 1`] = ` +
+ +`; + +exports[`UserTeaser given an user with linkToProfile, on desktop when hovering the user name renders the popover 1`] = ` + +`; + +exports[`UserTeaser given an user with linkToProfile, on touch screen renders 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user with linkToProfile, on touch screen when clicking the user name renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop renders 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop when hovering the user avatar renders the popover 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop when hovering the user name renders the popover 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen renders 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen when clicking the user avatar renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen when clicking the user name renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap new file mode 100644 index 000000000..2257e8a51 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaserHelper with linkToProfile and popover enabled, on touch screen renders button 1`] = ` +
+
+`; + +exports[`UserTeaserHelper with linkToProfile, on desktop renders link 1`] = ` +
+ +
+`; + +exports[`UserTeaserHelper without linkToProfile renders span 1`] = ` +
+ +
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap new file mode 100644 index 000000000..3eab03611 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap @@ -0,0 +1,547 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaserPopover does not show badges when disabled 1`] = ` +
+
+ + + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

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

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

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

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

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

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

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

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

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