Merge pull request #439 from Human-Connection/347-display_notifications

[WIP] Frontend implementation for notifications
This commit is contained in:
Robert Schäfer 2019-04-18 18:50:13 +02:00 committed by GitHub
commit 6cd8a4ef21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 976 additions and 162 deletions

View File

@ -3,21 +3,26 @@ import uuid from 'uuid/v4'
export default function (params) {
const {
id = uuid(),
key,
key = '',
type = 'crowdfunding',
status = 'permanent',
icon
icon = '/img/badges/indiegogo_en_panda.svg'
} = params
return `
mutation {
CreateBadge(
id: "${id}",
key: "${key}",
type: ${type},
status: ${status},
icon: "${icon}"
) { id }
return {
mutation: `
mutation(
$id: ID
$key: String!
$type: BadgeTypeEnum!
$status: BadgeStatusEnum!
$icon: String!
) {
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
id
}
}
`,
variables: { id, key, type, status, icon }
}
`
}

View File

@ -8,14 +8,15 @@ export default function (params) {
icon
} = params
return `
mutation {
CreateCategory(
id: "${id}",
name: "${name}",
slug: "${slug}",
icon: "${icon}"
) { id, name }
return {
mutation: `
mutation($id: ID, $name: String!, $slug: String, $icon: String!) {
CreateCategory(id: $id, name: $name, slug: $slug, icon: $icon) {
id
name
}
}
`
`,
variables: { id, name, slug, icon }
}
}

View File

@ -7,19 +7,17 @@ export default function (params) {
content = [
faker.lorem.sentence(),
faker.lorem.sentence()
].join('. '),
disabled = false,
deleted = false
].join('. ')
} = params
return `
mutation {
CreateComment(
id: "${id}",
content: "${content}",
disabled: ${disabled},
deleted: ${deleted}
) { id }
}
`
return {
mutation: `
mutation($id: ID!, $content: String!) {
CreateComment(id: $id, content: $content) {
id
}
}
`,
variables: { id, content }
}
}

View File

@ -71,8 +71,8 @@ export default function Factory (options = {}) {
return this
},
async create (node, properties) {
const mutation = this.factories[node](properties)
this.lastResponse = await this.graphQLClient.request(mutation)
const { mutation, variables } = this.factories[node](properties)
this.lastResponse = await this.graphQLClient.request(mutation, variables)
return this
},
async relate (node, relationship, properties) {

View File

@ -6,12 +6,15 @@ export default function (params) {
read = false
} = params
return `
mutation {
CreateNotification(
id: "${id}",
read: ${read},
) { id, read }
}
`
return {
mutation: `
mutation($id: ID, $read: Boolean) {
CreateNotification(id: $id, read: $read) {
id
read
}
}
`,
variables: { id, read }
}
}

View File

@ -5,20 +5,17 @@ export default function create (params) {
const {
id = uuid(),
name = faker.company.companyName(),
description = faker.company.catchPhrase(),
disabled = false,
deleted = false
description = faker.company.catchPhrase()
} = params
return `
mutation {
CreateOrganization(
id: "${id}",
name: "${name}",
description: "${description}",
disabled: ${disabled},
deleted: ${deleted}
) { name }
}
`
return {
mutation: `
mutation($id: ID!, $name: String!, $description: String!) {
CreateOrganization(id: $id, name: $name, description: $description) {
name
}
}
`,
variables: { id, name, description }
}
}

View File

@ -18,17 +18,31 @@ export default function (params) {
deleted = false
} = params
return `
mutation {
CreatePost(
id: "${id}",
slug: "${slug}",
title: "${title}",
content: "${content}",
image: "${image}",
visibility: ${visibility},
deleted: ${deleted}
) { title, content }
}
`
return {
mutation: `
mutation(
$id: ID!
$slug: String
$title: String!
$content: String!
$image: String
$visibility: VisibilityEnum
$deleted: Boolean
) {
CreatePost(
id: $id
slug: $slug
title: $title
content: $content
image: $image
visibility: $visibility
deleted: $deleted
) {
title
content
}
}
`,
variables: { id, slug, title, content, image, visibility, deleted }
}
}

