diff --git a/backend/src/graphql/resolvers/comments.ts b/backend/src/graphql/resolvers/comments.ts
index 3400b1d23..e07c6791d 100644
--- a/backend/src/graphql/resolvers/comments.ts
+++ b/backend/src/graphql/resolvers/comments.ts
@@ -108,10 +108,14 @@ export default {
count: {
postObservingUsersCount:
'-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(related:User) WHERE obs.active = true AND NOT related.deleted AND NOT related.disabled',
+ shoutedCount:
+ '<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
},
boolean: {
isPostObservedByMe:
'MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1',
+ shoutedByCurrentUser:
+ 'MATCH (this) RETURN EXISTS((this)<-[:SHOUTED]-(:User {id: $cypherParams.currentUserId}))',
},
}),
},
diff --git a/backend/src/graphql/types/enum/ShoutTypeEnum.gql b/backend/src/graphql/types/enum/ShoutTypeEnum.gql
index 87fcbc5ff..97c17316f 100644
--- a/backend/src/graphql/types/enum/ShoutTypeEnum.gql
+++ b/backend/src/graphql/types/enum/ShoutTypeEnum.gql
@@ -1,3 +1,4 @@
enum ShoutTypeEnum {
Post
+ Comment
}
\ No newline at end of file
diff --git a/backend/src/graphql/types/type/Comment.gql b/backend/src/graphql/types/type/Comment.gql
index b1fd7a838..f82a44a79 100644
--- a/backend/src/graphql/types/type/Comment.gql
+++ b/backend/src/graphql/types/type/Comment.gql
@@ -53,6 +53,14 @@ type Comment {
)
postObservingUsersCount: Int!
@cypher(statement: "MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(u:User) WHERE obs.active = true AND NOT u.disabled = true AND NOT u.deleted = true RETURN COUNT(DISTINCT u)")
+
+ shoutedByCurrentUser: Boolean!
+ @cypher(statement: "MATCH (this) RETURN EXISTS((this)<-[:SHOUTED]-(:User {id: $cypherParams.currentUserId}))")
+
+ shoutedCount: Int!
+ @cypher(
+ statement: "MATCH (this)<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true RETURN COUNT(DISTINCT related)"
+ )
}
type Query {
diff --git a/backend/src/graphql/types/type/Post.gql b/backend/src/graphql/types/type/Post.gql
index 784a5f5bc..e6b3a00a0 100644
--- a/backend/src/graphql/types/type/Post.gql
+++ b/backend/src/graphql/types/type/Post.gql
@@ -243,9 +243,9 @@ type Mutation {
markTeaserAsViewed(id: ID!): Post
# Shout the given Type and ID
- shout(id: ID!, type: ShoutTypeEnum): Boolean!
+ shout(id: ID!, type: ShoutTypeEnum!): Boolean!
# Unshout the given Type and ID
- unshout(id: ID!, type: ShoutTypeEnum): Boolean!
+ unshout(id: ID!, type: ShoutTypeEnum!): Boolean!
toggleObservePost(id: ID!, value: Boolean!): Post!
}
diff --git a/webapp/components/ActionButton.spec.js b/webapp/components/ActionButton.spec.js
new file mode 100644
index 000000000..3889d26db
--- /dev/null
+++ b/webapp/components/ActionButton.spec.js
@@ -0,0 +1,64 @@
+import { render, screen, fireEvent } from '@testing-library/vue'
+import '@testing-library/jest-dom'
+import ActionButton from './ActionButton.vue'
+
+const localVue = global.localVue
+
+describe('ActionButton.vue', () => {
+ let mocks
+
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn((t) => t),
+ }
+ })
+
+ let wrapper
+ const Wrapper = ({ isDisabled = false } = {}) => {
+ return render(ActionButton, {
+ mocks,
+ localVue,
+ propsData: {
+ icon: 'heart',
+ text: 'Click me',
+ count: 7,
+ disabled: isDisabled,
+ },
+ })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ describe('when not disabled', () => {
+ it('renders', () => {
+ const wrapper = Wrapper()
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ it('shows count', () => {
+ const count = screen.getByText('7')
+ expect(count).toBeInTheDocument()
+ })
+
+ it('button emits click event', async () => {
+ const button = screen.getByRole('button')
+ await fireEvent.click(button)
+ expect(wrapper.emitted().click).toEqual([[]])
+ })
+ })
+
+ describe('when disabled', () => {
+ it('renders', () => {
+ const wrapper = Wrapper({ isDisabled: true })
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ it('button does not emit click event', async () => {
+ const button = screen.getByRole('button')
+ await fireEvent.click(button)
+ expect(wrapper.emitted().click).toEqual([[]])
+ })
+ })
+})
diff --git a/webapp/components/ActionButton.vue b/webapp/components/ActionButton.vue
new file mode 100644
index 000000000..f440deff8
--- /dev/null
+++ b/webapp/components/ActionButton.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
diff --git a/webapp/components/CommentCard/CommentCard.spec.js b/webapp/components/CommentCard/CommentCard.spec.js
index 7764afd1e..78d87e497 100644
--- a/webapp/components/CommentCard/CommentCard.spec.js
+++ b/webapp/components/CommentCard/CommentCard.spec.js
@@ -17,7 +17,7 @@ describe('CommentCard.vue', () => {
postId: 'post42',
}
mocks = {
- $t: jest.fn(),
+ $t: jest.fn((t) => t),
$toast: {
success: jest.fn(),
error: jest.fn(),
diff --git a/webapp/components/CommentCard/CommentCard.vue b/webapp/components/CommentCard/CommentCard.vue
index d805d26ee..448dce76b 100644
--- a/webapp/components/CommentCard/CommentCard.vue
+++ b/webapp/components/CommentCard/CommentCard.vue
@@ -39,15 +39,25 @@
{{ isCollapsed ? $t('comment.show.more') : $t('comment.show.less') }}
-
+
+
+
+
@@ -59,6 +69,7 @@ import ContentMenu from '~/components/ContentMenu/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import CommentForm from '~/components/CommentForm/CommentForm'
import CommentMutations from '~/graphql/CommentMutations'
+import ShoutButton from '~/components/ShoutButton.vue'
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
export default {
@@ -67,6 +78,7 @@ export default {
ContentMenu,
ContentViewer,
CommentForm,
+ ShoutButton,
},
mixins: [scrollToAnchor],
data() {
@@ -98,6 +110,11 @@ export default {
hasLongContent() {
return this.$filters.removeHtml(this.comment.content).length > COMMENT_MAX_UNTRUNCATED_LENGTH
},
+ isAuthor() {
+ const { author } = this.comment
+ if (!author) return false
+ return this.$store.getters['auth/user'].id === author.id
+ },
isUnavailable() {
return (this.comment.deleted || this.comment.disabled) && !this.isModerator
},
@@ -192,19 +209,14 @@ export default {
margin-bottom: $space-small;
}
- > .base-button {
- align-self: flex-end;
+ .actions {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
}
}
-.reply-button {
- float: right;
- top: 0px;
-}
-.reply-button:after {
- clear: both;
-}
-
@keyframes highlight {
0% {
border: $border-size-base solid $color-primary;
@@ -214,3 +226,17 @@ export default {
}
}
+
+
diff --git a/webapp/components/Group/GroupTeaser.vue b/webapp/components/Group/GroupTeaser.vue
index 4a905b417..7e6523048 100644
--- a/webapp/components/Group/GroupTeaser.vue
+++ b/webapp/components/Group/GroupTeaser.vue
@@ -44,7 +44,7 @@
-
+
{
- let mocks
-
const Wrapper = (count = 1, postId = '123', isObserved = true) => {
- return mount(ObserveButton, {
- mocks,
+ return render(ObserveButton, {
+ mocks: {
+ $t: jest.fn((t) => t),
+ },
localVue,
propsData: {
count,
@@ -18,43 +18,39 @@ describe('ObserveButton', () => {
})
}
- let wrapper
-
- beforeEach(() => {
- mocks = {
- $t: jest.fn(),
- }
- })
-
describe('observed', () => {
+ let wrapper
+
beforeEach(() => {
wrapper = Wrapper(1, '123', true)
})
it('renders', () => {
- expect(wrapper.element).toMatchSnapshot()
+ expect(wrapper.container).toMatchSnapshot()
})
- it('emits toggleObservePost with false when clicked', () => {
- const button = wrapper.find('.base-button')
- button.trigger('click')
- expect(wrapper.emitted('toggleObservePost')).toEqual([['123', false]])
+ it('emits toggleObservePost with false when clicked', async () => {
+ const button = screen.getByRole('button')
+ await fireEvent.click(button)
+ expect(wrapper.emitted().toggleObservePost).toEqual([['123', false]])
})
})
describe('unobserved', () => {
+ let wrapper
+
beforeEach(() => {
wrapper = Wrapper(1, '123', false)
})
it('renders', () => {
- expect(wrapper.element).toMatchSnapshot()
+ expect(wrapper.container).toMatchSnapshot()
})
- it('emits toggleObservePost with true when clicked', () => {
- const button = wrapper.find('.base-button')
- button.trigger('click')
- expect(wrapper.emitted('toggleObservePost')).toEqual([['123', true]])
+ it('emits toggleObservePost with true when clicked', async () => {
+ const button = screen.getByRole('button')
+ await fireEvent.click(button)
+ expect(wrapper.emitted().toggleObservePost).toEqual([['123', true]])
})
})
})
diff --git a/webapp/components/ObserveButton.vue b/webapp/components/ObserveButton.vue
index 2c275709b..2c6488a5e 100644
--- a/webapp/components/ObserveButton.vue
+++ b/webapp/components/ObserveButton.vue
@@ -1,26 +1,27 @@
-
-
-
-
- {{ count }}x
- {{ $t('observeButton.observed') }}
-
-
+
-
-
diff --git a/webapp/components/PostTeaser/PostTeaser.vue b/webapp/components/PostTeaser/PostTeaser.vue
index cb2e8b66b..3b880d4d8 100644
--- a/webapp/components/PostTeaser/PostTeaser.vue
+++ b/webapp/components/PostTeaser/PostTeaser.vue
@@ -53,7 +53,7 @@
class="footer"
v-observe-visibility="(isVisible, entry) => visibilityChanged(isVisible, entry, post.id)"
>
-
+
{
beforeEach(() => {
mocks = {
- $t: jest.fn(),
+ $t: jest.fn((t) => t),
$apollo: {
mutate: jest.fn(),
},
}
})
- describe('mount', () => {
- let wrapper
- const Wrapper = () => {
- return mount(ShoutButton, { mocks, localVue })
- }
+ let wrapper
- beforeEach(() => {
- wrapper = Wrapper()
- })
+ const Wrapper = ({ isShouted = false } = {}) => {
+ return render(ShoutButton, { mocks, localVue, propsData: { isShouted } })
+ }
- it('renders button and text', () => {
- expect(mocks.$t).toHaveBeenCalledWith('shoutButton.shouted')
- expect(wrapper.findAll('.base-button')).toHaveLength(1)
- expect(wrapper.findAll('.shout-button-text')).toHaveLength(1)
- expect(wrapper.vm.shouted).toBe(false)
- expect(wrapper.vm.shoutedCount).toBe(0)
- })
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
- it('toggle the button', async () => {
- mocks.$apollo.mutate = jest.fn().mockResolvedValue({ data: { shout: 'WeDoShout' } })
- wrapper.find('.base-button').trigger('click')
- expect(wrapper.vm.shouted).toBe(true)
- expect(wrapper.vm.shoutedCount).toBe(1)
- await Vue.nextTick()
- expect(wrapper.vm.shouted).toBe(true)
- expect(wrapper.vm.shoutedCount).toBe(1)
- })
+ it('renders button and text', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ const button = screen.getByRole('button')
+ expect(button).toBeInTheDocument()
+ })
- it('toggle the button, but backend fails', async () => {
- mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' })
- await wrapper.find('.base-button').trigger('click')
- expect(wrapper.vm.shouted).toBe(true)
- expect(wrapper.vm.shoutedCount).toBe(1)
- await Vue.nextTick()
- expect(wrapper.vm.shouted).toBe(false)
- expect(wrapper.vm.shoutedCount).toBe(0)
+ it('toggle the button', async () => {
+ mocks.$apollo.mutate = jest.fn().mockResolvedValue({ data: { shout: 'WeDoShout' } })
+ const button = screen.getByRole('button')
+ await fireEvent.click(button)
+ expect(wrapper.container).toMatchSnapshot()
+ const shoutedCount = screen.getByText('1')
+ expect(shoutedCount).toBeInTheDocument()
+ })
+
+ it('toggle the button, but backend fails', async () => {
+ mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' })
+ const button = screen.getByRole('button')
+ await fireEvent.click(button)
+ expect(wrapper.container).toMatchSnapshot()
+ let shoutedCount = screen.getByText('1')
+ expect(shoutedCount).toBeInTheDocument()
+ await Vue.nextTick()
+ shoutedCount = screen.getByText('0')
+ expect(shoutedCount).toBeInTheDocument()
+ })
+
+ describe('when shouted', () => {
+ it('renders', () => {
+ wrapper = Wrapper({ isShouted: true })
+ expect(wrapper.container).toMatchSnapshot()
})
})
})
diff --git a/webapp/components/ShoutButton.vue b/webapp/components/ShoutButton.vue
index 4b644bc25..a8aca9e74 100644
--- a/webapp/components/ShoutButton.vue
+++ b/webapp/components/ShoutButton.vue
@@ -1,28 +1,28 @@
-
-
-
-
- {{ shoutedCount }}x
- {{ $t('shoutButton.shouted') }}
-
-
+
-
-
diff --git a/webapp/components/__snapshots__/ActionButton.spec.js.snap b/webapp/components/__snapshots__/ActionButton.spec.js.snap
new file mode 100644
index 000000000..c5d1d4581
--- /dev/null
+++ b/webapp/components/__snapshots__/ActionButton.spec.js.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ActionButton.vue when disabled renders 1`] = `
+
+`;
+
+exports[`ActionButton.vue when not disabled renders 1`] = `
+
+`;
diff --git a/webapp/components/__snapshots__/ObserveButton.spec.js.snap b/webapp/components/__snapshots__/ObserveButton.spec.js.snap
index c3ba629be..064e6256f 100644
--- a/webapp/components/__snapshots__/ObserveButton.spec.js.snap
+++ b/webapp/components/__snapshots__/ObserveButton.spec.js.snap
@@ -1,81 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ObserveButton observed renders 1`] = `
-
-
-
+
-
-
-
- 1x
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ 1
+
+
`;
exports[`ObserveButton unobserved renders 1`] = `
-
-
-
+
-
-
-
- 1x
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ 1
+
+
`;
diff --git a/webapp/components/__snapshots__/ShoutButton.spec.js.snap b/webapp/components/__snapshots__/ShoutButton.spec.js.snap
new file mode 100644
index 000000000..94254b870
--- /dev/null
+++ b/webapp/components/__snapshots__/ShoutButton.spec.js.snap
@@ -0,0 +1,117 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShoutButton.vue renders button and text 1`] = `
+
+`;
+
+exports[`ShoutButton.vue toggle the button 1`] = `
+
+`;
+
+exports[`ShoutButton.vue toggle the button, but backend fails 1`] = `
+
+`;
+
+exports[`ShoutButton.vue when shouted renders 1`] = `
+
+`;
diff --git a/webapp/components/_new/generic/BaseButton/BaseButton.vue b/webapp/components/_new/generic/BaseButton/BaseButton.vue
index a51c3101c..7985e66c7 100644
--- a/webapp/components/_new/generic/BaseButton/BaseButton.vue
+++ b/webapp/components/_new/generic/BaseButton/BaseButton.vue
@@ -112,7 +112,8 @@ export default {
}
&.--circle {
- width: $size-button-base;
+ width: var(--circle-button-width, $size-button-base);
+ height: var(--circle-button-width, $size-button-base);
border-radius: 50%;
}
diff --git a/webapp/components/_new/generic/BaseIcon/BaseIcon.vue b/webapp/components/_new/generic/BaseIcon/BaseIcon.vue
index ef09e2e03..48d408c25 100644
--- a/webapp/components/_new/generic/BaseIcon/BaseIcon.vue
+++ b/webapp/components/_new/generic/BaseIcon/BaseIcon.vue
@@ -55,7 +55,7 @@ export default {
}
&.--regular {
- height: 1.2em;
+ height: var(--icon-size, 1.2em);
}
&.--large {
diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js
index e1704923f..58cdbd30d 100644
--- a/webapp/graphql/Fragments.js
+++ b/webapp/graphql/Fragments.js
@@ -158,5 +158,7 @@ export const commentFragment = gql`
contentExcerpt
isPostObservedByMe
postObservingUsersCount
+ shoutedByCurrentUser
+ shoutedCount
}
`
diff --git a/webapp/pages/groups/_id/_slug.vue b/webapp/pages/groups/_id/_slug.vue
index 30c5bd4d1..e4db080bc 100644
--- a/webapp/pages/groups/_id/_slug.vue
+++ b/webapp/pages/groups/_id/_slug.vue
@@ -131,7 +131,7 @@
-
+
diff --git a/webapp/pages/post/_id/_slug/index.spec.js b/webapp/pages/post/_id/_slug/index.spec.js
index a7e5d3478..f37108afd 100644
--- a/webapp/pages/post/_id/_slug/index.spec.js
+++ b/webapp/pages/post/_id/_slug/index.spec.js
@@ -50,7 +50,7 @@ describe('PostSlug', () => {
})
const propsData = {}
mocks = {
- $t: jest.fn(),
+ $t: jest.fn((t) => t),
$filters: {
truncate: (a) => a,
removeHtml: (a) => a,
diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue
index 89d3e0be0..b400ccc32 100644
--- a/webapp/pages/post/_id/_slug/index.vue
+++ b/webapp/pages/post/_id/_slug/index.vue
@@ -77,7 +77,7 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-@media only screen and (max-width: 960px) {
- .shout-button {
- float: left;
- }
+