mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
feat(backend): notify posts in groups (#8346)
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
This commit is contained in:
parent
7b4b0774e4
commit
aedf8d93c7
@ -1,7 +1,7 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { cleanDatabase } from '@db/factories'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import { createGroupMutation } from '@graphql/groups'
|
||||
import CONFIG from '@src/config'
|
||||
@ -9,6 +9,11 @@ import createServer from '@src/server'
|
||||
|
||||
CONFIG.CATEGORIES_ACTIVE = false
|
||||
|
||||
const sendMailMock = jest.fn()
|
||||
jest.mock('../helpers/email/sendMail', () => ({
|
||||
sendMail: () => sendMailMock(),
|
||||
}))
|
||||
|
||||
let server, query, mutate, authenticatedUser
|
||||
|
||||
let postAuthor, firstFollower, secondFollower
|
||||
@ -89,8 +94,8 @@ afterAll(async () => {
|
||||
|
||||
describe('following users notifications', () => {
|
||||
beforeAll(async () => {
|
||||
postAuthor = await neode.create(
|
||||
'User',
|
||||
postAuthor = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'post-author',
|
||||
name: 'Post Author',
|
||||
@ -101,8 +106,8 @@ describe('following users notifications', () => {
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
firstFollower = await neode.create(
|
||||
'User',
|
||||
firstFollower = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'first-follower',
|
||||
name: 'First Follower',
|
||||
@ -113,8 +118,8 @@ describe('following users notifications', () => {
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
secondFollower = await neode.create(
|
||||
'User',
|
||||
secondFollower = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'second-follower',
|
||||
name: 'Second Follower',
|
||||
@ -136,6 +141,7 @@ describe('following users notifications', () => {
|
||||
mutation: followUserMutation,
|
||||
variables: { id: 'post-author' },
|
||||
})
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('the followed user writes a post', () => {
|
||||
@ -209,6 +215,10 @@ describe('following users notifications', () => {
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('sends only one email, as second follower has emails disabled', () => {
|
||||
expect(sendMailMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('followed user posts in public group', () => {
|
||||
@ -248,7 +258,7 @@ describe('following users notifications', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('sends notification to the first follower although he is no member of the group', async () => {
|
||||
it('sends a notification to the first follower', async () => {
|
||||
authenticatedUser = await firstFollower.toJson()
|
||||
await expect(
|
||||
query({
|
||||
|
||||
@ -125,6 +125,11 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo
|
||||
[notifyFollowingUsers(post.id, groupId, context)],
|
||||
'emailNotificationsFollowingUsers',
|
||||
)
|
||||
await publishNotifications(
|
||||
context,
|
||||
[notifyGroupMembersOfNewPost(post.id, groupId, context)],
|
||||
'emailNotificationsPostInGroup',
|
||||
)
|
||||
}
|
||||
return post
|
||||
}
|
||||
@ -216,6 +221,49 @@ const notifyFollowingUsers = async (postId, groupId, context) => {
|
||||
}
|
||||
}
|
||||
|
||||
const notifyGroupMembersOfNewPost = async (postId, groupId, context) => {
|
||||
if (!groupId) return []
|
||||
const reason = 'post_in_group'
|
||||
const cypher = `
|
||||
MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId })
|
||||
MATCH (post)-[:IN]->(group:Group { id: $groupId })<-[membership:MEMBER_OF]-(user:User)
|
||||
WHERE NOT membership.role = 'pending'
|
||||
AND NOT (user)-[:MUTED]->(group)
|
||||
AND NOT user.id = $userId
|
||||
WITH post, author, user
|
||||
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
WITH notification, author, user,
|
||||
post {.*, author: properties(author) } AS finalResource
|
||||
RETURN notification {
|
||||
.*,
|
||||
from: finalResource,
|
||||
to: properties(user),
|
||||
relatedUser: properties(author)
|
||||
}
|
||||
`
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const notificationTransactionResponse = await transaction.run(cypher, {
|
||||
postId,
|
||||
reason,
|
||||
groupId,
|
||||
userId: context.user.id,
|
||||
})
|
||||
return notificationTransactionResponse.records.map((record) => record.get('notification'))
|
||||
})
|
||||
try {
|
||||
const notifications = await writeTxResultPromise
|
||||
return notifications
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
|
||||
const cypher = `
|
||||
MATCH (user:User { id: $userId })
|
||||
|
||||
395
backend/src/middleware/notifications/posts-in-groups.spec.ts
Normal file
395
backend/src/middleware/notifications/posts-in-groups.spec.ts
Normal file
@ -0,0 +1,395 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import {
|
||||
createGroupMutation,
|
||||
joinGroupMutation,
|
||||
changeGroupMemberRoleMutation,
|
||||
} from '@graphql/groups'
|
||||
import CONFIG from '@src/config'
|
||||
import createServer from '@src/server'
|
||||
|
||||
CONFIG.CATEGORIES_ACTIVE = false
|
||||
|
||||
const sendMailMock = jest.fn()
|
||||
jest.mock('../helpers/email/sendMail', () => ({
|
||||
sendMail: () => sendMailMock(),
|
||||
}))
|
||||
|
||||
let server, query, mutate, authenticatedUser
|
||||
|
||||
let postAuthor, groupMember, pendingMember
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
const createPostMutation = gql`
|
||||
mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) {
|
||||
CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const notificationQuery = gql`
|
||||
query ($read: Boolean) {
|
||||
notifications(read: $read, orderBy: updatedAt_desc) {
|
||||
read
|
||||
reason
|
||||
createdAt
|
||||
relatedUser {
|
||||
id
|
||||
}
|
||||
from {
|
||||
__typename
|
||||
... on Post {
|
||||
id
|
||||
content
|
||||
}
|
||||
... on Comment {
|
||||
id
|
||||
content
|
||||
}
|
||||
... on Group {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const muteGroupMutation = gql`
|
||||
mutation ($id: ID!) {
|
||||
muteGroup(id: $id) {
|
||||
id
|
||||
isMutedByMe
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const unmuteGroupMutation = gql`
|
||||
mutation ($id: ID!) {
|
||||
unmuteGroup(id: $id) {
|
||||
id
|
||||
isMutedByMe
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const markAllAsRead = async () =>
|
||||
mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
markAllAsRead {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const createServerResult = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode,
|
||||
driver,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
server = createServerResult.server
|
||||
const createTestClientResult = createTestClient(server)
|
||||
query = createTestClientResult.query
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
driver.close()
|
||||
})
|
||||
|
||||
describe('notify group members of new posts in group', () => {
|
||||
beforeAll(async () => {
|
||||
postAuthor = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'post-author',
|
||||
name: 'Post Author',
|
||||
slug: 'post-author',
|
||||
},
|
||||
{
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
groupMember = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'group-member',
|
||||
name: 'Group Member',
|
||||
slug: 'group-member',
|
||||
},
|
||||
{
|
||||
email: 'test2@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
pendingMember = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'pending-member',
|
||||
name: 'Pending Member',
|
||||
slug: 'pending-member',
|
||||
},
|
||||
{
|
||||
email: 'test3@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
id: 'g-1',
|
||||
name: 'A closed group',
|
||||
description: 'A closed group to test the notifications to group members',
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g-1',
|
||||
userId: 'group-member',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await pendingMember.toJson()
|
||||
await mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g-1',
|
||||
userId: 'pending-member',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g-1',
|
||||
userId: 'group-member',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('group owner posts in group', () => {
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await markAllAsRead()
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await markAllAsRead()
|
||||
await mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id: 'post',
|
||||
title: 'This is the new post in the group',
|
||||
content: 'This is the content of the new post in the group',
|
||||
groupId: 'g-1',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('sends NO notification to the author of the post', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
notifications: [],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('sends NO notification to the pending group member', async () => {
|
||||
authenticatedUser = await pendingMember.toJson()
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
notifications: [],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('sends notification to the group member', async () => {
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
notifications: [
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
id: 'post',
|
||||
},
|
||||
read: false,
|
||||
reason: 'post_in_group',
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('sends one email', () => {
|
||||
expect(sendMailMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('group member mutes group', () => {
|
||||
it('sets the muted status correctly', async () => {
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: muteGroupMutation,
|
||||
variables: {
|
||||
id: 'g-1',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
muteGroup: {
|
||||
isMutedByMe: true,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('sends NO notification when another post is posted', async () => {
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await markAllAsRead()
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id: 'post-1',
|
||||
title: 'This is another post in the group',
|
||||
content: 'This is the content of another post in the group',
|
||||
groupId: 'g-1',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
notifications: [],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
describe('group member unmutes group again but disables email', () => {
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
await groupMember.update({ emailNotificationsPostInGroup: false })
|
||||
})
|
||||
|
||||
it('sets the muted status correctly', async () => {
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: unmuteGroupMutation,
|
||||
variables: {
|
||||
id: 'g-1',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
unmuteGroup: {
|
||||
isMutedByMe: false,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('sends notification when another post is posted', async () => {
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await markAllAsRead()
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id: 'post-2',
|
||||
title: 'This is yet another post in the group',
|
||||
content: 'This is the content of yet another post in the group',
|
||||
groupId: 'g-1',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await groupMember.toJson()
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
notifications: [
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
id: 'post-2',
|
||||
},
|
||||
read: false,
|
||||
reason: 'post_in_group',
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('sends NO email', () => {
|
||||
expect(sendMailMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -468,6 +468,8 @@ export default shield(
|
||||
CreateMessage: isAuthenticated,
|
||||
MarkMessagesAsSeen: isAuthenticated,
|
||||
toggleObservePost: isAuthenticated,
|
||||
muteGroup: and(isAuthenticated, isMemberOfGroup),
|
||||
unmuteGroup: and(isAuthenticated, isMemberOfGroup),
|
||||
},
|
||||
User: {
|
||||
email: or(isMyOwn, isAdmin),
|
||||
|
||||
@ -106,6 +106,7 @@ export const validateNotifyUsers = async (label, reason) => {
|
||||
'mentioned_in_comment',
|
||||
'commented_on_post',
|
||||
'followed_user_posted',
|
||||
'post_in_group',
|
||||
]
|
||||
if (!reasonsAllowed.includes(reason)) throw new Error('Notification reason is not allowed!')
|
||||
if (
|
||||
|
||||
@ -189,6 +189,10 @@ export default {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
emailNotificationsPostInGroup: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
|
||||
locale: {
|
||||
type: 'string',
|
||||
|
||||
@ -368,6 +368,64 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
muteGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { id: groupId } = params
|
||||
const userId = context.user.id
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (group:Group { id: $groupId })
|
||||
MATCH (user:User { id: $userId })
|
||||
MERGE (user)-[m:MUTED]->(group)
|
||||
SET m.createdAt = toString(datetime())
|
||||
RETURN group { .* }
|
||||
`,
|
||||
{
|
||||
groupId,
|
||||
userId,
|
||||
},
|
||||
)
|
||||
const [group] = await transactionResponse.records.map((record) => record.get('group'))
|
||||
return group
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
unmuteGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { id: groupId } = params
|
||||
const userId = context.user.id
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (group:Group { id: $groupId })
|
||||
MATCH (user:User { id: $userId })
|
||||
OPTIONAL MATCH (user)-[m:MUTED]->(group)
|
||||
DELETE m
|
||||
RETURN group { .* }
|
||||
`,
|
||||
{
|
||||
groupId,
|
||||
userId,
|
||||
},
|
||||
)
|
||||
const [group] = await transactionResponse.records.map((record) => record.get('group'))
|
||||
return group
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
...Resolver('Group', {
|
||||
@ -380,6 +438,10 @@ export default {
|
||||
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
|
||||
location: '-[:IS_IN]->(related:Location)',
|
||||
},
|
||||
boolean: {
|
||||
isMutedByMe:
|
||||
'MATCH (this)<-[:MUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@ -679,6 +679,10 @@ describe('emailNotificationSettings', () => {
|
||||
name: 'followingUsers',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'postInGroup',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -773,6 +777,10 @@ describe('emailNotificationSettings', () => {
|
||||
name: 'followingUsers',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'postInGroup',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -387,6 +387,10 @@ export default {
|
||||
name: 'followingUsers',
|
||||
value: parent.emailNotificationsFollowingUsers ?? true,
|
||||
},
|
||||
{
|
||||
name: 'postInGroup',
|
||||
value: parent.emailNotificationsPostInGroup ?? true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -2,6 +2,7 @@ enum EmailNotificationSettingsName {
|
||||
commentOnObservedPost
|
||||
mention
|
||||
followingUsers
|
||||
postInGroup
|
||||
chatMessage
|
||||
groupMemberJoined
|
||||
groupMemberLeft
|
||||
|
||||
@ -41,6 +41,10 @@ type Group {
|
||||
myRole: GroupMemberRole # if 'null' then the current user is no member
|
||||
|
||||
posts: [Post] @relation(name: "IN", direction: "IN")
|
||||
|
||||
isMutedByMe: Boolean!
|
||||
@cypher(
|
||||
statement: "MATCH (this)<-[m:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(m) >= 1")
|
||||
}
|
||||
|
||||
|
||||
@ -137,4 +141,7 @@ type Mutation {
|
||||
groupId: ID!
|
||||
userId: ID!
|
||||
): User
|
||||
|
||||
muteGroup(id: ID!): Group
|
||||
unmuteGroup(id: ID!): Group
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ enum NotificationReason {
|
||||
changed_group_member_role
|
||||
removed_user_from_group
|
||||
followed_user_posted
|
||||
post_in_group
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": "Hat einen neuen Betrag geschrieben …",
|
||||
"mentioned_in_comment": "Hat Dich in einem Kommentar erwähnt …",
|
||||
"mentioned_in_post": "Hat Dich in einem Beitrag erwähnt …",
|
||||
"post_in_group": "Hat einen Beitrag in der Gruppe geschrieben …",
|
||||
"removed_user_from_group": "Hat Dich aus der Gruppe entfernt …",
|
||||
"user_joined_group": "Ist Deiner Gruppe beigetreten …",
|
||||
"user_left_group": "Hat deine Gruppe verlassen …"
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": "Wrote a new post …",
|
||||
"mentioned_in_comment": "Mentioned you in a comment …",
|
||||
"mentioned_in_post": "Mentioned you in a post …",
|
||||
"post_in_group": "Posted in a group …",
|
||||
"removed_user_from_group": "Removed you from group …",
|
||||
"user_joined_group": "Joined your group …",
|
||||
"user_left_group": "Left your group …"
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": null,
|
||||
"mentioned_in_comment": "Le mencionó en un comentario …",
|
||||
"mentioned_in_post": "Le mencionó en una contribución …",
|
||||
"post_in_group": null,
|
||||
"removed_user_from_group": null,
|
||||
"user_joined_group": null,
|
||||
"user_left_group": null
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": null,
|
||||
"mentioned_in_comment": "Vous a mentionné dans un commentaire…",
|
||||
"mentioned_in_post": "Vous a mentionné dans un post…",
|
||||
"post_in_group": null,
|
||||
"removed_user_from_group": null,
|
||||
"user_joined_group": null,
|
||||
"user_left_group": null
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": null,
|
||||
"mentioned_in_comment": null,
|
||||
"mentioned_in_post": null,
|
||||
"post_in_group": null,
|
||||
"removed_user_from_group": null,
|
||||
"user_joined_group": null,
|
||||
"user_left_group": null
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": null,
|
||||
"mentioned_in_comment": null,
|
||||
"mentioned_in_post": null,
|
||||
"post_in_group": null,
|
||||
"removed_user_from_group": null,
|
||||
"user_joined_group": null,
|
||||
"user_left_group": null
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": null,
|
||||
"mentioned_in_comment": null,
|
||||
"mentioned_in_post": null,
|
||||
"post_in_group": null,
|
||||
"removed_user_from_group": null,
|
||||
"user_joined_group": null,
|
||||
"user_left_group": null
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": null,
|
||||
"mentioned_in_comment": "Mentionou você em um comentário …",
|
||||
"mentioned_in_post": "Mencinou você em um post …",
|
||||
"post_in_group": null,
|
||||
"removed_user_from_group": null,
|
||||
"user_joined_group": null,
|
||||
"user_left_group": null
|
||||
|
||||
@ -741,6 +741,7 @@
|
||||
"followed_user_posted": null,
|
||||
"mentioned_in_comment": "Упоминание в комментарии....",
|
||||
"mentioned_in_post": "Упоминание в посте....",
|
||||
"post_in_group": null,
|
||||
"removed_user_from_group": null,
|
||||
"user_joined_group": null,
|
||||
"user_left_group": null
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user