View File

@ -6,15 +6,15 @@ export default function create (params) {
id
} = params
return `
mutation {
report(
description: "${description}",
id: "${id}",
) {
id,
createdAt
return {
mutation: `
mutation($id: ID!, $description: String!) {
report(description: $description, id: $id) {
id
createdAt
}
}
}
`
`,
variables: { id, description }
}
}

View File

@ -3,15 +3,17 @@ import uuid from 'uuid/v4'
export default function (params) {
const {
id = uuid(),
name
name = '#human-connection'
} = params
return `
mutation {
CreateTag(
id: "${id}",
name: "${name}",
) { name }
}
`
return {
mutation: `
mutation($id: ID!, $name: String!) {
CreateTag(id: $id, name: $name) {
name
}
}
`,
variables: { id, name }
}
}

View File

@ -10,34 +10,42 @@ export default function create (params) {
password = '1234',
role = 'user',
avatar = faker.internet.avatar(),
about = faker.lorem.paragraph(),
disabled = false,
deleted = false
about = faker.lorem.paragraph()
} = params
return `
mutation {
CreateUser(
id: "${id}",
name: "${name}",
slug: "${slug}",
password: "${password}",
email: "${email}",
avatar: "${avatar}",
about: "${about}",
role: ${role},
disabled: ${disabled},
deleted: ${deleted}
return {
mutation: `
mutation(
$id: ID!
$name: String
$slug: String
$password: String!
$email: String
$avatar: String
$about: String
$role: UserGroupEnum
) {
id
name
slug
email
avatar
role
deleted
disabled
CreateUser(
id: $id
name: $name
slug: $slug
password: $password
email: $email
avatar: $avatar
about: $about
role: $role
) {
id
name
slug
email
avatar
role
deleted
disabled
}
}
}
`
`,
variables: { id, name, slug, password, email, avatar, about, role }
}
}

View File

@ -1,3 +1,4 @@
import faker from 'faker'
import Factory from './factories'
/* eslint-disable no-multi-spaces */
@ -88,20 +89,23 @@ import Factory from './factories'
f.create('Tag', { id: 't4', name: 'Freiheit' })
])
const mention1 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
const mention2 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
await Promise.all([
asAdmin.create('Post', { id: 'p0' }),
asModerator.create('Post', { id: 'p1' }),
asUser.create('Post', { id: 'p2', deleted: true }),
asUser.create('Post', { id: 'p2' }),
asTick.create('Post', { id: 'p3' }),
asTrick.create('Post', { id: 'p4' }),
asTrack.create('Post', { id: 'p5' }),
asAdmin.create('Post', { id: 'p6' }),
asModerator.create('Post', { id: 'p7' }),
asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }),
asUser.create('Post', { id: 'p8' }),
asTick.create('Post', { id: 'p9' }),
asTrick.create('Post', { id: 'p10' }),
asTrack.create('Post', { id: 'p11' }),
asAdmin.create('Post', { id: 'p12' }),
asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }),
asModerator.create('Post', { id: 'p13' }),
asUser.create('Post', { id: 'p14' }),
asTick.create('Post', { id: 'p15' })

View File

@ -1,10 +1,23 @@
# End-to-End Testing
## Configure cypress
First, you have to tell cypress how to connect to your local neo4j database
among other things. You can copy our template configuration and change the new
file according to your needs.
Make sure you are at the root level of the project. Then:
```bash
# in the top level folder Human-Connection/
$ cp cypress.env.template.json cypress.env.json
```
## Run Tests
To run the tests, make sure you are at the root level of the project, in your console and run the following command:
To run the tests, do this:
```bash
# in the top level folder Human-Connection/
$ yarn cypress:setup
```

View File

@ -83,6 +83,13 @@ The following features will be implemented. This gets done in three steps:
* Editing Comments
* Upvote comments of others
### Notifications
[Cucumber features](./integration/notifications)
* User @-mentionings
* Notify authors for comments
* Administrative notifications to all users
### Contribution List
* Show Posts by Tiles

