Merge branch 'master' of github.com:Human-Connection/Human-Connection into 1707-reporting-with-specific-information

This commit is contained in:
Wolfgang Huß 2019-10-02 08:59:10 +02:00
commit 90fdd24697
30 changed files with 663 additions and 807 deletions

View File

@ -9,4 +9,4 @@
],
"editor.formatOnSave": false,
"eslint.autoFixOnSave": true
}
}

View File

@ -43,19 +43,18 @@
"dependencies": {
"@hapi/joi": "^16.1.4",
"@sentry/node": "^5.6.2",
"activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4",
"apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16",
"apollo-server": "~2.9.3",
"apollo-server": "~2.9.4",
"apollo-server-express": "^2.9.4",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~6.0.0",
"date-fns": "2.3.0",
"date-fns": "2.4.1",
"debug": "~4.1.1",
"dotenv": "~8.1.0",
"express": "^4.17.1",
@ -63,8 +62,8 @@
"graphql": "^14.5.8",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.5",
"graphql-middleware-sentry": "^3.2.0",
"graphql-middleware": "~4.0.1",
"graphql-middleware-sentry": "^3.2.1",
"graphql-shield": "~6.1.0",
"graphql-tag": "~2.10.1",
"helmet": "~3.21.1",
@ -73,21 +72,21 @@
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.0",
"metascraper": "^4.10.3",
"metascraper-audio": "^5.7.5",
"metascraper-author": "^5.7.4",
"metascraper-audio": "^5.7.6",
"metascraper-author": "^5.7.6",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.7.4",
"metascraper-date": "^5.7.6",
"metascraper-description": "^5.7.5",
"metascraper-image": "^5.7.5",
"metascraper-lang": "^5.7.4",
"metascraper-image": "^5.7.6",
"metascraper-lang": "^5.7.6",
"metascraper-lang-detector": "^4.8.5",
"metascraper-logo": "^5.7.5",
"metascraper-publisher": "^5.7.4",
"metascraper-soundcloud": "^5.7.4",
"metascraper-title": "^5.7.5",
"metascraper-url": "^5.7.5",
"metascraper-video": "^5.7.5",
"metascraper-youtube": "^5.7.5",
"metascraper-logo": "^5.7.6",
"metascraper-publisher": "^5.7.6",
"metascraper-soundcloud": "^5.7.6",
"metascraper-title": "^5.7.6",
"metascraper-url": "^5.7.6",
"metascraper-video": "^5.7.6",
"metascraper-youtube": "^5.7.6",
"minimatch": "^3.0.4",
"mustache": "^3.1.0",
"neo4j-driver": "~1.7.6",
@ -108,9 +107,9 @@
"devDependencies": {
"@babel/cli": "~7.6.2",
"@babel/core": "~7.6.2",
"@babel/node": "~7.6.1",
"@babel/node": "~7.6.2",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.6.0",
"@babel/preset-env": "~7.6.2",
"@babel/register": "~7.6.2",
"apollo-server-testing": "~2.9.4",
"babel-core": "~7.0.0-0",
@ -118,7 +117,7 @@
"babel-jest": "~24.9.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~6.4.0",
"eslint": "~6.5.1",
"eslint-config-prettier": "~6.3.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.18.2",
@ -129,7 +128,7 @@
"eslint-plugin-standard": "~4.0.1",
"graphql-request": "~1.8.2",
"jest": "~24.9.0",
"nodemon": "~1.19.2",
"nodemon": "~1.19.3",
"prettier": "~1.18.2",
"supertest": "~4.0.2"
}

View File

@ -1,7 +1,9 @@
import { extractNameFromId, extractDomainFromUrl, signAndSend } from './utils'
import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
// import { extractDomainFromUrl, signAndSend } from './utils'
import { extractNameFromId, signAndSend } from './utils'
import { isPublicAddressed } from './utils/activity'
// import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
import request from 'request'
import as from 'activitystrea.ms'
// import as from 'activitystrea.ms'
import NitroDataSource from './NitroDataSource'
import router from './routes'
import Collections from './Collections'
@ -33,71 +35,71 @@ export default class ActivityPub {
}
}
handleFollowActivity(activity) {
debug(`inside FOLLOW ${activity.actor}`)
const toActorName = extractNameFromId(activity.object)
const fromDomain = extractDomainFromUrl(activity.actor)
const dataSource = this.dataSource
// handleFollowActivity(activity) {
// debug(`inside FOLLOW ${activity.actor}`)
// const toActorName = extractNameFromId(activity.object)
// const fromDomain = extractDomainFromUrl(activity.actor)
// const dataSource = this.dataSource
return new Promise((resolve, reject) => {
request(
{
url: activity.actor,
headers: {
Accept: 'application/activity+json',
},
},
async (err, response, toActorObject) => {
if (err) return reject(err)
// save shared inbox
toActorObject = JSON.parse(toActorObject)
await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
// return new Promise((resolve, reject) => {
// request(
// {
// url: activity.actor,
// headers: {
// Accept: 'application/activity+json',
// },
// },
// async (err, response, toActorObject) => {
// if (err) return reject(err)
// // save shared inbox
// toActorObject = JSON.parse(toActorObject)
// await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
activity.object,
)
// const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
// activity.object,
// )
const followActivity = as
.follow()
.id(activity.id)
.actor(activity.actor)
.object(activity.object)
// const followActivity = as
// .follow()
// .id(activity.id)
// .actor(activity.actor)
// .object(activity.object)
// add follower if not already in collection
if (followersCollectionPage.orderedItems.includes(activity.actor)) {
debug('follower already in collection!')
debug(`inbox = ${toActorObject.inbox}`)
resolve(
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
} else {
followersCollectionPage.orderedItems.push(activity.actor)
}
debug(`toActorObject = ${toActorObject}`)
toActorObject =
typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
debug(`inbox = ${toActorObject.inbox}`)
debug(`outbox = ${toActorObject.outbox}`)
debug(`followers = ${toActorObject.followers}`)
debug(`following = ${toActorObject.following}`)
// // add follower if not already in collection
// if (followersCollectionPage.orderedItems.includes(activity.actor)) {
// debug('follower already in collection!')
// debug(`inbox = ${toActorObject.inbox}`)
// resolve(
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
// )
// } else {
// followersCollectionPage.orderedItems.push(activity.actor)
// }
// debug(`toActorObject = ${toActorObject}`)
// toActorObject =
// typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
// debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
// debug(`inbox = ${toActorObject.inbox}`)
// debug(`outbox = ${toActorObject.outbox}`)
// debug(`followers = ${toActorObject.followers}`)
// debug(`following = ${toActorObject.following}`)
try {
await dataSource.saveFollowersCollectionPage(followersCollectionPage)
debug('follow activity saved')
resolve(
sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
} catch (e) {
debug('followers update error!', e)
resolve(
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
}
},
)
})
}
// try {
// await dataSource.saveFollowersCollectionPage(followersCollectionPage)
// debug('follow activity saved')
// resolve(
// sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
// )
// } catch (e) {
// debug('followers update error!', e)
// resolve(
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
// )
// }
// },
// )
// })
// }
handleUndoActivity(activity) {
debug('inside UNDO')

View File

@ -18,9 +18,9 @@ router.post('/', async function(req, res, next) {
case 'Undo':
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
await activityPub.handleFollowActivity(req.body).catch(next)
break
// case 'Follow':
// await activityPub.handleFollowActivity(req.body).catch(next)
// break
case 'Delete':
await activityPub.handleDeleteActivity(req.body).catch(next)
break

View File

@ -56,9 +56,9 @@ router.post('/:name/inbox', verify, async function(req, res, next) {
case 'Undo':
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
await activityPub.handleFollowActivity(req.body).catch(next)
break
// case 'Follow':
// await activityPub.handleFollowActivity(req.body).catch(next)
// break
case 'Delete':
await activityPub.handleDeleteActivity(req.body).catch(next)
break

View File

@ -1,10 +1,11 @@
import { activityPub } from '../ActivityPub'
import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
import { throwErrorIfApolloErrorOccurred } from './index'
// import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
import crypto from 'crypto'
import as from 'activitystrea.ms'
// import as from 'activitystrea.ms'
import gql from 'graphql-tag'
const debug = require('debug')('ea:utils:activity')
// const debug = require('debug')('ea:utils:activity')
export function createNoteObject(text, name, id, published) {
const createUuid = crypto.randomBytes(16).toString('hex')
@ -62,41 +63,41 @@ export async function getActorId(name) {
}
}
export function sendAcceptActivity(theBody, name, targetDomain, url) {
as.accept()
.id(
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
crypto.randomBytes(16).toString('hex'),
)
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
if (!err) {
return signAndSend(doc, name, targetDomain, url)
} else {
debug(`error serializing Accept object: ${err}`)
throw new Error('error serializing Accept object')
}
})
}
// export function sendAcceptActivity(theBody, name, targetDomain, url) {
// as.accept()
// .id(
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
// crypto.randomBytes(16).toString('hex'),
// )
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
// .object(theBody)
// .prettyWrite((err, doc) => {
// if (!err) {
// return signAndSend(doc, name, targetDomain, url)
// } else {
// debug(`error serializing Accept object: ${err}`)
// throw new Error('error serializing Accept object')
// }
// })
// }
export function sendRejectActivity(theBody, name, targetDomain, url) {
as.reject()
.id(
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
crypto.randomBytes(16).toString('hex'),
)
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
if (!err) {
return signAndSend(doc, name, targetDomain, url)
} else {
debug(`error serializing Accept object: ${err}`)
throw new Error('error serializing Accept object')
}
})
}
// export function sendRejectActivity(theBody, name, targetDomain, url) {
// as.reject()
// .id(
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
// crypto.randomBytes(16).toString('hex'),
// )
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
// .object(theBody)
// .prettyWrite((err, doc) => {
// if (!err) {
// return signAndSend(doc, name, targetDomain, url)
// } else {
// debug(`error serializing Accept object: ${err}`)
// throw new Error('error serializing Accept object')
// }
// })
// }
export function isPublicAddressed(postObject) {
if (typeof postObject.to === 'string') {

View File

@ -1,51 +1,51 @@
import { generateRsaKeyPair } from '../activitypub/security'
import { activityPub } from '../activitypub/ActivityPub'
import as from 'activitystrea.ms'
// import as from 'activitystrea.ms'
const debug = require('debug')('backend:schema')
// const debug = require('debug')('backend:schema')
export default {
Mutation: {
CreatePost: async (resolve, root, args, context, info) => {
args.activityId = activityPub.generateStatusId(context.user.slug)
args.objectId = activityPub.generateStatusId(context.user.slug)
// CreatePost: async (resolve, root, args, context, info) => {
// args.activityId = activityPub.generateStatusId(context.user.slug)
// args.objectId = activityPub.generateStatusId(context.user.slug)
const post = await resolve(root, args, context, info)
// const post = await resolve(root, args, context, info)
const { user: author } = context
const actorId = author.actorId
debug(`actorId = ${actorId}`)
const createActivity = await new Promise((resolve, reject) => {
as.create()
.id(`${actorId}/status/${args.activityId}`)
.actor(`${actorId}`)
.object(
as
.article()
.id(`${actorId}/status/${post.id}`)
.content(post.content)
.to('https://www.w3.org/ns/activitystreams#Public')
.publishedNow()
.attributedTo(`${actorId}`),
)
.prettyWrite((err, doc) => {
if (err) {
reject(err)
} else {
debug(doc)
const parsedDoc = JSON.parse(doc)
parsedDoc.send = true
resolve(JSON.stringify(parsedDoc))
}
})
})
try {
await activityPub.sendActivity(createActivity)
} catch (e) {
debug(`error sending post activity\n${e}`)
}
return post
},
// const { user: author } = context
// const actorId = author.actorId
// debug(`actorId = ${actorId}`)
// const createActivity = await new Promise((resolve, reject) => {
// as.create()
// .id(`${actorId}/status/${args.activityId}`)
// .actor(`${actorId}`)
// .object(
// as
// .article()
// .id(`${actorId}/status/${post.id}`)
// .content(post.content)
// .to('https://www.w3.org/ns/activitystreams#Public')
// .publishedNow()
// .attributedTo(`${actorId}`),
// )
// .prettyWrite((err, doc) => {
// if (err) {
// reject(err)
// } else {
// debug(doc)
// const parsedDoc = JSON.parse(doc)
// parsedDoc.send = true
// resolve(JSON.stringify(parsedDoc))
// }
// })
// })
// try {
// await activityPub.sendActivity(createActivity)
// } catch (e) {
// debug(`error sending post activity\n${e}`)
// }
// return post
// },
SignupVerification: async (resolve, root, args, context, info) => {
const keys = generateRsaKeyPair()
Object.assign(args, keys)

View File

@ -21,7 +21,7 @@ if (!hasEmailConfig) {
const transporter = nodemailer.createTransport({
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS === 'true',
secure: false, // true for 465, false for other ports
auth: hasAuthData && {
user: CONFIG.SMTP_USERNAME,

File diff suppressed because it is too large Load Diff

View File

@ -351,10 +351,12 @@ When("I log in with the following credentials:", table => {
});
When("open the notification menu and click on the first item", () => {
cy.get(".notifications-menu").click();
cy.get(".notifications-menu").invoke('show').click(); // "invoke('show')" because of the delay for show the menu
cy.get(".notification-mention-post")
.first()
.click();
.click({
force: true
});
});
Then("see {int} unread notifications in the top menu", count => {

View File

@ -1,4 +1,4 @@
Feature: Notifications for a mentions
Feature: Notification for a mention
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

View File

@ -0,0 +1,10 @@
export default function(to, from, savedPosition) {
if (savedPosition) return savedPosition
// Edge case: If you click on a notification from a comment and then on the
// post page you click on 'comments', we avoid a "jumping" scroll behavior,
// ie. jump to the top and scroll back from there
if (to.path === from.path && to.hash !== from.hash) return false
return { x: 0, y: 0 }
}

View File

@ -32,6 +32,7 @@ describe('Comment.vue', () => {
truncate: a => a,
removeHtml: a => a,
},
$scrollTo: jest.fn(),
$apollo: {
mutate: jest.fn().mockResolvedValue({
data: {
@ -51,6 +52,8 @@ describe('Comment.vue', () => {
})
describe('shallowMount', () => {
beforeEach(jest.useFakeTimers)
Wrapper = () => {
const store = new Vuex.Store({
getters,
@ -117,7 +120,35 @@ describe('Comment.vue', () => {
})
})
beforeEach(jest.useFakeTimers)
describe('scrollToAnchor mixin', () => {
describe('$route.hash !== comment.id', () => {
beforeEach(() => {
mocks.$route = {
hash: '',
}
})
it('skips $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).not.toHaveBeenCalled()
})
})
describe('$route.hash === comment.id', () => {
beforeEach(() => {
mocks.$route = {
hash: '#commentId-2',
}
})
it('calls $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).toHaveBeenCalledWith('#commentId-2')
})
})
})
describe('test callbacks', () => {
beforeEach(() => {

View File

@ -10,7 +10,7 @@
</ds-card>
</div>
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
<ds-card :id="`commentId-${comment.id}`">
<ds-card :id="anchor">
<ds-space margin-bottom="small" margin-top="small">
<hc-user :user="author" :date-time="comment.createdAt" />
<!-- Content Menu (can open Modals) -->
@ -80,8 +80,10 @@ import ContentMenu from '~/components/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import HcCommentForm from '~/components/CommentForm/CommentForm'
import CommentMutations from '~/graphql/CommentMutations'
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
export default {
mixins: [scrollToAnchor],
data: function() {
return {
isCollapsed: true,
@ -109,6 +111,9 @@ export default {
user: 'auth/user',
isModerator: 'auth/isModerator',
}),
anchor() {
return `commentId-${this.comment.id}`
},
displaysComment() {
return !this.unavailable || this.isModerator
},
@ -142,6 +147,9 @@ export default {
},
},
methods: {
checkAnchor(anchor) {
return `#${this.anchor}` === anchor
},
isAuthor(id) {
return this.user.id === id
},

View File

@ -42,6 +42,7 @@ describe('CommentList.vue', () => {
truncate: a => a,
removeHtml: a => a,
},
$scrollTo: jest.fn(),
$apollo: {
queries: {
Post: {
@ -65,12 +66,46 @@ describe('CommentList.vue', () => {
})
}
beforeEach(() => {
it('displays a comments counter', () => {
wrapper = Wrapper()
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
})
it('displays a comments counter', () => {
wrapper = Wrapper()
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
})
describe('scrollToAnchor mixin', () => {
beforeEach(jest.useFakeTimers)
describe('$route.hash !== `#comments`', () => {
beforeEach(() => {
mocks.$route = {
hash: '',
}
})
it('skips $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).not.toHaveBeenCalled()
})
})
describe('$route.hash === `#comments`', () => {
beforeEach(() => {
mocks.$route = {
hash: '#comments',
}
})
it('calls $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).toHaveBeenCalledWith('#comments')
})
})
})
})
})

View File

@ -30,8 +30,10 @@
</template>
<script>
import Comment from '~/components/Comment/Comment'
import scrollToAnchor from '~/mixins/scrollToAnchor'
export default {
mixins: [scrollToAnchor],
components: {
Comment,
},
@ -39,6 +41,9 @@ export default {
post: { type: Object, default: () => {} },
},
methods: {
checkAnchor(anchor) {
return anchor === '#comments'
},
updateCommentList(updatedComment) {
this.post.comments = this.post.comments.map(comment => {
return comment.id === updatedComment.id ? updatedComment : comment

View File

@ -75,7 +75,7 @@ export default {
const followedUser = follow ? data.followUser : data.unfollowUser
this.$emit('update', followedUser)
} catch {
} catch (err) {
optimisticResult.followedByCurrentUser = !follow
this.$emit('optimistic', optimisticResult)
}

View File

@ -69,16 +69,16 @@ describe('Notification', () => {
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.commented_on_post',
'notifications.reason.commented_on_post',
)
})
it('renders title', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain("It's a post title")
})
it('renders the "Comment:"', () => {
it('renders the identifier "notifications.comment"', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('Comment:')
expect(wrapper.text()).toContain('notifications.comment')
})
it('renders the contentExcerpt', () => {
wrapper = Wrapper()
@ -119,7 +119,7 @@ describe('Notification', () => {
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.mentioned_in_post',
'notifications.reason.mentioned_in_post',
)
})
it('renders title', () => {
@ -169,7 +169,7 @@ describe('Notification', () => {
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.mentioned_in_comment',
'notifications.reason.mentioned_in_comment',
)
})
it('renders title', () => {
@ -177,9 +177,9 @@ describe('Notification', () => {
expect(wrapper.text()).toContain("It's a post title")
})
it('renders the "Comment:"', () => {
it('renders the identifier "notifications.comment"', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('Comment:')
expect(wrapper.text()).toContain('notifications.comment')
})
it('renders the contentExcerpt', () => {

View File

@ -5,7 +5,7 @@
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
</ds-space>
<ds-text class="reason-text-for-test" color="soft">
{{ $t(`notifications.menu.${notification.reason}`) }}
{{ $t(`notifications.reason.${notification.reason}`) }}
</ds-text>
</client-only>
<ds-space margin-bottom="x-small" />
@ -23,7 +23,9 @@
>
<ds-space margin-bottom="x-small" />
<div>
<span v-if="isComment" class="comment-notification-header">Comment:</span>
<span v-if="isComment" class="comment-notification-header">
{{ $t(`notifications.comment`) }}:
</span>
{{ from.contentExcerpt | removeHtml }}
</div>
</ds-card>

View File

@ -46,11 +46,46 @@ describe('NotificationMenu.vue', () => {
expect(wrapper.contains('.dropdown')).toBe(false)
})
describe('given only unread notifications', () => {
beforeEach(() => {
data = () => {
return {
displayedNotifications: [
{
id: 'notification-41',
read: true,
post: {
id: 'post-1',
title: 'some post title',
contentExcerpt: 'this is a post content',
author: {
id: 'john-1',
slug: 'john-doe',
name: 'John Doe',
},
},
},
],
}
}
})
it('counter displays 0', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('0')
})
it('button is not primary', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').props('primary')).toBe(false)
})
})
describe('given some notifications', () => {
beforeEach(() => {
data = () => {
return {
notifications: [
displayedNotifications: [
{
id: 'notification-41',
read: false,
@ -79,15 +114,34 @@ describe('NotificationMenu.vue', () => {
},
},
},
{
id: 'notification-43',
read: true,
post: {
id: 'post-3',
title: 'read 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', () => {
it('displays the number of unread notifications', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('2')
})
it('renders primary button', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').props('primary')).toBe(true)
})
})
})
})

View File

@ -1,16 +1,16 @@
<template>
<ds-button v-if="totalNotifications <= 0" class="notifications-menu" disabled icon="bell">
{{ totalNotifications }}
<ds-button v-if="!notificationsCount" class="notifications-menu" disabled icon="bell">
{{ unreadNotificationsCount }}
</ds-button>
<dropdown v-else class="notifications-menu" :placement="placement">
<template slot="default" slot-scope="{ toggleMenu }">
<ds-button primary icon="bell" @click.prevent="toggleMenu">
{{ totalNotifications }}
<ds-button :primary="!!unreadNotificationsCount" icon="bell" @click.prevent="toggleMenu">
{{ unreadNotificationsCount }}
</ds-button>
</template>
<template slot="popover">
<div class="notifications-menu-popover">
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
<notification-list :notifications="displayedNotifications" @markAsRead="markAsRead" />
</div>
</template>
</dropdown>
@ -18,6 +18,7 @@
<script>
import Dropdown from '~/components/Dropdown'
import { NOTIFICATIONS_POLL_INTERVAL } from '~/constants/notifications'
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
import NotificationList from '../NotificationList/NotificationList'
@ -29,6 +30,7 @@ export default {
},
data() {
return {
displayedNotifications: [],
notifications: [],
}
},
@ -46,17 +48,29 @@ export default {
variables,
})
if (!(markAsRead && markAsRead.read === true)) return
this.notifications = this.notifications.map(n => {
return n.from.id === markAsRead.from.id ? markAsRead : n
this.displayedNotifications = this.displayedNotifications.map(n => {
return this.equalNotification(n, markAsRead) ? markAsRead : n
})
} catch (err) {
throw new Error(err)
this.$toast.error(err.message)
}
},
equalNotification(a, b) {
return a.from.id === b.from.id && a.createdAt === b.createdAt && a.reason === b.reason
},
},
computed: {
totalNotifications() {
return (this.notifications || []).length
notificationsCount() {
return (this.displayedNotifications || []).length
},
unreadNotificationsCount() {
let countUnread = 0
if (this.displayedNotifications) {
this.displayedNotifications.forEach(notification => {
if (!notification.read) countUnread++
})
}
return countUnread
},
},
apollo: {
@ -64,6 +78,23 @@ export default {
query() {
return notificationQuery(this.$i18n)
},
pollInterval() {
return NOTIFICATIONS_POLL_INTERVAL
},
update(data) {
const newNotifications = data.notifications.filter(newN => {
return !this.displayedNotifications.find(oldN => this.equalNotification(newN, oldN))
})
this.displayedNotifications = newNotifications
.concat(this.displayedNotifications)
.sort((a, b) => {
return new Date(b.createdAt) - new Date(a.createdAt)
})
return data.notifications
},
error(error) {
this.$toast.error(error)
},
},
},
}

View File

@ -0,0 +1 @@
export const NOTIFICATIONS_POLL_INTERVAL = 60000

View File

@ -135,11 +135,12 @@
}
},
"notifications": {
"menu": {
"reason": {
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
"commented_on_post": "Hat deinen Beitrag kommentiert …"
}
},
"comment": "Kommentar"
},
"search": {
"placeholder": "Suchen",

View File

@ -136,11 +136,12 @@
}
},
"notifications": {
"menu": {
"reason": {
"mentioned_in_post": "Mentioned you in a post …",
"mentioned_in_comment": "Mentioned you in a comment …",
"commented_on_post": "Commented on your post …"
}
},
"comment": "Comment"
},
"search": {
"placeholder": "Search",

View File

@ -0,0 +1,23 @@
function scrollToAnchor(anchor, { checkAnchor, $scrollTo }) {
if (typeof checkAnchor !== 'function')
throw new Error(
'You must define `checkAnchor` on the component if you use scrollToAnchor mixin!',
)
if (!checkAnchor(anchor)) return
setTimeout(() => {
$scrollTo(anchor)
}, 250)
}
export default {
watch: {
$route(to, from) {
const anchor = to && to.hash
scrollToAnchor(anchor, this)
},
},
mounted() {
const anchor = this.$route && this.$route.hash
scrollToAnchor(anchor, this)
},
}

View File

@ -124,80 +124,6 @@ export default {
middleware: ['authenticated', 'termsAndConditions'],
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active',
scrollBehavior: (to, _from, savedPosition) => {
let position = false
// if no children detected and scrollToTop is not explicitly disabled
if (
to.matched.length < 2 &&
to.matched.every(r => r.components.default.options.scrollToTop !== false)
) {
// scroll to the top of the page
position = {
x: 0,
y: 0,
}
} else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
// if one of the children has scrollToTop option set to true
position = {
x: 0,
y: 0,
}
}
// savedPosition is only available for popstate navigations (back button)
if (savedPosition) {
position = savedPosition
}
return new Promise(resolve => {
// wait for the out transition to complete (if necessary)
window.$nuxt.$once('triggerScroll', () => {
let processInterval = null
let processTime = 0
const callInterval = 100
const callIntervalLimit = 2000
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
// CSS.escape() is not supported with IE and Edge.
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
hash = '#' + window.CSS.escape(hash.substr(1))
}
try {
processInterval = setInterval(() => {
const hashIsFound = document.querySelector(hash)
if (hashIsFound) {
position = {
selector: hash,
offset: { x: 0, y: -500 },
}
}
processTime += callInterval
if (hashIsFound || processTime >= callIntervalLimit) {
clearInterval(processInterval)
processInterval = null
}
}, callInterval)
} catch (e) {
/* eslint-disable-next-line no-console */
console.warn(
'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).',
)
}
}
let resolveInterval = setInterval(() => {
if (!processInterval) {
clearInterval(resolveInterval)
resolve(position)
}
}, callInterval)
})
})
},
},
/*
@ -216,6 +142,13 @@ export default {
keys: envWhitelist,
},
],
[
'vue-scrollto/nuxt',
{
offset: -100, // to compensate fixed navbar height
duration: 1000,
},
],
'cookie-universal-nuxt',
'@nuxtjs/apollo',
'@nuxtjs/axios',

View File

@ -63,7 +63,7 @@
"apollo-client": "~2.6.4",
"cookie-universal-nuxt": "~2.0.18",
"cross-env": "~6.0.0",
"date-fns": "2.4.0",
"date-fns": "2.4.1",
"express": "~4.17.1",
"graphql": "~14.5.8",
"isemail": "^3.2.0",
@ -76,13 +76,14 @@
"stack-utils": "^1.0.2",
"string-hash": "^1.1.3",
"tippy.js": "^4.3.5",
"tiptap": "~1.25.0",
"tiptap-extensions": "~1.27.0",
"tiptap": "~1.26.0",
"tiptap-extensions": "~1.28.0",
"trunc-html": "^1.1.2",
"v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13",
"vue-infinite-scroll": "^2.0.2",
"vue-izitoast": "^1.2.1",
"vue-scrollto": "^2.17.1",
"vue-sweetalert-icons": "~4.2.0",
"vuex-i18n": "~1.13.1",
"xregexp": "^4.2.4",
@ -110,7 +111,7 @@
"eslint": "~5.16.0",
"eslint-config-prettier": "~6.3.0",
"eslint-config-standard": "~12.0.0",
"eslint-loader": "~3.0.1",
"eslint-loader": "~3.0.2",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.17.0",
"eslint-plugin-node": "~10.0.0",

View File

@ -77,17 +77,6 @@ export default {
]
},
},
watch: {
$route(to, from) {
if (to.hash === '#comments') {
window.scroll({
top: document.getElementById('comments').offsetTop,
left: 0,
behavior: 'smooth',
})
}
},
},
}
</script>

View File

@ -197,7 +197,7 @@ export default {
.ds-card-image {
img {
max-height: 300px;
height: 300px;
object-fit: cover;
object-position: center;
}

View File

@ -4235,6 +4235,11 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
bezier-easing@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86"
integrity sha1-wE3+i5JtbsrKGBPWn/F5t8ICXYY=
bfj@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.1.tgz#05a3b7784fbd72cfa3c22e56002ef99336516c48"
@ -5822,10 +5827,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.4.0.tgz#e02d1d08ce80ae1db3de40a0028c9f54203d034b"
integrity sha512-xS547fK1omgCgOGbyU0fBY2pdeXQ9/WO/PMsVgX1jtF56dXNHrV3Z+GKWIOE7IG+UEeu+fTyTlnIvBKbxXxdSw==
date-fns@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.4.1.tgz#b53f9bb65ae6bd9239437035710e01cf383b625e"
integrity sha512-2RhmH/sjDSCYW2F3ZQxOUx/I7PvzXpi89aQL2d3OAxSTwLx6NilATeUbe0menFE3Lu5lFkOFci36ivimwYHHxw==
date-fns@^1.27.2:
version "1.30.1"
@ -6477,11 +6482,12 @@ eslint-import-resolver-node@^0.3.2:
debug "^2.6.9"
resolve "^1.5.0"
eslint-loader@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-3.0.1.tgz#03f5693d7c2dc5b710c2bbe85ca500536dc3c852"
integrity sha512-opQF7tGGf793wrpBex6WP7TzcGqJ5/vpQ9nziuznYNWSw/g4dB/5M4y8h7TJP5u6R6tBIFkJheV3MJxsVbNHNg==
eslint-loader@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-3.0.2.tgz#5a627316a51d6f41d357b9f6f0554e91506cdd6e"
integrity sha512-S5VnD+UpVY1PyYRqeBd/4pgsmkvSokbHqTXAQMpvCyRr3XN2tvSLo9spm2nEpqQqh9dezw3os/0zWihLeOg2Rw==
dependencies:
fs-extra "^8.1.0"
loader-fs-cache "^1.0.2"
loader-utils "^1.2.3"
object-hash "^1.3.1"
@ -7316,16 +7322,7 @@ fs-extra@^7.0.0, fs-extra@^7.0.1:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.0.1.tgz#90294081f978b1f182f347a440a209154344285b"
integrity sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^8.1.0:
fs-extra@^8.0.1, fs-extra@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
@ -12333,10 +12330,10 @@ prosemirror-commands@^1.0.8:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-dropcursor@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.1.tgz#c60ed1ed6c58804a06a75db06a0d993b087b7622"
integrity sha512-GeUyMO/tOEf8MXrP7Xb7UIMrfK86OGh0fnyBrHfhav4VjY9cw65mNoqHy87CklE5711AhCP5Qzfp8RL/hVKusg==
prosemirror-dropcursor@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.2.tgz#d54428e0fdbc0fb3d4c5809acd1ad031e6cb6855"
integrity sha512-QHZbYPr8AY0g88TC/Wp7jpYbUoSpTSO8sqHNGvvZOInsAyylIdOpsrfhY1NC+/lh+iuwka0YogGtq2mmE7cr4g==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
@ -12377,10 +12374,10 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.1:
prosemirror-state "^1.0.0"
w3c-keyname "^1.1.8"
prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.7.1.tgz#f140a6e366e1e283aa7a94dbb8c2c7d13139689e"
integrity sha512-hYrZPbJvdo2QWERmkCuS80BEf5Rcf3+S28ETr4xu8XKPYjmU6aeQn23G1Fu/2rwqUmk5ZyWYo2nyEsN+Cdv2Qg==
prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.7.2.tgz#829abd7fb496783ba088936d2d7aff228206829a"
integrity sha512-mopozod/qNTB6utEyY8q4w1nCLDakpr39d8smzHno/wuAivCzBU8HkC9YOx1MBdTcTU6sXiIEh08hQfkC3damw==
dependencies:
orderedmap "^1.0.0"
@ -12400,10 +12397,10 @@ prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.2.4:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-tables@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.9.1.tgz#1669100ee9f64b0c269824dcd1c0584c66075acb"
integrity sha512-n5h2OvlnQGsW1ToT1WOIlemV/3PDw4miUQoHEpawOk2oDhi46czKdzEg/rq3z0f/aZ3CwoyxviuqAZChBILC4A==
prosemirror-tables@^0.9.5:
version "0.9.5"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.9.5.tgz#94d9881a46051e6fff3c51edffafa346da084def"
integrity sha512-RlAF/D7OvnDCOL8B6Qt6KuBkb0w3SedTdrou7wH7Nn2ml7+M5xUalW/h1f7dMD3wjsU47/Cn8zTbEkCDIpIggw==
dependencies:
prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0"
@ -12411,10 +12408,10 @@ prosemirror-tables@^0.9.1:
prosemirror-transform "^1.0.0"
prosemirror-view "^1.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.1.3.tgz#28cfdf1f9ee514edc40466be7b7db39eed545fdf"
integrity sha512-1O6Di5lOL1mp4nuCnQNkHY7l2roIW5y8RH4ZG3hMYmkmDEWzTaFFnxxAAHsE5ipGLBSRcTlP7SsDhYBIdSuLpQ==
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.1.4.tgz#30b35f02dd7761dd8139e5eb7612831fd031036a"
integrity sha512-1Y3XuaFJtwusYDvojcCxi3VZvNIntPVoh/dpeVaIM5Vf1V+M6xiIWcDgktUWWRovMxEhdibnpt5eyFmYJJhHtQ==
dependencies:
prosemirror-model "^1.0.0"
@ -12423,10 +12420,10 @@ prosemirror-utils@^0.9.6:
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973"
integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.10.0.tgz#7de5de75f0c90f8b9f09d09ed4467554d59adddb"
integrity sha512-STHw0xHfk+XPMqMLTKykRL1qEMtO+n1GWINBl94IPIq82AmWO1Ors4wVw93HKo/oIadWRrP/7faNJKh1UVLrTg==
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.11.4:
version "1.11.4"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.11.4.tgz#f80aec8924d59d4c3456dcc5bfea733758ec9b40"
integrity sha512-J0g7xiCDx+p3CtpC69E7HvMmnW7yCILEhOXxSANZPX8iIwUrVTfdWKAzufi9F9MoM08ewsaF254xV90NpkGWVQ==
dependencies:
prosemirror-model "^1.1.0"
prosemirror-state "^1.0.0"
@ -14515,62 +14512,62 @@ tippy.js@^4.3.5:
dependencies:
popper.js "^1.14.7"
tiptap-commands@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.11.0.tgz#8c259e93d70447e93cedefcfa73a36301cd60a81"
integrity sha512-WDX3JfI6Z80CCxkDfKUn6ya2UT3r1AM/McbB63oXq6iUyY5wZmw+qu/9LkSe3aISRTy9tfUKzJLjB7w9UnQ9Ig==
tiptap-commands@^1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.12.0.tgz#603b1c710c6950950eb1a7fc5279008f36bc2962"
integrity sha512-LWAVHOxsFR4yUJuruEwJ2QMwe0e9S4kHQ4HVIPEIofhuXKW4vmjvvX9Lzgi4cHy5cXC/TBAU2D43BNy7vdH1Kg==
dependencies:
prosemirror-commands "^1.0.8"
prosemirror-inputrules "^1.0.4"
prosemirror-model "^1.7.1"
prosemirror-model "^1.7.2"
prosemirror-schema-list "^1.0.3"
prosemirror-state "^1.2.4"
prosemirror-tables "^0.9.1"
prosemirror-tables "^0.9.5"
prosemirror-utils "^0.9.6"
tiptap-utils "^1.7.0"
tiptap-utils "^1.8.0"
tiptap-extensions@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.27.0.tgz#547d2fbc8234818d195eef45a46e14aed7859a70"
integrity sha512-DVazwQuEkWGjE45nhznB9LbD233s/0KOmWHcN6V1Ixm+/97Gaw1fEPUTIz/tHPYg3WKhPFOxI965sSB8Ne7Dnw==
tiptap-extensions@~1.28.0:
version "1.28.0"
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.28.0.tgz#4704945e7a4fe33a77de11847f7ca3058008895e"
integrity sha512-yGKXGUnOrLhnXpnhTrL4tDJv+CSgyqVu0//M80uiY097btYnf/K0t7i0StRCY3Xg5mX5YFL9Q01f9Ppyi2jgtQ==
dependencies:
lowlight "^1.12.1"
prosemirror-collab "^1.1.2"
prosemirror-history "^1.0.4"
prosemirror-model "^1.7.1"
prosemirror-model "^1.7.2"
prosemirror-state "^1.2.4"
prosemirror-tables "^0.9.1"
prosemirror-transform "^1.1.3"
prosemirror-tables "^0.9.5"
prosemirror-transform "^1.1.4"
prosemirror-utils "^0.9.6"
prosemirror-view "^1.10.0"
tiptap "^1.25.0"
tiptap-commands "^1.11.0"
prosemirror-view "^1.11.4"
tiptap "^1.26.0"
tiptap-commands "^1.12.0"
tiptap-utils@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.7.0.tgz#eb9d1f9e6be7b2e6b50b8aaf0bb47f1a68266b41"
integrity sha512-nJUrzR2cf+kcPyE2bIfzjnOewOynLm9kofQPIk2tMYwXfsgeNvYhMIbul4AJPYNoYyOLGUQ+vGpF6/5eUtC2Ew==
tiptap-utils@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.8.0.tgz#cb03a263a1b1672bf4cccccb2078506fa91bd112"
integrity sha512-0k7zuhwrNpEAnoiH8kjAE9IUnqV8FNX1bv9W7we+jhQZPUuxODcpMX1oUkrN9i1seFVfPcxgQa+SmIy63kRKig==
dependencies:
prosemirror-model "^1.7.1"
prosemirror-model "^1.7.2"
prosemirror-state "^1.2.4"
prosemirror-tables "^0.9.1"
prosemirror-tables "^0.9.5"
prosemirror-utils "^0.9.6"
tiptap@^1.25.0, tiptap@~1.25.0:
version "1.25.0"
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.25.0.tgz#4e518805ac99bb3d157b99f8b902231ad89f054a"
integrity sha512-wPE96JjoHIMaWTPkZZqz0cayVe+QgR+1J7FR4h5MvJepPgrtwwQmgVVx7jAG7yXXZZdUhhrYlL2yMji4V7Vpjw==
tiptap@^1.26.0, tiptap@~1.26.0:
version "1.26.0"
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.26.0.tgz#edaa07b4b9b6836d433d0b8017d26d37cc0cc3c9"
integrity sha512-lKJnZ4jL3luu3C5Y5aZIEj2spAfNPSwc5HPB+n9HhpSaWAfGM9XTOLm6I0EIbkLHiCnYNjItlLP6p1g+KPdtSw==
dependencies:
prosemirror-commands "^1.0.8"
prosemirror-dropcursor "^1.1.1"
prosemirror-dropcursor "^1.1.2"
prosemirror-gapcursor "^1.0.4"
prosemirror-inputrules "^1.0.4"
prosemirror-keymap "^1.0.1"
prosemirror-model "^1.7.1"
prosemirror-model "^1.7.2"
prosemirror-state "^1.2.4"
prosemirror-view "^1.10.0"
tiptap-commands "^1.11.0"
tiptap-utils "^1.7.0"
prosemirror-view "^1.11.4"
tiptap-commands "^1.12.0"
tiptap-utils "^1.8.0"
title-case@^2.1.0:
version "2.1.1"
@ -15315,6 +15312,13 @@ vue-router@~3.0.7:
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.7.tgz#b36ca107b4acb8ff5bc4ff824584059c23fcb87b"
integrity sha512-utJ+QR3YlIC/6x6xq17UMXeAfxEvXA0VKD3PiSio7hBOZNusA1jXcbxZxVEfJunLp48oonjTepY8ORoIlRx/EQ==
vue-scrollto@^2.17.1:
version "2.17.1"
resolved "https://registry.yarnpkg.com/vue-scrollto/-/vue-scrollto-2.17.1.tgz#cd62ee0b98cf7e2ba9fd94f029addcd093978a48"
integrity sha512-uxOJXg6cZL88B+hTXRHDJMR+gHGiaS70ZTNk55fE5Z2TdwyIx9K/IHoNeTrtBrM6u3FASAIymKjZaQLmDf8Ykg==
dependencies:
bezier-easing "2.1.0"
vue-server-renderer@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.6.10.tgz#cb2558842ead360ae2ec1f3719b75564a805b375"