Merge pull request #1133 from Human-Connection/384-emotions-on-posts
Emotions on posts
@ -157,6 +157,8 @@ const permissions = shield(
|
|||||||
User: or(noEmailFilter, isAdmin),
|
User: or(noEmailFilter, isAdmin),
|
||||||
isLoggedIn: allow,
|
isLoggedIn: allow,
|
||||||
Badge: allow,
|
Badge: allow,
|
||||||
|
PostsEmotionsCountByEmotion: allow,
|
||||||
|
PostsEmotionsByCurrentUser: allow,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
'*': deny,
|
'*': deny,
|
||||||
@ -178,7 +180,6 @@ const permissions = shield(
|
|||||||
// RemoveBadgeRewarded: isAdmin,
|
// RemoveBadgeRewarded: isAdmin,
|
||||||
reward: isAdmin,
|
reward: isAdmin,
|
||||||
unreward: isAdmin,
|
unreward: isAdmin,
|
||||||
// addFruitToBasket: isAuthenticated
|
|
||||||
follow: isAuthenticated,
|
follow: isAuthenticated,
|
||||||
unfollow: isAuthenticated,
|
unfollow: isAuthenticated,
|
||||||
shout: isAuthenticated,
|
shout: isAuthenticated,
|
||||||
@ -192,6 +193,8 @@ const permissions = shield(
|
|||||||
DeleteUser: isDeletingOwnAccount,
|
DeleteUser: isDeletingOwnAccount,
|
||||||
requestPasswordReset: allow,
|
requestPasswordReset: allow,
|
||||||
resetPassword: allow,
|
resetPassword: allow,
|
||||||
|
AddPostEmotions: isAuthenticated,
|
||||||
|
RemovePostEmotions: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: isMyOwn,
|
email: isMyOwn,
|
||||||
|
|||||||
34
backend/src/models/Post.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import uuid from 'uuid/v4'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
activityId: { type: 'string', allow: [null] },
|
||||||
|
objectId: { type: 'string', allow: [null] },
|
||||||
|
author: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'WROTE',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
|
title: { type: 'string', disallow: [null], min: 3 },
|
||||||
|
slug: { type: 'string', allow: [null] },
|
||||||
|
content: { type: 'string', disallow: [null], min: 3 },
|
||||||
|
contentExcerpt: { type: 'string', allow: [null] },
|
||||||
|
image: { type: 'string', allow: [null] },
|
||||||
|
deleted: { type: 'boolean', default: false },
|
||||||
|
disabled: { type: 'boolean', default: false },
|
||||||
|
disabledBy: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'DISABLED',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
isoDate: true,
|
||||||
|
required: true,
|
||||||
|
default: () => new Date().toISOString(),
|
||||||
|
},
|
||||||
|
language: { type: 'string', allow: [null] },
|
||||||
|
}
|
||||||
@ -56,4 +56,19 @@ module.exports = {
|
|||||||
required: true,
|
required: true,
|
||||||
default: () => new Date().toISOString(),
|
default: () => new Date().toISOString(),
|
||||||
},
|
},
|
||||||
|
emoted: {
|
||||||
|
type: 'relationships',
|
||||||
|
relationship: 'EMOTED',
|
||||||
|
target: 'Post',
|
||||||
|
direction: 'out',
|
||||||
|
properties: {
|
||||||
|
emotion: {
|
||||||
|
type: 'string',
|
||||||
|
valid: ['happy', 'cry', 'surprised', 'angry', 'funny'],
|
||||||
|
invalid: [null],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eager: true,
|
||||||
|
cascade: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,4 +6,5 @@ export default {
|
|||||||
InvitationCode: require('./InvitationCode.js'),
|
InvitationCode: require('./InvitationCode.js'),
|
||||||
EmailAddress: require('./EmailAddress.js'),
|
EmailAddress: require('./EmailAddress.js'),
|
||||||
SocialMedia: require('./SocialMedia.js'),
|
SocialMedia: require('./SocialMedia.js'),
|
||||||
|
Post: require('./Post.js'),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export default {
|
|||||||
delete params.categoryIds
|
delete params.categoryIds
|
||||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||||
params.id = params.id || uuid()
|
params.id = params.id || uuid()
|
||||||
|
|
||||||
let createPostCypher = `CREATE (post:Post {params})
|
let createPostCypher = `CREATE (post:Post {params})
|
||||||
WITH post
|
WITH post
|
||||||
MATCH (author:User {id: $userId})
|
MATCH (author:User {id: $userId})
|
||||||
@ -70,5 +71,80 @@ export default {
|
|||||||
|
|
||||||
return post.properties
|
return post.properties
|
||||||
},
|
},
|
||||||
|
AddPostEmotions: async (object, params, context, resolveInfo) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
const { to, data } = params
|
||||||
|
const { user } = context
|
||||||
|
const transactionRes = await session.run(
|
||||||
|
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
||||||
|
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
|
||||||
|
RETURN userFrom, postTo, emotedRelation`,
|
||||||
|
{ user, to, data },
|
||||||
|
)
|
||||||
|
session.close()
|
||||||
|
const [emoted] = transactionRes.records.map(record => {
|
||||||
|
return {
|
||||||
|
from: { ...record.get('userFrom').properties },
|
||||||
|
to: { ...record.get('postTo').properties },
|
||||||
|
...record.get('emotedRelation').properties,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return emoted
|
||||||
|
},
|
||||||
|
RemovePostEmotions: async (object, params, context, resolveInfo) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
const { to, data } = params
|
||||||
|
const { id: from } = context.user
|
||||||
|
const transactionRes = await session.run(
|
||||||
|
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||||
|
DELETE emotedRelation
|
||||||
|
RETURN userFrom, postTo`,
|
||||||
|
{ from, to, data },
|
||||||
|
)
|
||||||
|
session.close()
|
||||||
|
const [emoted] = transactionRes.records.map(record => {
|
||||||
|
return {
|
||||||
|
from: { ...record.get('userFrom').properties },
|
||||||
|
to: { ...record.get('postTo').properties },
|
||||||
|
emotion: data.emotion,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return emoted
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Query: {
|
||||||
|
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
const { postId, data } = params
|
||||||
|
const transactionRes = await session.run(
|
||||||
|
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||||
|
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||||
|
`,
|
||||||
|
{ postId, data },
|
||||||
|
)
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
const [emotionsCount] = transactionRes.records.map(record => {
|
||||||
|
return record.get('emotionsCount').low
|
||||||
|
})
|
||||||
|
|
||||||
|
return emotionsCount
|
||||||
|
},
|
||||||
|
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
const { postId } = params
|
||||||
|
const transactionRes = await session.run(
|
||||||
|
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||||
|
RETURN collect(emoted.emotion) as emotion`,
|
||||||
|
{ userId: context.user.id, postId },
|
||||||
|
)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
const [emotions] = transactionRes.records.map(record => {
|
||||||
|
return record.get('emotion')
|
||||||
|
})
|
||||||
|
return emotions
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { GraphQLClient } from 'graphql-request'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login } from '../../jest/helpers'
|
import { host, login, gql } from '../../jest/helpers'
|
||||||
|
import { neode, getDriver } from '../../bootstrap/neo4j'
|
||||||
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
const driver = getDriver()
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
let client
|
let client
|
||||||
let userParams
|
let userParams
|
||||||
let authorParams
|
let authorParams
|
||||||
@ -14,7 +20,7 @@ const oldContent = 'Old content'
|
|||||||
const newTitle = 'New title'
|
const newTitle = 'New title'
|
||||||
const newContent = 'New content'
|
const newContent = 'New content'
|
||||||
const createPostVariables = { title: postTitle, content: postContent }
|
const createPostVariables = { title: postTitle, content: postContent }
|
||||||
const createPostWithCategoriesMutation = `
|
const createPostWithCategoriesMutation = gql`
|
||||||
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
|
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
|
||||||
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
|
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
|
||||||
id
|
id
|
||||||
@ -27,7 +33,7 @@ const createPostWithCategoriesVariables = {
|
|||||||
content: postContent,
|
content: postContent,
|
||||||
categoryIds: ['cat9', 'cat4', 'cat15'],
|
categoryIds: ['cat9', 'cat4', 'cat15'],
|
||||||
}
|
}
|
||||||
const postQueryWithCategories = `
|
const postQueryWithCategories = gql`
|
||||||
query($id: ID) {
|
query($id: ID) {
|
||||||
Post(id: $id) {
|
Post(id: $id) {
|
||||||
categories {
|
categories {
|
||||||
@ -41,9 +47,9 @@ const createPostWithoutCategoriesVariables = {
|
|||||||
content: 'I should be able to filter it out',
|
content: 'I should be able to filter it out',
|
||||||
categoryIds: null,
|
categoryIds: null,
|
||||||
}
|
}
|
||||||
const postQueryFilteredByCategory = `
|
const postQueryFilteredByCategory = gql`
|
||||||
query Post($filter: _PostFilter) {
|
query Post($filter: _PostFilter) {
|
||||||
Post(filter: $filter) {
|
Post(filter: $filter) {
|
||||||
title
|
title
|
||||||
id
|
id
|
||||||
categories {
|
categories {
|
||||||
@ -56,13 +62,28 @@ const postCategoriesFilterParam = { categories_some: { id_in: ['cat4'] } }
|
|||||||
const postQueryFilteredByCategoryVariables = {
|
const postQueryFilteredByCategoryVariables = {
|
||||||
filter: postCategoriesFilterParam,
|
filter: postCategoriesFilterParam,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createPostMutation = gql`
|
||||||
|
mutation($title: String!, $content: String!) {
|
||||||
|
CreatePost(title: $title, content: $content) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
content
|
||||||
|
slug
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
userParams = {
|
userParams = {
|
||||||
|
id: 'u198',
|
||||||
name: 'TestUser',
|
name: 'TestUser',
|
||||||
email: 'test@example.org',
|
email: 'test@example.org',
|
||||||
password: '1234',
|
password: '1234',
|
||||||
}
|
}
|
||||||
authorParams = {
|
authorParams = {
|
||||||
|
id: 'u25',
|
||||||
email: 'author@example.org',
|
email: 'author@example.org',
|
||||||
password: '1234',
|
password: '1234',
|
||||||
}
|
}
|
||||||
@ -74,22 +95,12 @@ afterEach(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('CreatePost', () => {
|
describe('CreatePost', () => {
|
||||||
const mutation = `
|
|
||||||
mutation($title: String!, $content: String!) {
|
|
||||||
CreatePost(title: $title, content: $content) {
|
|
||||||
title
|
|
||||||
content
|
|
||||||
slug
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
client = new GraphQLClient(host)
|
client = new GraphQLClient(host)
|
||||||
await expect(client.request(mutation, createPostVariables)).rejects.toThrow('Not Authorised')
|
await expect(client.request(createPostMutation, createPostVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -107,19 +118,23 @@ describe('CreatePost', () => {
|
|||||||
content: postContent,
|
content: postContent,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
|
await expect(client.request(createPostMutation, createPostVariables)).resolves.toMatchObject(
|
||||||
|
expected,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('assigns the authenticated user as author', async () => {
|
it('assigns the authenticated user as author', async () => {
|
||||||
await client.request(mutation, createPostVariables)
|
await client.request(createPostMutation, createPostVariables)
|
||||||
const { User } = await client.request(
|
const { User } = await client.request(
|
||||||
`{
|
gql`
|
||||||
User(name: "TestUser") {
|
{
|
||||||
contributions {
|
User(name: "TestUser") {
|
||||||
title
|
contributions {
|
||||||
|
title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
`,
|
||||||
{ headers },
|
{ headers },
|
||||||
)
|
)
|
||||||
expect(User).toEqual([{ contributions: [{ title: postTitle }] }])
|
expect(User).toEqual([{ contributions: [{ title: postTitle }] }])
|
||||||
@ -128,13 +143,15 @@ describe('CreatePost', () => {
|
|||||||
describe('disabled and deleted', () => {
|
describe('disabled and deleted', () => {
|
||||||
it('initially false', async () => {
|
it('initially false', async () => {
|
||||||
const expected = { CreatePost: { disabled: false, deleted: false } }
|
const expected = { CreatePost: { disabled: false, deleted: false } }
|
||||||
await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
|
await expect(
|
||||||
|
client.request(createPostMutation, createPostVariables),
|
||||||
|
).resolves.toMatchObject(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('language', () => {
|
describe('language', () => {
|
||||||
it('allows a user to set the language of the post', async () => {
|
it('allows a user to set the language of the post', async () => {
|
||||||
const createPostWithLanguageMutation = `
|
const createPostWithLanguageMutation = gql`
|
||||||
mutation($title: String!, $content: String!, $language: String) {
|
mutation($title: String!, $content: String!, $language: String) {
|
||||||
CreatePost(title: $title, content: $content, language: $language) {
|
CreatePost(title: $title, content: $content, language: $language) {
|
||||||
language
|
language
|
||||||
@ -222,7 +239,7 @@ describe('UpdatePost', () => {
|
|||||||
title: oldTitle,
|
title: oldTitle,
|
||||||
content: oldContent,
|
content: oldContent,
|
||||||
})
|
})
|
||||||
updatePostMutation = `
|
updatePostMutation = gql`
|
||||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||||
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||||
id
|
id
|
||||||
@ -328,7 +345,7 @@ describe('UpdatePost', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('DeletePost', () => {
|
describe('DeletePost', () => {
|
||||||
const mutation = `
|
const mutation = gql`
|
||||||
mutation($id: ID!) {
|
mutation($id: ID!) {
|
||||||
DeletePost(id: $id) {
|
DeletePost(id: $id) {
|
||||||
id
|
id
|
||||||
@ -383,3 +400,315 @@ describe('DeletePost', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('emotions', () => {
|
||||||
|
let addPostEmotionsVariables,
|
||||||
|
someUser,
|
||||||
|
ownerNode,
|
||||||
|
owner,
|
||||||
|
postMutationAction,
|
||||||
|
user,
|
||||||
|
postQueryAction,
|
||||||
|
postToEmote,
|
||||||
|
postToEmoteNode
|
||||||
|
const PostsEmotionsCountQuery = `
|
||||||
|
query($id: ID!) {
|
||||||
|
Post(id: $id) {
|
||||||
|
emotionsCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const PostsEmotionsQuery = gql`
|
||||||
|
query($id: ID!) {
|
||||||
|
Post(id: $id) {
|
||||||
|
emotions {
|
||||||
|
emotion
|
||||||
|
User {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const addPostEmotionsMutation = gql`
|
||||||
|
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
|
||||||
|
AddPostEmotions(to: $to, data: $data) {
|
||||||
|
from {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
emotion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
beforeEach(async () => {
|
||||||
|
userParams.id = 'u1987'
|
||||||
|
authorParams.id = 'u257'
|
||||||
|
createPostVariables.id = 'p1376'
|
||||||
|
const someUserNode = await instance.create('User', userParams)
|
||||||
|
someUser = await someUserNode.toJson()
|
||||||
|
ownerNode = await instance.create('User', authorParams)
|
||||||
|
owner = await ownerNode.toJson()
|
||||||
|
postToEmoteNode = await instance.create('Post', createPostVariables)
|
||||||
|
postToEmote = await postToEmoteNode.toJson()
|
||||||
|
await postToEmoteNode.relateTo(ownerNode, 'author')
|
||||||
|
|
||||||
|
postMutationAction = async (user, mutation, variables) => {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
driver,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { mutate } = createTestClient(server)
|
||||||
|
|
||||||
|
return mutate({
|
||||||
|
mutation,
|
||||||
|
variables,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
postQueryAction = async (postQuery, variables) => {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
driver,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
return query({ query: postQuery, variables })
|
||||||
|
}
|
||||||
|
addPostEmotionsVariables = {
|
||||||
|
to: { id: postToEmote.id },
|
||||||
|
data: { emotion: 'happy' },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('AddPostEmotions', () => {
|
||||||
|
let postsEmotionsQueryVariables
|
||||||
|
beforeEach(async () => {
|
||||||
|
postsEmotionsQueryVariables = { id: postToEmote.id }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
user = null
|
||||||
|
const addPostEmotions = await postMutationAction(
|
||||||
|
user,
|
||||||
|
addPostEmotionsMutation,
|
||||||
|
addPostEmotionsVariables,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(addPostEmotions.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated and not the author', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
user = someUser
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an emotion to the post', async () => {
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
AddPostEmotions: {
|
||||||
|
from: { id: user.id },
|
||||||
|
to: addPostEmotionsVariables.to,
|
||||||
|
emotion: 'happy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables),
|
||||||
|
).resolves.toEqual(expect.objectContaining(expected))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('limits the addition of the same emotion to 1', async () => {
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
emotionsCount: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
|
||||||
|
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
|
||||||
|
await expect(
|
||||||
|
postQueryAction(PostsEmotionsCountQuery, postsEmotionsQueryVariables),
|
||||||
|
).resolves.toEqual(expect.objectContaining(expected))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows a user to add more than one emotion', async () => {
|
||||||
|
const expectedEmotions = [
|
||||||
|
{ emotion: 'happy', User: { id: user.id } },
|
||||||
|
{ emotion: 'surprised', User: { id: user.id } },
|
||||||
|
]
|
||||||
|
const expectedResponse = {
|
||||||
|
data: { Post: [{ emotions: expect.arrayContaining(expectedEmotions) }] },
|
||||||
|
}
|
||||||
|
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
|
||||||
|
addPostEmotionsVariables.data.emotion = 'surprised'
|
||||||
|
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
|
||||||
|
await expect(
|
||||||
|
postQueryAction(PostsEmotionsQuery, postsEmotionsQueryVariables),
|
||||||
|
).resolves.toEqual(expect.objectContaining(expectedResponse))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as author', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
user = owner
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an emotion to the post', async () => {
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
AddPostEmotions: {
|
||||||
|
from: { id: owner.id },
|
||||||
|
to: addPostEmotionsVariables.to,
|
||||||
|
emotion: 'happy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables),
|
||||||
|
).resolves.toEqual(expect.objectContaining(expected))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('RemovePostEmotions', () => {
|
||||||
|
let removePostEmotionsVariables, postsEmotionsQueryVariables
|
||||||
|
const removePostEmotionsMutation = gql`
|
||||||
|
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
|
||||||
|
RemovePostEmotions(to: $to, data: $data) {
|
||||||
|
from {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
emotion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
beforeEach(async () => {
|
||||||
|
await ownerNode.relateTo(postToEmoteNode, 'emoted', { emotion: 'cry' })
|
||||||
|
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
|
||||||
|
|
||||||
|
postsEmotionsQueryVariables = { id: postToEmote.id }
|
||||||
|
removePostEmotionsVariables = {
|
||||||
|
to: { id: postToEmote.id },
|
||||||
|
data: { emotion: 'cry' },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
user = null
|
||||||
|
const removePostEmotions = await postMutationAction(
|
||||||
|
user,
|
||||||
|
removePostEmotionsMutation,
|
||||||
|
removePostEmotionsVariables,
|
||||||
|
)
|
||||||
|
expect(removePostEmotions.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
describe('but not the emoter', () => {
|
||||||
|
it('returns null if the emotion could not be found', async () => {
|
||||||
|
user = someUser
|
||||||
|
const removePostEmotions = await postMutationAction(
|
||||||
|
user,
|
||||||
|
removePostEmotionsMutation,
|
||||||
|
removePostEmotionsVariables,
|
||||||
|
)
|
||||||
|
expect(removePostEmotions).toEqual(
|
||||||
|
expect.objectContaining({ data: { RemovePostEmotions: null } }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('as the emoter', () => {
|
||||||
|
it('removes an emotion from a post', async () => {
|
||||||
|
user = owner
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
RemovePostEmotions: {
|
||||||
|
to: { id: postToEmote.id },
|
||||||
|
from: { id: user.id },
|
||||||
|
emotion: 'cry',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
postMutationAction(user, removePostEmotionsMutation, removePostEmotionsVariables),
|
||||||
|
).resolves.toEqual(expect.objectContaining(expected))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes only the requested emotion, not all emotions', async () => {
|
||||||
|
const expectedEmotions = [{ emotion: 'happy', User: { id: authorParams.id } }]
|
||||||
|
const expectedResponse = {
|
||||||
|
data: { Post: [{ emotions: expect.arrayContaining(expectedEmotions) }] },
|
||||||
|
}
|
||||||
|
await postMutationAction(user, removePostEmotionsMutation, removePostEmotionsVariables)
|
||||||
|
await expect(
|
||||||
|
postQueryAction(PostsEmotionsQuery, postsEmotionsQueryVariables),
|
||||||
|
).resolves.toEqual(expect.objectContaining(expectedResponse))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('posts emotions count', () => {
|
||||||
|
let PostsEmotionsCountByEmotionVariables
|
||||||
|
let PostsEmotionsByCurrentUserVariables
|
||||||
|
|
||||||
|
const PostsEmotionsCountByEmotionQuery = gql`
|
||||||
|
query($postId: ID!, $data: _EMOTEDInput!) {
|
||||||
|
PostsEmotionsCountByEmotion(postId: $postId, data: $data)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const PostsEmotionsByCurrentUserQuery = gql`
|
||||||
|
query($postId: ID!) {
|
||||||
|
PostsEmotionsByCurrentUser(postId: $postId)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
beforeEach(async () => {
|
||||||
|
await ownerNode.relateTo(postToEmoteNode, 'emoted', { emotion: 'cry' })
|
||||||
|
|
||||||
|
PostsEmotionsCountByEmotionVariables = {
|
||||||
|
postId: postToEmote.id,
|
||||||
|
data: { emotion: 'cry' },
|
||||||
|
}
|
||||||
|
PostsEmotionsByCurrentUserVariables = { postId: postToEmote.id }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('PostsEmotionsCountByEmotion', () => {
|
||||||
|
it("returns a post's emotions count", async () => {
|
||||||
|
const expectedResponse = { data: { PostsEmotionsCountByEmotion: 1 } }
|
||||||
|
await expect(
|
||||||
|
postQueryAction(PostsEmotionsCountByEmotionQuery, PostsEmotionsCountByEmotionVariables),
|
||||||
|
).resolves.toEqual(expect.objectContaining(expectedResponse))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('PostsEmotionsCountByEmotion', () => {
|
||||||
|
it("returns a currentUser's emotions on a post", async () => {
|
||||||
|
const expectedResponse = { data: { PostsEmotionsByCurrentUser: ['cry'] } }
|
||||||
|
await expect(
|
||||||
|
postQueryAction(PostsEmotionsByCurrentUserQuery, PostsEmotionsByCurrentUserVariables),
|
||||||
|
).resolves.toEqual(expect.objectContaining(expectedResponse))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -7,4 +7,4 @@ type EMOTED @relation(name: "EMOTED") {
|
|||||||
#updatedAt: DateTime
|
#updatedAt: DateTime
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,8 @@ type Post {
|
|||||||
)
|
)
|
||||||
|
|
||||||
emotions: [EMOTED]
|
emotions: [EMOTED]
|
||||||
|
emotionsCount: Int!
|
||||||
|
@cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
@ -89,4 +91,11 @@ type Mutation {
|
|||||||
language: String
|
language: String
|
||||||
categoryIds: [ID]
|
categoryIds: [ID]
|
||||||
): Post
|
): Post
|
||||||
|
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
|
||||||
|
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
|
||||||
|
PostsEmotionsByCurrentUser(postId: ID!): [String]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,6 +128,22 @@ export default function Factory(options = {}) {
|
|||||||
this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver })
|
this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver })
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
async emote({ to, data }) {
|
||||||
|
const mutation = `
|
||||||
|
mutation {
|
||||||
|
AddPostEmotions(
|
||||||
|
to: { id: "${to}" },
|
||||||
|
data: { emotion: ${data} }
|
||||||
|
) {
|
||||||
|
from { id }
|
||||||
|
to { id }
|
||||||
|
emotion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
this.lastResponse = await this.graphQLClient.request(mutation)
|
||||||
|
return this
|
||||||
|
},
|
||||||
}
|
}
|
||||||
result.authenticateAs.bind(result)
|
result.authenticateAs.bind(result)
|
||||||
result.create.bind(result)
|
result.create.bind(result)
|
||||||
|
|||||||
@ -488,6 +488,116 @@ import Factory from './factories'
|
|||||||
from: 'p15',
|
from: 'p15',
|
||||||
to: 'Demokratie',
|
to: 'Demokratie',
|
||||||
}),
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u1',
|
||||||
|
to: 'p15',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u2',
|
||||||
|
to: 'p15',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u3',
|
||||||
|
to: 'p15',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u4',
|
||||||
|
to: 'p15',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u5',
|
||||||
|
to: 'p15',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u6',
|
||||||
|
to: 'p15',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u7',
|
||||||
|
to: 'p15',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u2',
|
||||||
|
to: 'p14',
|
||||||
|
data: 'cry',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u3',
|
||||||
|
to: 'p13',
|
||||||
|
data: 'angry',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u4',
|
||||||
|
to: 'p12',
|
||||||
|
data: 'funny',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u5',
|
||||||
|
to: 'p11',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u6',
|
||||||
|
to: 'p10',
|
||||||
|
data: 'cry',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u5',
|
||||||
|
to: 'p9',
|
||||||
|
data: 'happy',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u4',
|
||||||
|
to: 'p8',
|
||||||
|
data: 'angry',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u3',
|
||||||
|
to: 'p7',
|
||||||
|
data: 'funny',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u2',
|
||||||
|
to: 'p6',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u1',
|
||||||
|
to: 'p5',
|
||||||
|
data: 'cry',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u2',
|
||||||
|
to: 'p4',
|
||||||
|
data: 'happy',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u3',
|
||||||
|
to: 'p3',
|
||||||
|
data: 'angry',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u4',
|
||||||
|
to: 'p2',
|
||||||
|
data: 'funny',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u5',
|
||||||
|
to: 'p1',
|
||||||
|
data: 'surprised',
|
||||||
|
}),
|
||||||
|
f.emote({
|
||||||
|
from: 'u6',
|
||||||
|
to: 'p0',
|
||||||
|
data: 'cry',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
127
webapp/components/Emotions/Emotions.spec.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import Emotions from './Emotions.vue'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import PostMutations from '~/graphql/PostMutations.js'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
localVue.use(Vuex)
|
||||||
|
|
||||||
|
describe('Emotions.vue', () => {
|
||||||
|
let wrapper
|
||||||
|
let mocks
|
||||||
|
let propsData
|
||||||
|
let getters
|
||||||
|
let funnyButton
|
||||||
|
let funnyImage
|
||||||
|
const funnyImageSrc = '/img/svg/emoji/funny_color.svg'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$apollo: {
|
||||||
|
mutate: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
AddPostEmotions: {
|
||||||
|
to: { id: 'p143' },
|
||||||
|
data: { emotion: 'happy' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
RemovePostEmotions: {
|
||||||
|
from: { id: 'u176' },
|
||||||
|
to: { id: 'p143' },
|
||||||
|
data: { emotion: 'happy' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
query: jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
PostsEmotionsCountByEmotion: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
$t: jest.fn(),
|
||||||
|
}
|
||||||
|
propsData = {
|
||||||
|
post: { id: 'p143' },
|
||||||
|
}
|
||||||
|
getters = {
|
||||||
|
'auth/user': () => {
|
||||||
|
return { id: 'u176' }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
describe('mount', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
getters,
|
||||||
|
})
|
||||||
|
return mount(Emotions, { mocks, propsData, store, localVue })
|
||||||
|
}
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("queries the post's emotions count for each of the 5 emotions", () => {
|
||||||
|
expect(mocks.$apollo.query).toHaveBeenCalledTimes(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('adding emotions', () => {
|
||||||
|
let expectedParams
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.vm.PostsEmotionsCountByEmotion.funny = 0
|
||||||
|
funnyButton = wrapper.findAll('button').at(0)
|
||||||
|
funnyButton.trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the colored image when the button is active', () => {
|
||||||
|
funnyImage = wrapper.findAll('img').at(0)
|
||||||
|
expect(funnyImage.attributes().src).toEqual(funnyImageSrc)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends the AddPostEmotionsMutation for an emotion when clicked', () => {
|
||||||
|
expectedParams = {
|
||||||
|
mutation: PostMutations().AddPostEmotionsMutation,
|
||||||
|
variables: { to: { id: 'p143' }, data: { emotion: 'funny' } },
|
||||||
|
}
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('increases the PostsEmotionsCountByEmotion for the emotion clicked', () => {
|
||||||
|
expect(wrapper.vm.PostsEmotionsCountByEmotion.funny).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an emotion to selectedEmotions to show the colored image when the button is active', () => {
|
||||||
|
expect(wrapper.vm.selectedEmotions).toEqual(['funny'])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('removing emotions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
funnyButton.trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends the RemovePostEmotionsMutation when a user clicks on an active emotion', () => {
|
||||||
|
expectedParams = {
|
||||||
|
mutation: PostMutations().RemovePostEmotionsMutation,
|
||||||
|
variables: { to: { id: 'p143' }, data: { emotion: 'funny' } },
|
||||||
|
}
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('decreases the PostsEmotionsCountByEmotion for the emotion clicked', async () => {
|
||||||
|
expect(wrapper.vm.PostsEmotionsCountByEmotion.funny).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes an emotion from selectedEmotions to show the default image', async () => {
|
||||||
|
expect(wrapper.vm.selectedEmotions).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
115
webapp/components/Emotions/Emotions.vue
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<ds-flex :gutter="{ lg: 'large' }" class="emotions-flex">
|
||||||
|
<div v-for="emotion in Object.keys(PostsEmotionsCountByEmotion)" :key="emotion">
|
||||||
|
<ds-flex-item :width="{ lg: '100%' }">
|
||||||
|
<hc-emotions-button
|
||||||
|
@toggleEmotion="toggleEmotion"
|
||||||
|
:PostsEmotionsCountByEmotion="PostsEmotionsCountByEmotion"
|
||||||
|
:iconPath="iconPath(emotion)"
|
||||||
|
:emotion="emotion"
|
||||||
|
/>
|
||||||
|
</ds-flex-item>
|
||||||
|
</div>
|
||||||
|
</ds-flex>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import HcEmotionsButton from '~/components/EmotionsButton/EmotionsButton'
|
||||||
|
import { PostsEmotionsByCurrentUser } from '~/graphql/PostQuery.js'
|
||||||
|
import PostMutations from '~/graphql/PostMutations.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HcEmotionsButton,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
post: { type: Object, default: () => {} },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedEmotions: [],
|
||||||
|
PostsEmotionsCountByEmotion: { funny: 0, happy: 0, surprised: 0, cry: 0, angry: 0 },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
currentUser: 'auth/user',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
Object.keys(this.PostsEmotionsCountByEmotion).map(emotion => {
|
||||||
|
this.emotionsCount(emotion)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
iconPath(emotion) {
|
||||||
|
if (this.isActive(emotion)) {
|
||||||
|
return `/img/svg/emoji/${emotion}_color.svg`
|
||||||
|
}
|
||||||
|
return `/img/svg/emoji/${emotion}.svg`
|
||||||
|
},
|
||||||
|
toggleEmotion(emotion) {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: this.isActive(emotion)
|
||||||
|
? PostMutations().RemovePostEmotionsMutation
|
||||||
|
: PostMutations().AddPostEmotionsMutation,
|
||||||
|
variables: {
|
||||||
|
to: { id: this.post.id },
|
||||||
|
data: { emotion },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.isActive(emotion)
|
||||||
|
? this.PostsEmotionsCountByEmotion[emotion]--
|
||||||
|
: this.PostsEmotionsCountByEmotion[emotion]++
|
||||||
|
|
||||||
|
const index = this.selectedEmotions.indexOf(emotion)
|
||||||
|
if (index > -1) {
|
||||||
|
this.selectedEmotions.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
this.selectedEmotions.push(emotion)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isActive(emotion) {
|
||||||
|
const index = this.selectedEmotions.indexOf(emotion)
|
||||||
|
if (index > -1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
emotionsCount(emotion) {
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: gql`
|
||||||
|
query($postId: ID!, $data: _EMOTEDInput!) {
|
||||||
|
PostsEmotionsCountByEmotion(postId: $postId, data: $data)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { postId: this.post.id, data: { emotion } },
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
})
|
||||||
|
.then(({ data: { PostsEmotionsCountByEmotion } }) => {
|
||||||
|
this.PostsEmotionsCountByEmotion[emotion] = PostsEmotionsCountByEmotion
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
PostsEmotionsByCurrentUser: {
|
||||||
|
query() {
|
||||||
|
return PostsEmotionsByCurrentUser()
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
postId: this.post.id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result({ data: { PostsEmotionsByCurrentUser } }) {
|
||||||
|
this.selectedEmotions = PostsEmotionsByCurrentUser
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
50
webapp/components/EmotionsButton/EmotionsButton.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ds-button size="large" ghost @click="toggleEmotion(emotion)" class="emotions-buttons">
|
||||||
|
<img :src="iconPath" width="40" />
|
||||||
|
</ds-button>
|
||||||
|
<ds-space margin-bottom="xx-small" />
|
||||||
|
<div class="emotions-mobile-space">
|
||||||
|
<p class="emotions-label">{{ $t(`contribution.emotions-label.${emotion}`) }}</p>
|
||||||
|
<p style="display: inline" :key="PostsEmotionsCountByEmotion[emotion]">
|
||||||
|
{{ PostsEmotionsCountByEmotion[emotion] }}x
|
||||||
|
</p>
|
||||||
|
{{ $t('contribution.emotions-label.emoted') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
iconPath: { type: String, default: null },
|
||||||
|
PostsEmotionsCountByEmotion: { type: Object, default: () => {} },
|
||||||
|
emotion: { type: String, default: null },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleEmotion(emotion) {
|
||||||
|
this.$emit('toggleEmotion', emotion)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.emotions-flex {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotions-label {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotions-buttons {
|
||||||
|
&:hover {
|
||||||
|
background-color: $background-color-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 960px) {
|
||||||
|
.emotions-mobile-space {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-space margin="large" style="text-align: center">
|
<ds-space margin="xx-small" class="text-align-center">
|
||||||
<ds-button
|
<ds-button
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@ -88,4 +88,7 @@ export default {
|
|||||||
.shout-button-text {
|
.shout-button-text {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.text-align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -60,5 +60,31 @@ export default () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
AddPostEmotionsMutation: gql`
|
||||||
|
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
|
||||||
|
AddPostEmotions(to: $to, data: $data) {
|
||||||
|
emotion
|
||||||
|
from {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
RemovePostEmotionsMutation: gql`
|
||||||
|
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
|
||||||
|
RemovePostEmotions(to: $to, data: $data) {
|
||||||
|
emotion
|
||||||
|
from {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,6 +71,7 @@ export default i18n => {
|
|||||||
}
|
}
|
||||||
shoutedCount
|
shoutedCount
|
||||||
shoutedByCurrentUser
|
shoutedByCurrentUser
|
||||||
|
emotionsCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -120,3 +121,11 @@ export const filterPosts = i18n => {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PostsEmotionsByCurrentUser = () => {
|
||||||
|
return gql`
|
||||||
|
query PostsEmotionsByCurrentUser($postId: ID!) {
|
||||||
|
PostsEmotionsByCurrentUser(postId: $postId)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
@ -420,6 +420,14 @@
|
|||||||
"languageSelectLabel": "Sprache",
|
"languageSelectLabel": "Sprache",
|
||||||
"categories": {
|
"categories": {
|
||||||
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
||||||
|
},
|
||||||
|
"emotions-label": {
|
||||||
|
"funny": "Lustig",
|
||||||
|
"happy": "Glücklich",
|
||||||
|
"surprised": "Erstaunt",
|
||||||
|
"cry": "Zum Weinen",
|
||||||
|
"angry": "Verärgert",
|
||||||
|
"emoted": "angegeben"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"changelog": {
|
"changelog": {
|
||||||
|
|||||||
@ -420,6 +420,14 @@
|
|||||||
"languageSelectLabel": "Language",
|
"languageSelectLabel": "Language",
|
||||||
"categories": {
|
"categories": {
|
||||||
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
||||||
|
},
|
||||||
|
"emotions-label": {
|
||||||
|
"funny": "Funny",
|
||||||
|
"happy": "Happy",
|
||||||
|
"surprised": "Surprised",
|
||||||
|
"cry": "Cry",
|
||||||
|
"angry": "Angry",
|
||||||
|
"emoted": "emoted"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"changelog": {
|
"changelog": {
|
||||||
|
|||||||
@ -31,7 +31,7 @@ describe('PostSlug', () => {
|
|||||||
$filters: {
|
$filters: {
|
||||||
truncate: a => a,
|
truncate: a => a,
|
||||||
},
|
},
|
||||||
// If you mocking router, than don't use VueRouter with lacalVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
|
// If you are mocking the router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
|
||||||
$router: {
|
$router: {
|
||||||
history: {
|
history: {
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
|
|||||||
@ -40,14 +40,30 @@
|
|||||||
<ds-space margin="xx-small" />
|
<ds-space margin="xx-small" />
|
||||||
<hc-tag v-for="tag in post.tags" :key="tag.id" :name="tag.name" />
|
<hc-tag v-for="tag in post.tags" :key="tag.id" :name="tag.name" />
|
||||||
</div>
|
</div>
|
||||||
<!-- Shout Button -->
|
<ds-space margin-top="x-large">
|
||||||
<hc-shout-button
|
<ds-flex :gutter="{ lg: 'small' }">
|
||||||
v-if="post.author"
|
<ds-flex-item
|
||||||
:disabled="isAuthor(post.author.id)"
|
:width="{ lg: '75%', md: '75%', sm: '75%' }"
|
||||||
:count="post.shoutedCount"
|
class="emotions-buttons-mobile"
|
||||||
:is-shouted="post.shoutedByCurrentUser"
|
>
|
||||||
:post-id="post.id"
|
<hc-emotions :post="post" />
|
||||||
/>
|
</ds-flex-item>
|
||||||
|
<ds-flex-item :width="{ lg: '10%', md: '3%', sm: '3%' }" />
|
||||||
|
<!-- Shout Button -->
|
||||||
|
<ds-flex-item
|
||||||
|
:width="{ lg: '15%', md: '22%', sm: '22%', base: '100%' }"
|
||||||
|
class="shout-button"
|
||||||
|
>
|
||||||
|
<hc-shout-button
|
||||||
|
v-if="post.author"
|
||||||
|
:disabled="isAuthor(post.author.id)"
|
||||||
|
:count="post.shoutedCount"
|
||||||
|
:is-shouted="post.shoutedByCurrentUser"
|
||||||
|
:post-id="post.id"
|
||||||
|
/>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-space>
|
||||||
<!-- Comments -->
|
<!-- Comments -->
|
||||||
<ds-section slot="footer">
|
<ds-section slot="footer">
|
||||||
<hc-comment-list :post="post" />
|
<hc-comment-list :post="post" />
|
||||||
@ -69,6 +85,7 @@ import HcCommentForm from '~/components/comments/CommentForm'
|
|||||||
import HcCommentList from '~/components/comments/CommentList'
|
import HcCommentList from '~/components/comments/CommentList'
|
||||||
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
|
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
|
||||||
import PostQuery from '~/graphql/PostQuery.js'
|
import PostQuery from '~/graphql/PostQuery.js'
|
||||||
|
import HcEmotions from '~/components/Emotions/Emotions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PostSlug',
|
name: 'PostSlug',
|
||||||
@ -84,6 +101,7 @@ export default {
|
|||||||
ContentMenu,
|
ContentMenu,
|
||||||
HcCommentForm,
|
HcCommentForm,
|
||||||
HcCommentList,
|
HcCommentList,
|
||||||
|
HcEmotions,
|
||||||
ContentViewer,
|
ContentViewer,
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
@ -198,4 +216,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media only screen and (max-width: 960px) {
|
||||||
|
.shout-button {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
1
webapp/static/img/svg/emoji/angry.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="<Pfad>" cx="40" cy="40.1" r="40" fill="#cac9c9"/><ellipse data-name="<Pfad>" cx="38.4" cy="38.8" rx="37.4" ry="37.6" fill="#d7d8d8"/><path d="M39.8 55.6c8.8 0 13.4 7.7 13.4 11.6a.8.8 0 0 1-1 1s-5-6-12.4-6-12.4 6-12.4 6a.8.8 0 0 1-1-1c0-4 4.6-11.6 13.4-11.6z" fill="#303030"/><ellipse cx="26.4" cy="40.3" rx="4" ry="7.7" fill="#303030"/><ellipse cx="50.4" cy="39.9" rx="4" ry="7.7" fill="#303030"/><path d="M14.5 27.2s14.7-2 18.6 10c.2.3 0 .5-.4 0a22.8 22.8 0 0 0-18-6.8v-3.2zm47.5 0s-14.5-2-18.4 10c0 .3 0 .5.4 0a22.8 22.8 0 0 1 18.2-6.8v-3.2z" fill="#303030"/></svg>
|
||||||
|
After Width: | Height: | Size: 658 B |
1
webapp/static/img/svg/emoji/angry_color.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><defs><radialGradient id="a" cx="37.4" cy="38.6" r="37.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ed6c70" stop-opacity=".7"/><stop offset=".3" stop-color="#ed6c70" stop-opacity=".5"/><stop offset=".8" stop-color="#ed6c70" stop-opacity=".1"/><stop offset="1" stop-color="#ed6c70" stop-opacity="0"/></radialGradient><radialGradient id="b" cx="37.4" cy="38.6" r="37.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcea1c" stop-opacity=".2"/><stop offset=".8" stop-color="#fcea1c" stop-opacity=".1"/><stop offset="1" stop-color="#fcea1c" stop-opacity="0"/></radialGradient></defs><circle data-name="<Pfad>" cx="40" cy="40" r="40" fill="#dedc03"/><ellipse data-name="<Pfad>" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#fcea1c"/><ellipse data-name="<Pfad>" cx="37.4" cy="38.6" rx="37.4" ry="37.6" fill="url(#a)"/><ellipse data-name="<Pfad>" cx="37.4" cy="38.6" rx="37.4" ry="37.6" fill="url(#b)"/><ellipse cx="26.4" cy="40.2" rx="4" ry="7.7" fill="#303030"/><ellipse cx="50.4" cy="39.8" rx="4" ry="7.7" fill="#303030"/><path d="M14.5 27S29.2 25.3 33 37c.2.4 0 .6-.4 0a22.8 22.8 0 0 0-18-6.7V27zM62 27s-14.5-1.8-18.4 10c0 .4 0 .6.4 0a22.8 22.8 0 0 1 18.2-6.7V27zM39.8 55.5c8.8 0 13.4 7.7 13.4 11.6a.8.8 0 0 1-1 1s-5-6-12.4-6-12.4 6-12.4 6a.8.8 0 0 1-1-1c0-3.8 4.6-11.5 13.4-11.5z" fill="#303030"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
1
webapp/static/img/svg/emoji/cry.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="<Pfad>" cx="40" cy="40" r="40" fill="#cbc9c9"/><ellipse data-name="<Pfad>" cx="38.5" cy="38.1" rx="37.4" ry="37.6" fill="#d7d7d7"/><path d="M15.8 24.5v3.2s10 1.3 12-11c0 0-6 8.8-12 7.8zm48.4 0v3.2s-10 1-12-11c0 0 6 9 12 7.8zm-23.8 27c13 0 18.4 7.7 18.4 11.6a2 2 0 0 1-2 2c-1 0-9-6-16.4-6S25 65 24 65a2 2 0 0 1-2-2c0-3.8 5.4-11.5 18.4-11.5z" fill="#303030"/><path d="M40.5 55a7.5 7.5 0 0 1 7.6 5.7 21 21 0 0 0-7.6-1.7 21.5 21.5 0 0 0-7.5 1.7 7.4 7.4 0 0 1 7.5-5.8z" fill="#ed6b70"/><path d="M35.6 42.2s-4.2-3-11.4-3-11.4 3-11.4 3S14.6 35 24.2 35s11.4 7 11.4 7zm32.4 0s-4-3-11.3-3-11.4 3-11.4 3S47 35 56.7 35 68 42 68 42z" fill="#303030"/><path d="M13.8 42.5v35c0 3.4 6 3.4 6 0v-35c0-3.7-6-3.7-6-.4v.5zm47.2 0v35c0 3.4 6 3.4 6 0v-35c0-3.7-6-3.7-6-.4v.5z" fill="#71caeb"/></svg>
|
||||||
|
After Width: | Height: | Size: 866 B |
1
webapp/static/img/svg/emoji/cry_color.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="<Pfad>" cx="40" cy="40" r="40" fill="#dedc03"/><ellipse data-name="<Pfad>" cx="38.5" cy="38.2" rx="37.4" ry="37.6" fill="#fcea1c"/><path d="M15.8 24.5v3.3s10 1.2 12-11c0 0-6 8.8-12 7.7zm48.4 0v3.3s-10 1-12-11c0 0 6 8.8 12 7.7zm-23.8 27c13 0 18.4 7.7 18.4 11.6a2 2 0 0 1-2 2c-1 0-9-6-16.4-6S25 65 24 65a2 2 0 0 1-2-2c0-3.8 5.4-11.5 18.4-11.5z" fill="#303030"/><path d="M40.5 55a7.5 7.5 0 0 1 7.6 5.8 21 21 0 0 0-7.6-1.7A21.5 21.5 0 0 0 33 61a7.4 7.4 0 0 1 7.5-5.8z" fill="#ed6c70"/><path d="M35.6 42.2s-4.2-3-11.4-3-11.4 3-11.4 3S14.6 35 24.2 35s11.4 7.2 11.4 7.2zm32.4 0s-4-3-11.3-3-11.4 3-11.4 3S47 35 56.7 35 68 42.2 68 42.2z" fill="#303030"/><path d="M13.8 42.5v35c0 3.4 6 3.4 6 0v-35c0-3.6-6-3.6-6-.3v.3zm47.2 0v35c0 3.4 6 3.4 6 0v-35c0-3.6-6-3.6-6-.3v.3z" fill="#71caeb"/></svg>
|
||||||
|
After Width: | Height: | Size: 874 B |
1
webapp/static/img/svg/emoji/funny.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="<Pfad>" cx="40.1" cy="40" r="40" fill="#cbc9c9"/><ellipse data-name="<Pfad>" cx="38.4" cy="38.6" rx="37.4" ry="37.6" fill="#d7d7d7"/><path d="M17.6 50h45.6s-3.7 19.6-22.8 19.6S17.6 50 17.6 50z" fill="#303030"/><path d="M40.4 59.8c8 0 9.8 7.8 9.8 7.8a22.6 22.6 0 0 1-9.8 2 22.7 22.7 0 0 1-9.7-2s1.5-7.8 9.7-7.8z" fill="#ed6b70"/><path d="M14.2 33.3s15-2.6 21.5 7.4A83.5 83.5 0 0 0 14.2 42c-10.3 2 4.6-4 14-3 0 0-.8-4-14-5.7zm51.6 0s-15-2.6-21.5 7.4A83.5 83.5 0 0 1 65.8 42c10.3 2-4.6-4-14-3 0 0 1-4 14-5.7z" fill="#303030"/></svg>
|
||||||
|
After Width: | Height: | Size: 620 B |
1
webapp/static/img/svg/emoji/funny_color.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="<Pfad>" cx="40.1" cy="40" r="40" fill="#dedc03"/><ellipse data-name="<Pfad>" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#fcea1c"/><path d="M17.6 50h45.6s-3.7 19.7-22.8 19.7S17.6 50 17.6 50z" fill="#303030"/><path d="M40.4 60c8 0 9.8 7.6 9.8 7.6a22.6 22.6 0 0 1-9.8 2 22.7 22.7 0 0 1-9.7-2s1.5-7.7 9.7-7.7z" fill="#ed6b70"/><path d="M14.2 33.4s15-2.6 21.5 7.3A83.5 83.5 0 0 0 14.2 42c-10.3 2 4.6-4 14-3 0 0-.8-4-14-5.6zm51.6 0s-15-2.6-21.5 7.3A83.5 83.5 0 0 1 65.8 42c10.3 2-4.6-4-14-3 0 0 1-4 14-5.6z" fill="#303030"/></svg>
|
||||||
|
After Width: | Height: | Size: 618 B |
1
webapp/static/img/svg/emoji/happy.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="<Pfad>" cx="40" cy="40" r="40" fill="#cbc9c9"/><ellipse data-name="<Pfad>" cx="38.4" cy="38.6" rx="37.4" ry="37.6" fill="#d7d7d7"/><ellipse cx="55.8" cy="39" rx="4" ry="7.7" fill="#303030"/><ellipse cx="24.1" cy="39" rx="4" ry="7.7" fill="#303030"/><ellipse cx="24.1" cy="45.9" rx="5.7" ry=".8" fill="#303030"/><ellipse cx="55.8" cy="45.9" rx="5.7" ry=".8" fill="#303030"/><path d="M17.5 55s8.3 4 22.8 4S63 55 63 55s-3.6 14.6-22.7 14.6S17.5 55 17.5 55z" fill="#303030"/><path d="M40.3 62.8c8 0 9.4 5.4 9.2 5.5a28.8 28.8 0 0 1-9.2 1.3 28.8 28.8 0 0 1-9.2-1.3s1-5.5 9.3-5.5z" fill="#ed6b70"/></svg>
|
||||||
|
After Width: | Height: | Size: 687 B |
1
webapp/static/img/svg/emoji/happy_color.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="<Pfad>" cx="40" cy="40" r="40" fill="#dedc03"/><ellipse data-name="<Pfad>" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#fcea1c"/><ellipse cx="55.8" cy="39" rx="4" ry="7.7" fill="#303030"/><ellipse cx="24.1" cy="39" rx="4" ry="7.7" fill="#303030"/><ellipse cx="24.1" cy="45.9" rx="5.7" ry=".8" fill="#303030"/><ellipse cx="55.8" cy="45.9" rx="5.7" ry=".8" fill="#303030"/><path d="M17.5 55s8.3 4 22.8 4S63 55 63 55s-3.6 14.7-22.7 14.7S17.5 55 17.5 55z" fill="#303030"/><path d="M40.3 63c8 0 9.4 5.3 9.2 5.3a28.8 28.8 0 0 1-9.2 1.4 28.8 28.8 0 0 1-9.2-1.4s1-5.4 9.3-5.4z" fill="#ed6b70"/></svg>
|
||||||
|
After Width: | Height: | Size: 685 B |
1
webapp/static/img/svg/emoji/surprised.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="new" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><style></style><circle id="XMLID_40_" cx="40" cy="40" r="40" fill="#cac9c9"/><ellipse id="XMLID_39_" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#d7d8d8"/><ellipse id="Oval" fill="#303030" cx="55.8" cy="39" rx="4" ry="7.7"/><ellipse id="Oval" fill="#303030" cx="24.1" cy="39" rx="4" ry="7.7"/><path d="M45 61.4c0-2.3-1.2-4.4-6-4.4-5 0-6 2.2-6 4.4 0 2.3 1 6.6 6 6.6 4.8 0 6-4.4 6-6.6zM51.1 19L50 16s8.9-4.6 15 6c0 0-8.6-6.1-13.9-3zM26.4 18.4l.6-3.1S17 12.6 13 24c0 0 7.5-7.6 13.4-5.6z" id="Path" fill="#303030"/></svg>
|
||||||
|
After Width: | Height: | Size: 588 B |
1
webapp/static/img/svg/emoji/surprised_color.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="new" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><style></style><circle id="XMLID_40_" cx="40" cy="40" r="40" fill="#dedc03"/><ellipse id="XMLID_39_" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#fcea1c"/><ellipse id="Oval" fill="#303030" cx="55.8" cy="39" rx="4" ry="7.7"/><ellipse id="Oval" fill="#303030" cx="24.1" cy="39" rx="4" ry="7.7"/><path d="M45 61.4c0-2.3-1.2-4.4-6-4.4-5 0-6 2.2-6 4.4 0 2.3 1 6.6 6 6.6 4.8 0 6-4.4 6-6.6zM51.1 19L50 16s8.9-4.6 15 6c0 0-8.6-6.1-13.9-3zM26.4 18.4l.6-3.1S17 12.6 13 24c0 0 7.5-7.6 13.4-5.6z" id="Path" fill="#303030"/></svg>
|
||||||
|
After Width: | Height: | Size: 588 B |