View File

@ -293,3 +293,43 @@ Then('I can login successfully with password {string}', password => {
})
cy.get('.iziToast-wrapper').should('contain', "You are logged in!")
})
When('I log in with the following credentials:', table => {
const { email, password } = table.hashes()[0]
cy.login({ email, password })
})
When('open the notification menu and click on the first item', () => {
cy.get('.notifications-menu').click()
cy.get('.notification-mention-post').first().click()
})
Then('see {int} unread notifications in the top menu', count => {
cy.get('.notifications-menu').should('contain', count)
})
Then('I get to the post page of {string}', path => {
path = path.replace('...', '')
cy.url().should('contain', '/post/')
cy.url().should('contain', path)
})
When('I start to write a new post with the title {string} beginning with:', (title, intro) => {
cy.get('.post-add-button').click()
cy.get('input[name="title"]').type(title)
cy.get('.ProseMirror').type(intro)
})
When('mention {string} in the text', (mention) => {
cy.get('.ProseMirror').type(' @')
cy.get('.suggestion-list__item').contains(mention).click()
cy.debug()
})
Then('the notification gets marked as read', () => {
cy.get('.notification').first().should('have.class', 'read')
})
Then('there are no notifications in the top menu', () => {
cy.get('.notifications-menu').should('contain', '0')
})

View File

@ -0,0 +1,31 @@
Feature: Notifications for a mentions
As a user
I want to be notified if sb. mentions me in a post or comment
In order join conversations about or related to me
Background:
Given we have the following user accounts:
| name | slug | email | password |
| Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 |
| Matt Rider | matt-rider | matt@example.org | 4321 |
Scenario: Mention another user, re-login as this user and see notifications
Given I log in with the following credentials:
| email | password |
| wolle@example.org | 1234 |
And I start to write a new post with the title "Hey Matt" beginning with:
"""
Big shout to our fellow contributor
"""
And mention "@matt-rider" in the text
And I click on "Save"
When I log out
And I log in with the following credentials:
| email | password |
| matt@example.org | 4321 |
And see 1 unread notifications in the top menu
And open the notification menu and click on the first item
Then I get to the post page of ".../hey-matt"
And the notification gets marked as read
But when I refresh the page
Then there are no notifications in the top menu

View File

@ -46,7 +46,8 @@ Cypress.Commands.add('login', ({ email, password }) => {
cy.get('button[name=submit]')
.as('submitButton')
.click()
cy.location('pathname').should('eq', '/') // we're in!
cy.get('.iziToast-message').should('contain', 'You are logged in!')
cy.get('.iziToast-close').click()
})
Cypress.Commands.add('logout', (email, password) => {

View File

@ -4,11 +4,12 @@
:image="post.image"
:class="{'post-card': true, 'disabled-content': post.disabled}"
>
<a
v-router-link
<nuxt-link
class="post-link"
:href="href(post)"
>{{ post.title }}</a>
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
>
{{ post.title }}
</nuxt-link>
<!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view -->
<ds-space margin-bottom="large">
@ -75,6 +76,7 @@
import HcUser from '~/components/User'
import ContentMenu from '~/components/ContentMenu'
import { randomBytes } from 'crypto'
import { mapGetters } from 'vuex'
export default {
name: 'HcPostCard',
@ -89,26 +91,16 @@ export default {
}
},
computed: {
...mapGetters({
user: 'auth/user'
}),
excerpt() {
// remove all links from excerpt to prevent issues with the serounding link
let excerpt = this.post.contentExcerpt.replace(/<a.*>(.+)<\/a>/gim, '$1')
// do not display content that is only linebreaks
if (excerpt.replace(/<br>/gim, '').trim() === '') {
excerpt = ''
}
return excerpt
return this.$filters.removeLinks(this.post.contentExcerpt)
},
isAuthor() {
return this.$store.getters['auth/user'].id === this.post.author.id
}
},
methods: {
href(post) {
return this.$router.resolve({
name: 'post-id-slug',
params: { id: post.id, slug: post.slug }
}).href
const { author } = this.post
if (!author) return false
return this.user.id === this.post.author.id
}
}
}
@ -130,6 +122,7 @@ export default {
}
.post-link {
margin: 15px;
display: block;
position: absolute;
top: 0;

View File

@ -0,0 +1,62 @@
import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
import PostCard from '.'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import Filters from '~/plugins/vue-filters'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
localVue.use(Filters)
config.stubs['no-ssr'] = '<span><slot /></span>'
config.stubs['v-popover'] = '<span><slot /></span>'
describe('PostCard', () => {
let wrapper
let stubs
let mocks
let propsData
let getters
beforeEach(() => {
propsData = {}
stubs = {
NuxtLink: RouterLinkStub
}
mocks = {
$t: jest.fn()
}
getters = {
'auth/user': () => {
return {}
}
}
})
const Wrapper = () => {
const store = new Vuex.Store({
getters
})
return mount(PostCard, {
stubs,
mocks,
propsData,
store,
localVue
})
}
describe('given a post', () => {
beforeEach(() => {
propsData.post = {
title: "It's a title"
}
})
it('renders title', () => {
expect(Wrapper().text()).toContain("It's a title")
})
})
})

View File

@ -0,0 +1,71 @@
<template>
<ds-space
:class="{'notification': true, 'read': notification.read}"
margin-bottom="x-small"
>
<no-ssr>
<ds-space margin-bottom="x-small">
<hc-user
:user="post.author"
:date-time="post.createdAt"
:trunc="35"
/>
</ds-space>
<ds-text color="soft">
{{ $t("notifications.menu.mentioned") }}
</ds-text>
</no-ssr>
<ds-space margin-bottom="x-small" />
<nuxt-link
class="notification-mention-post"
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
@click.native="$emit('read')"
>
<ds-space margin-bottom="x-small">
<ds-card
:header="post.title"
:image="post.image"
hover
space="x-small"
>
<ds-space margin-bottom="x-small" />
<!-- eslint-disable vue/no-v-html -->
<div v-html="excerpt" />
<!-- eslint-enable vue/no-v-html -->
</ds-card>
</ds-space>
</nuxt-link>
</ds-space>
</template>
<script>
import HcUser from '~/components/User'
export default {
name: 'Notification',
components: {
HcUser
},
props: {
notification: {
type: Object,
required: true
}
},
computed: {
excerpt() {
return this.$filters.removeLinks(this.post.contentExcerpt)
},
post() {
return this.notification.post || {}
}
}
}
</script>
<style>
.notification.read {
opacity: 0.6; /* Real browsers */
filter: alpha(opacity = 60); /* MSIE */
}
</style>

View File

@ -0,0 +1,64 @@
import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
import Notification from '.'
import Styleguide from '@human-connection/styleguide'
import Filters from '~/plugins/vue-filters'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(Filters)
config.stubs['no-ssr'] = '<span><slot /></span>'
describe('Notification', () => {
let wrapper
let stubs
let mocks
let propsData
beforeEach(() => {
propsData = {}
mocks = {
$t: jest.fn()
}
stubs = {
NuxtLink: RouterLinkStub
}
})
const Wrapper = () => {
return mount(Notification, {
stubs,
mocks,
propsData,
localVue
})
}
describe('given a notification', () => {
beforeEach(() => {
propsData.notification = {
post: {
title: "It's a title"
}
}
})
it('renders title', () => {
expect(Wrapper().text()).toContain("It's a title")
})
it('has no class "read"', () => {
expect(Wrapper().classes()).not.toContain('read')
})
describe('that is read', () => {
beforeEach(() => {
propsData.notification.read = true
})
it('has class "read"', () => {
expect(Wrapper().classes()).toContain('read')
})
})
})
})

View File

@ -0,0 +1,32 @@
<template>
<div>
<notification
v-for="notification in notifications"
:key="notification.id"
:notification="notification"
@read="markAsRead(notification.id)"
/>
</div>
</template>
<script>
import Notification from '../Notification'
export default {
name: 'NotificationList',
components: {
Notification
},
props: {
notifications: {
type: Array,
required: true
}
},
methods: {
markAsRead(notificationId) {
this.$emit('markAsRead', notificationId)
}
}
}
</script>

View File

@ -0,0 +1,130 @@
import {
config,
shallowMount,
mount,
createLocalVue,
RouterLinkStub
} from '@vue/test-utils'
import NotificationList from '.'
import Notification from '../Notification'
import Vue from 'vue'
import Vuex from 'vuex'
import Filters from '~/plugins/vue-filters'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
localVue.use(Filters)
localVue.filter('truncate', string => string)
config.stubs['no-ssr'] = '<span><slot /></span>'
config.stubs['v-popover'] = '<span><slot /></span>'
describe('NotificationList.vue', () => {
let wrapper
let Wrapper
let mocks
let stubs
let store
let propsData
beforeEach(() => {
store = new Vuex.Store({
getters: {
'auth/user': () => {
return {}
}
}
})
mocks = {
$t: jest.fn()
}
stubs = {
NuxtLink: RouterLinkStub
}
propsData = {
notifications: [
{
id: 'notification-41',
read: false,
post: {
id: 'post-1',
title: 'some post title',
contentExcerpt: 'this is a post content',
author: {
id: 'john-1',
slug: 'john-doe',
name: 'John Doe'
}
}
},
{
id: 'notification-42',
read: false,
post: {
id: 'post-2',
title: 'another post title',
contentExcerpt: 'this is yet another post content',
author: {
id: 'john-1',
slug: 'john-doe',
name: 'John Doe'
}
}
}
]
}
})
describe('shallowMount', () => {
const Wrapper = () => {
return shallowMount(NotificationList, {
propsData,
mocks,
store,
localVue
})
}
beforeEach(() => {
wrapper = Wrapper()
})
it('renders Notification.vue for each notification of the user', () => {
expect(wrapper.findAll(Notification)).toHaveLength(2)
})
})
describe('mount', () => {
const Wrapper = () => {
return mount(NotificationList, {
propsData,
mocks,
stubs,
store,
localVue
})
}
beforeEach(() => {
wrapper = Wrapper()
})
describe('click on a notification', () => {
beforeEach(() => {
wrapper
.findAll('.notification-mention-post')
.at(1)
.trigger('click')
})
it("emits 'markAsRead' with the notificationId", () => {
expect(wrapper.emitted('markAsRead')).toBeTruthy()
expect(wrapper.emitted('markAsRead')[0]).toEqual(['notification-42'])
})
})
})
})

View File

@ -0,0 +1,112 @@
<template>
<ds-button
v-if="totalNotifications <= 0"
class="notifications-menu"
disabled
icon="bell"
>
{{ totalNotifications }}
</ds-button>
<dropdown
v-else
class="notifications-menu"
>
<template
slot="default"
slot-scope="{toggleMenu}"
>
<ds-button
primary
icon="bell"
@click.prevent="toggleMenu"
>
{{ totalNotifications }}
</ds-button>
</template>
<template
slot="popover"
>
<div class="notifications-menu-popover">
<notification-list
:notifications="notifications"
@markAsRead="markAsRead"
/>
</div>
</template>
</dropdown>
</template>
<script>
import NotificationList from '../NotificationList'
import Dropdown from '~/components/Dropdown'
import gql from 'graphql-tag'
const MARK_AS_READ = gql(`
mutation($id: ID!, $read: Boolean!) {
UpdateNotification(id: $id, read: $read) {
id
read
}
}`)
const NOTIFICATIONS = gql(`{
currentUser {
id
notifications(read: false, orderBy: createdAt_desc) {
id read createdAt
post {
id createdAt disabled deleted title contentExcerpt slug
author { id slug name disabled deleted }
}
}
}
}`)
export default {
name: 'NotificationMenu',
components: {
NotificationList,
Dropdown
},
computed: {
totalNotifications() {
return (this.notifications || []).length
}
},
methods: {
async markAsRead(notificationId) {
const variables = { id: notificationId, read: true }
try {
await this.$apollo.mutate({
mutation: MARK_AS_READ,
variables
})
} catch (err) {
throw new Error(err)
}
}
},
apollo: {
notifications: {
query: NOTIFICATIONS,
update: data => {
const {
currentUser: { notifications }
} = data
return notifications
}
}
}
}
</script>
<style>
.notifications-menu {
display: flex;
align-items: center;
}
.notifications-menu-popover {
max-width: 500px;
}
</style>

View File

@ -0,0 +1,94 @@
import { config, shallowMount, createLocalVue } from '@vue/test-utils'
import NotificationMenu from '.'
import Styleguide from '@human-connection/styleguide'
import Filters from '~/plugins/vue-filters'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(Filters)
localVue.filter('truncate', string => string)
config.stubs['dropdown'] = '<span class="dropdown"><slot /></span>'
describe('NotificationMenu.vue', () => {
let wrapper
let Wrapper
let mocks
let data
beforeEach(() => {
mocks = {
$t: jest.fn()
}
data = () => {
return {
notifications: []
}
}
})
describe('shallowMount', () => {
const Wrapper = () => {
return shallowMount(NotificationMenu, {
data,
mocks,
localVue
})
}
it('counter displays 0', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('0')
})
it('no dropdown is rendered', () => {
wrapper = Wrapper()
expect(wrapper.contains('.dropdown')).toBe(false)
})
describe('given some notifications', () => {
beforeEach(() => {
data = () => {
return {
notifications: [
{
id: 'notification-41',
read: false,
post: {
id: 'post-1',
title: 'some post title',
contentExcerpt: 'this is a post content',
author: {
id: 'john-1',
slug: 'john-doe',
name: 'John Doe'
}
}
},
{
id: 'notification-42',
read: false,
post: {
id: 'post-2',
title: 'another post title',
contentExcerpt: 'this is yet another post content',
author: {
id: 'john-1',
slug: 'john-doe',
name: 'John Doe'
}
}
}
]
}
}
})
it('displays the total number of notifications', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('2')
})
})
})
})

View File

@ -10,7 +10,7 @@
</template>
<script>
import seo from '~/components/mixins/seo'
import seo from '~/mixins/seo'
export default {
mixins: [seo]

View File

@ -31,6 +31,9 @@
/>
</no-ssr>
<template v-if="isLoggedIn">
<no-ssr>
<notification-menu />
</no-ssr>
<no-ssr>
<dropdown class="avatar-menu">
<template
@ -113,10 +116,11 @@
<script>
import { mapGetters, mapActions } from 'vuex'
import LocaleSwitch from '~/components/LocaleSwitch'
import Dropdown from '~/components/Dropdown'
import SearchInput from '~/components/SearchInput.vue'
import Modal from '~/components/Modal'
import seo from '~/components/mixins/seo'
import NotificationMenu from '~/components/notifications/NotificationMenu'
import Dropdown from '~/components/Dropdown'
import seo from '~/mixins/seo'
export default {
components: {
@ -124,7 +128,8 @@ export default {
LocaleSwitch,
SearchInput,
Modal,
LocaleSwitch
LocaleSwitch,
NotificationMenu
},
mixins: [seo],
data() {

View File

@ -18,6 +18,11 @@
"commented": "Kommentiert",
"socialMedia": "Wo sonst finde ich"
},
"notifications": {
"menu": {
"mentioned": "hat dich in einem Beitrag erwähnt"
}
},
"search": {
"placeholder": "Suchen",
"hint": "Wonach suchst du?",

View File

@ -18,6 +18,11 @@
"commented": "Commented",
"socialMedia": "Where else can I find"
},
"notifications": {
"menu": {
"mentioned": "has mentioned you in a post"
}
},
"search": {
"placeholder": "Search",
"hint": "What are you searching for?",

View File

@ -34,7 +34,7 @@
<script>
import gql from 'graphql-tag'
import uniqBy from 'lodash/uniqBy'
import HcPostCard from '~/components/PostCard.vue'
import HcPostCard from '~/components/PostCard'
import HcLoadMore from '~/components/LoadMore.vue'
export default {

View File

@ -56,7 +56,7 @@
<script>
import gql from 'graphql-tag'
import HcPostCard from '~/components/PostCard.vue'
import HcPostCard from '~/components/PostCard'
import HcEmpty from '~/components/Empty.vue'
export default {

View File

@ -323,7 +323,7 @@
import uniqBy from 'lodash/uniqBy'
import User from '~/components/User'
import HcPostCard from '~/components/PostCard.vue'
import HcPostCard from '~/components/PostCard'
import HcFollowButton from '~/components/FollowButton.vue'
import HcCountTo from '~/components/CountTo.vue'
import HcBadges from '~/components/Badges.vue'

View File

@ -6,7 +6,7 @@ import formatRelative from 'date-fns/formatRelative'
import addSeconds from 'date-fns/addSeconds'
import accounting from 'accounting'
export default ({ app }) => {
export default ({ app = {} }) => {
const locales = {
en: enUS,
de: de,
@ -88,6 +88,17 @@ export default ({ app }) => {
return index === 0 ? letter.toUpperCase() : letter.toLowerCase()
})
.replace(/\s+/g, '')
},
removeLinks: content => {
if (!content) return ''
// remove all links from excerpt to prevent issues with the surrounding link
let excerpt = content.replace(/<a.*>(.+)<\/a>/gim, '$1')
// do not display content that is only linebreaks
if (excerpt.replace(/<br>/gim, '').trim() === '') {
excerpt = ''
}
return excerpt
}
})

View File

@ -74,21 +74,38 @@ export const actions = {
data: { currentUser }
} = await client.query({
query: gql(`{
currentUser {
currentUser {
id
name
slug
email
avatar
role
about
locationName
socialMedia {
id
name
slug
email
avatar
role
about
locationName
socialMedia {
id
url
url
}
notifications(read: false, orderBy: createdAt_desc) {
id
read
createdAt
post {
author {
id
slug
name
disabled
deleted
}
title
contentExcerpt
slug
}
}
}`)
}
}`)
})
if (!currentUser) return dispatch('logout')
commit('SET_USER', currentUser)

View File

@ -0,0 +1,89 @@
import gql from 'graphql-tag'
export const state = () => {
return {
notifications: null,
pending: false
}
}
export const mutations = {
SET_NOTIFICATIONS(state, notifications) {
state.notifications = notifications
},
SET_PENDING(state, pending) {
state.pending = pending
},
UPDATE_NOTIFICATIONS(state, notification) {
const notifications = state.notifications
const toBeUpdated = notifications.find(n => {
return n.id === notification.id
})
toBeUpdated = { ...toBeUpdated, ...notification }
}
}
export const getters = {
notifications(state) {
return !!state.notifications
}
}
export const actions = {
async init({ getters, commit }) {
if (getters.notifications) return
commit('SET_PENDING', true)
const client = this.app.apolloProvider.defaultClient
let notifications
try {
const {
data: { currentUser }
} = await client.query({
query: gql(`{
currentUser {
id
notifications(orderBy: createdAt_desc) {
id
read
createdAt
post {
author {
id
slug
name
disabled
deleted
}
title
contentExcerpt
slug
}
}
}
}`)
})
notifications = currentUser.notifications
console.log(notifications)
commit('SET_NOTIFICATIONS', notifications)
} finally {
commit('SET_PENDING', false)
}
return notifications
},
async markAsRead({ commit, rootGetters }, notificationId) {
const client = this.app.apolloProvider.defaultClient
const mutation = gql(`
mutation($id: ID!, $read: Boolean!) {
UpdateNotification(id: $id, read: $read) {
id
read
}
}
`)
const variables = { id: notificationId, read: true }
const {
data: { UpdateNotification }
} = await client.mutate({ mutation, variables })
commit('UPDATE_NOTIFICATIONS', UpdateNotification)
}
}