Merge branch '5059-epic-groups' into 5140-My-Groups-Page

This commit is contained in:
ogerly 2022-09-09 10:14:46 +02:00
commit a9a2097557
11 changed files with 514 additions and 123 deletions

View File

@ -323,6 +323,7 @@ export default shield(
GenerateInviteCode: isAuthenticated, GenerateInviteCode: isAuthenticated,
switchUserRole: isAdmin, switchUserRole: isAdmin,
markTeaserAsViewed: allow, markTeaserAsViewed: allow,
saveCategorySettings: isAuthenticated,
}, },
User: { User: {
email: or(isMyOwn, isAdmin), email: or(isMyOwn, isAdmin),

View File

@ -18,19 +18,25 @@ export default {
if (isMember === true) { if (isMember === true) {
groupCypher = ` groupCypher = `
MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupIdCypher}) MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupIdCypher})
WITH group, membership
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
RETURN group {.*, myRole: membership.role} RETURN group {.*, myRole: membership.role}
` `
} else { } else {
if (isMember === false) { if (isMember === false) {
groupCypher = ` groupCypher = `
MATCH (group:Group${groupIdCypher}) MATCH (group:Group${groupIdCypher})
WHERE NOT (:User {id: $userId})-[:MEMBER_OF]->(group) WHERE (NOT (:User {id: $userId})-[:MEMBER_OF]->(group))
WITH group
WHERE group.groupType IN ['public', 'closed']
RETURN group {.*, myRole: NULL} RETURN group {.*, myRole: NULL}
` `
} else { } else {
groupCypher = ` groupCypher = `
MATCH (group:Group${groupIdCypher}) MATCH (group:Group${groupIdCypher})
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
WITH group, membership
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
RETURN group {.*, myRole: membership.role} RETURN group {.*, myRole: membership.role}
` `
} }

View File

@ -267,6 +267,7 @@ describe('in mode', () => {
describe('authenticated', () => { describe('authenticated', () => {
let otherUser let otherUser
let ownerOfHiddenGroupUser
beforeAll(async () => { beforeAll(async () => {
otherUser = await Factory.build( otherUser = await Factory.build(
@ -276,7 +277,18 @@ describe('in mode', () => {
name: 'Other TestUser', name: 'Other TestUser',
}, },
{ {
email: 'test2@example.org', email: 'other-user@example.org',
password: '1234',
},
)
ownerOfHiddenGroupUser = await Factory.build(
'user',
{
id: 'owner-of-hidden-group',
name: 'Owner Of Hidden Group',
},
{
email: 'owner-of-hidden-group@example.org',
password: '1234', password: '1234',
}, },
) )
@ -293,6 +305,59 @@ describe('in mode', () => {
categoryIds, categoryIds,
}, },
}) })
authenticatedUser = await ownerOfHiddenGroupUser.toJson()
await mutate({
mutation: createGroupMutation,
variables: {
id: 'hidden-group',
name: 'Investigative Journalism Group',
about: 'We will change all.',
description: 'We research …' + descriptionAdditional100,
groupType: 'hidden',
actionRadius: 'global',
categoryIds,
},
})
await mutate({
mutation: createGroupMutation,
variables: {
id: 'second-hidden-group',
name: 'Second Investigative Journalism Group',
about: 'We will change all.',
description: 'We research …' + descriptionAdditional100,
groupType: 'hidden',
actionRadius: 'global',
categoryIds,
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'second-hidden-group',
userId: 'current-user',
roleInGroup: 'pending',
},
})
await mutate({
mutation: createGroupMutation,
variables: {
id: 'third-hidden-group',
name: 'Third Investigative Journalism Group',
about: 'We will change all.',
description: 'We research …' + descriptionAdditional100,
groupType: 'hidden',
actionRadius: 'global',
categoryIds,
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'third-hidden-group',
userId: 'current-user',
roleInGroup: 'usual',
},
})
authenticatedUser = await user.toJson() authenticatedUser = await user.toJson()
await mutate({ await mutate({
mutation: createGroupMutation, mutation: createGroupMutation,
@ -309,9 +374,11 @@ describe('in mode', () => {
}) })
describe('query groups', () => { describe('query groups', () => {
describe('in general finds only listed groups no hidden groups where user is none or pending member', () => {
describe('without any filters', () => { describe('without any filters', () => {
it('finds all groups', async () => { it('finds all listed groups', async () => {
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({ const result = await query({ query: groupQuery, variables: {} })
expect(result).toMatchObject({
data: { data: {
Group: expect.arrayContaining([ Group: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@ -324,10 +391,16 @@ describe('in mode', () => {
slug: 'uninteresting-group', slug: 'uninteresting-group',
myRole: null, myRole: null,
}), }),
expect.objectContaining({
id: 'third-hidden-group',
slug: 'third-investigative-journalism-group',
myRole: 'usual',
}),
]), ]),
}, },
errors: undefined, errors: undefined,
}) })
expect(result.data.Group.length).toBe(3)
}) })
describe('categories', () => { describe('categories', () => {
@ -367,11 +440,11 @@ describe('in mode', () => {
}) })
}) })
describe('with given id', () => {
describe("id = 'my-group'", () => { describe("id = 'my-group'", () => {
it('finds only the group with this id', async () => { it('finds only the listed group with this id', async () => {
await expect( const result = await query({ query: groupQuery, variables: { id: 'my-group' } })
query({ query: groupQuery, variables: { id: 'my-group' } }), expect(result).toMatchObject({
).resolves.toMatchObject({
data: { data: {
Group: [ Group: [
expect.objectContaining({ expect.objectContaining({
@ -383,33 +456,81 @@ describe('in mode', () => {
}, },
errors: undefined, errors: undefined,
}) })
expect(result.data.Group.length).toBe(1)
})
})
describe("id = 'third-hidden-group'", () => {
it("finds only the hidden group where I'm 'usual' member", async () => {
const result = await query({
query: groupQuery,
variables: { id: 'third-hidden-group' },
})
expect(result).toMatchObject({
data: {
Group: expect.arrayContaining([
expect.objectContaining({
id: 'third-hidden-group',
slug: 'third-investigative-journalism-group',
myRole: 'usual',
}),
]),
},
errors: undefined,
})
expect(result.data.Group.length).toBe(1)
})
})
describe("id = 'second-hidden-group'", () => {
it("finds no hidden group where I'm 'pending' member", async () => {
const result = await query({
query: groupQuery,
variables: { id: 'second-hidden-group' },
})
expect(result.data.Group.length).toBe(0)
})
})
describe("id = 'hidden-group'", () => {
it("finds no hidden group where I'm not(!) a member at all", async () => {
const result = await query({
query: groupQuery,
variables: { id: 'hidden-group' },
})
expect(result.data.Group.length).toBe(0)
})
}) })
}) })
describe('isMember = true', () => { describe('isMember = true', () => {
it('finds only groups where user is member', async () => { it('finds only listed groups where user is member', async () => {
await expect( const result = await query({ query: groupQuery, variables: { isMember: true } })
query({ query: groupQuery, variables: { isMember: true } }), expect(result).toMatchObject({
).resolves.toMatchObject({
data: { data: {
Group: [ Group: expect.arrayContaining([
{ expect.objectContaining({
id: 'my-group', id: 'my-group',
slug: 'the-best-group', slug: 'the-best-group',
myRole: 'owner', myRole: 'owner',
}, }),
], expect.objectContaining({
id: 'third-hidden-group',
slug: 'third-investigative-journalism-group',
myRole: 'usual',
}),
]),
}, },
errors: undefined, errors: undefined,
}) })
expect(result.data.Group.length).toBe(2)
}) })
}) })
describe('isMember = false', () => { describe('isMember = false', () => {
it('finds only groups where user is not(!) member', async () => { it('finds only listed groups where user is not(!) member', async () => {
await expect( const result = await query({ query: groupQuery, variables: { isMember: false } })
query({ query: groupQuery, variables: { isMember: false } }), expect(result).toMatchObject({
).resolves.toMatchObject({
data: { data: {
Group: expect.arrayContaining([ Group: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@ -421,6 +542,8 @@ describe('in mode', () => {
}, },
errors: undefined, errors: undefined,
}) })
expect(result.data.Group.length).toBe(1)
})
}) })
}) })
}) })

View File

@ -269,6 +269,45 @@ export default {
session.close() session.close()
} }
}, },
saveCategorySettings: async (object, args, context, resolveInfo) => {
const { activeCategories } = args
const {
user: { id },
} = context
const session = context.driver.session()
await session.writeTransaction((transaction) => {
return transaction.run(
`
MATCH (user:User { id: $id })-[previousCategories:NOT_INTERESTED_IN]->(category:Category)
DELETE previousCategories
RETURN user, category
`,
{ id },
)
})
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const saveCategorySettingsResponse = await transaction.run(
`
MATCH (category:Category) WHERE NOT category.id IN $activeCategories
MATCH (user:User { id: $id })
MERGE (user)-[r:NOT_INTERESTED_IN]->(category)
RETURN user, r, category
`,
{ id, activeCategories },
)
const [user] = await saveCategorySettingsResponse.records.map((record) =>
record.get('user'),
)
return user
})
try {
await writeTxResultPromise
return true
} finally {
session.close()
}
},
}, },
User: { User: {
email: async (parent, params, context, resolveInfo) => { email: async (parent, params, context, resolveInfo) => {

View File

@ -3,6 +3,7 @@ import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import { categories } from '../../constants/categories'
const categoryIds = ['cat9'] const categoryIds = ['cat9']
let user let user
@ -56,6 +57,12 @@ const switchUserRoleMutation = gql`
} }
` `
const saveCategorySettings = gql`
mutation ($activeCategories: [String]) {
saveCategorySettings(activeCategories: $activeCategories)
}
`
beforeAll(async () => { beforeAll(async () => {
await cleanDatabase() await cleanDatabase()
@ -544,3 +551,146 @@ describe('switch user role', () => {
}) })
}) })
}) })
describe('save category settings', () => {
beforeEach(async () => {
await Promise.all(
categories.map(({ icon, name }, index) => {
Factory.build('category', {
id: `cat${index + 1}`,
slug: name,
name,
icon,
})
}),
)
})
beforeEach(async () => {
user = await Factory.build('user', {
id: 'user',
role: 'user',
})
variables = {
activeCategories: ['cat1', 'cat3', 'cat5'],
}
})
describe('not authenticated', () => {
beforeEach(async () => {
authenticatedUser = undefined
})
it('throws an error', async () => {
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
expect.objectContaining({
errors: [
expect.objectContaining({
message: 'Not Authorized!',
}),
],
}),
)
})
})
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
const userQuery = gql`
query ($id: ID) {
User(id: $id) {
activeCategories {
id
}
}
}
`
describe('no categories saved', () => {
it('returns true for active categories mutation', async () => {
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
expect.objectContaining({
data: { saveCategorySettings: true },
}),
)
})
describe('query for user', () => {
beforeEach(async () => {
await mutate({ mutation: saveCategorySettings, variables })
})
it('returns the active categories when user is queried', async () => {
await expect(
query({ query: userQuery, variables: { id: authenticatedUser.id } }),
).resolves.toEqual(
expect.objectContaining({
data: {
User: [
{
activeCategories: expect.arrayContaining([
{ id: 'cat1' },
{ id: 'cat3' },
{ id: 'cat5' },
]),
},
],
},
}),
)
})
})
})
describe('categories already saved', () => {
beforeEach(async () => {
variables = {
activeCategories: ['cat1', 'cat3', 'cat5'],
}
await mutate({ mutation: saveCategorySettings, variables })
variables = {
activeCategories: ['cat10', 'cat11', 'cat12', 'cat8', 'cat9'],
}
})
it('returns true', async () => {
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
expect.objectContaining({
data: { saveCategorySettings: true },
}),
)
})
describe('query for user', () => {
beforeEach(async () => {
await mutate({ mutation: saveCategorySettings, variables })
})
it('returns the new active categories when user is queried', async () => {
await expect(
query({ query: userQuery, variables: { id: authenticatedUser.id } }),
).resolves.toEqual(
expect.objectContaining({
data: {
User: [
{
activeCategories: expect.arrayContaining([
{ id: 'cat10' },
{ id: 'cat11' },
{ id: 'cat12' },
{ id: 'cat8' },
{ id: 'cat9' },
]),
},
],
},
}),
)
})
})
})
})
})

View File

@ -115,6 +115,14 @@ type User {
emotions: [EMOTED] emotions: [EMOTED]
activeCategories: [Category] @cypher(
statement: """
MATCH (category:Category)
WHERE NOT ((this)-[:NOT_INTERESTED_IN]->(category))
RETURN category
"""
)
myRoleInGroup: GroupMemberRole myRoleInGroup: GroupMemberRole
} }
@ -222,4 +230,6 @@ type Mutation {
unblockUser(id: ID!): User unblockUser(id: ID!): User
switchUserRole(role: UserRole!, id: ID!): User switchUserRole(role: UserRole!, id: ID!): User
saveCategorySettings(activeCategories: [String]): Boolean
} }

View File

@ -0,0 +1,58 @@
<template>
<dropdown ref="category-menu" placement="top-start" :offset="8" class="category-menu">
<base-button
slot="default"
:filled="filterActive"
:ghost="!filterActive"
slot-scope="{ toggleMenu }"
@click.prevent="toggleMenu()"
>
<ds-text uppercase>{{ $t('admin.categories.name') }}</ds-text>
</base-button>
<template slot="popover">
<div class="category-menu-options">
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
<categories-filter v-if="categoriesActive" />
</div>
</template>
</dropdown>
</template>
<script>
import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex'
import CategoriesFilter from './CategoriesFilter'
export default {
name: 'CategoriesMenu',
components: {
Dropdown,
CategoriesFilter,
},
props: {
placement: { type: String },
offset: { type: [String, Number] },
},
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
computed: {
...mapGetters({
filterActive: 'posts/isActive',
}),
},
}
</script>
<style lang="scss">
.category-menu-options {
max-width: $size-max-width-filter-menu;
padding: $space-small $space-x-small;
> .title {
font-size: $font-size-large;
}
}
</style>

View File

@ -14,7 +14,6 @@
<div class="filter-menu-options"> <div class="filter-menu-options">
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2> <h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
<following-filter /> <following-filter />
<categories-filter v-if="categoriesActive" />
</div> </div>
<div class="filter-menu-options"> <div class="filter-menu-options">
<h2 class="title">{{ $t('filter-menu.order-by') }}</h2> <h2 class="title">{{ $t('filter-menu.order-by') }}</h2>
@ -29,24 +28,17 @@ import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import FollowingFilter from './FollowingFilter' import FollowingFilter from './FollowingFilter'
import OrderByFilter from './OrderByFilter' import OrderByFilter from './OrderByFilter'
import CategoriesFilter from './CategoriesFilter'
export default { export default {
components: { components: {
Dropdown, Dropdown,
FollowingFilter, FollowingFilter,
CategoriesFilter,
OrderByFilter, OrderByFilter,
}, },
props: { props: {
placement: { type: String }, placement: { type: String },
offset: { type: [String, Number] }, offset: { type: [String, Number] },
}, },
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
computed: { computed: {
...mapGetters({ ...mapGetters({
filterActive: 'posts/isActive', filterActive: 'posts/isActive',

View File

@ -15,6 +15,15 @@
> >
<base-button icon="bars" @click="toggleMobileMenuView" circle /> <base-button icon="bars" @click="toggleMobileMenuView" circle />
</ds-flex-item> </ds-flex-item>
<ds-flex-item
v-if="categoriesActive && isLoggedIn"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
style="flex-grow: 0; flex-basis: auto"
>
<client-only>
<categories-menu></categories-menu>
</client-only>
</ds-flex-item>
<ds-flex-item <ds-flex-item
:width="{ base: '45%', sm: '45%', md: '45%', lg: '50%' }" :width="{ base: '45%', sm: '45%', md: '45%', lg: '50%' }"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }" :class="{ 'hide-mobile-menu': !toggleMobileMenu }"
@ -90,6 +99,7 @@ import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import PageFooter from '~/components/PageFooter/PageFooter' import PageFooter from '~/components/PageFooter/PageFooter'
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu' import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
import InviteButton from '~/components/InviteButton/InviteButton' import InviteButton from '~/components/InviteButton/InviteButton'
import CategoriesMenu from '~/components/FilterMenu/CategoriesMenu.vue'
export default { export default {
components: { components: {
@ -102,6 +112,7 @@ export default {
FilterMenu, FilterMenu,
PageFooter, PageFooter,
InviteButton, InviteButton,
CategoriesMenu,
}, },
mixins: [seo], mixins: [seo],
data() { data() {
@ -109,6 +120,7 @@ export default {
mobileSearchVisible: false, mobileSearchVisible: false,
toggleMobileMenu: false, toggleMobileMenu: false,
inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling, inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling,
categoriesActive: this.$env.CATEGORIES_ACTIVE,
} }
}, },
computed: { computed: {

View File

@ -11,7 +11,7 @@
"admin": { "admin": {
"categories": { "categories": {
"categoryName": "Name", "categoryName": "Name",
"name": "Kategorien", "name": "Themen",
"postCount": "Beiträge" "postCount": "Beiträge"
}, },
"dashboard": { "dashboard": {
@ -98,7 +98,7 @@
} }
}, },
"common": { "common": {
"category": "Kategorie ::: Kategorien", "category": "Thema ::: Themen",
"comment": "Kommentar ::: Kommentare", "comment": "Kommentar ::: Kommentare",
"letsTalk": "Miteinander reden", "letsTalk": "Miteinander reden",
"loading": "wird geladen", "loading": "wird geladen",
@ -113,7 +113,7 @@
"takeAction": "Aktiv werden", "takeAction": "Aktiv werden",
"user": "Benutzer ::: Benutzer", "user": "Benutzer ::: Benutzer",
"validations": { "validations": {
"categories": "es müssen eine bis drei Kategorien ausgewählt werden", "categories": "es müssen eine bis drei Themen ausgewählt werden",
"email": "muss eine gültige E-Mail-Adresse sein", "email": "muss eine gültige E-Mail-Adresse sein",
"url": "muss eine gültige URL sein" "url": "muss eine gültige URL sein"
}, },
@ -214,7 +214,7 @@
"amount-shouts": "{amount} recommendations", "amount-shouts": "{amount} recommendations",
"amount-views": "{amount} views", "amount-views": "{amount} views",
"categories": { "categories": {
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt" "infoSelectedNoOfMaxCategories": "{chosen} von {max} Themen ausgewählt"
}, },
"category": { "category": {
"name": { "name": {
@ -349,7 +349,7 @@
}, },
"filter-menu": { "filter-menu": {
"all": "Alle", "all": "Alle",
"categories": "Themenkategorien", "categories": "Themen",
"emotions": "Emotionen", "emotions": "Emotionen",
"filter-by": "Filtern nach ...", "filter-by": "Filtern nach ...",
"following": "Benutzern, denen ich folge", "following": "Benutzern, denen ich folge",
@ -478,7 +478,7 @@
"noDecision": "Keine Entscheidung!", "noDecision": "Keine Entscheidung!",
"numberOfUsers": "{count} Nutzern", "numberOfUsers": "{count} Nutzern",
"previousDecision": "Vorherige Entscheidung:", "previousDecision": "Vorherige Entscheidung:",
"reasonCategory": "Kategorie", "reasonCategory": "Thema",
"reasonDescription": "Beschreibung", "reasonDescription": "Beschreibung",
"reportedOn": "Datum", "reportedOn": "Datum",
"status": "Aktueller Status", "status": "Aktueller Status",
@ -600,8 +600,8 @@
}, },
"reason": { "reason": {
"category": { "category": {
"invalid": "Bitte wähle eine gültige Kategorie aus", "invalid": "Bitte wähle ein gültiges Thema aus",
"label": "Wähle eine Kategorie:", "label": "Wähle ein Thema:",
"options": { "options": {
"advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.", "advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
"criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.", "criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
@ -612,7 +612,7 @@
"other": "Andere …", "other": "Andere …",
"pornographic_content_links": "Das Senden oder Verlinken eindeutig pornografischen Materials." "pornographic_content_links": "Das Senden oder Verlinken eindeutig pornografischen Materials."
}, },
"placeholder": "Kategorie …" "placeholder": "Thema …"
}, },
"description": { "description": {
"label": "Bitte erkläre: Warum möchtest Du dies melden?", "label": "Bitte erkläre: Warum möchtest Du dies melden?",

View File

@ -11,7 +11,7 @@
"admin": { "admin": {
"categories": { "categories": {
"categoryName": "Name", "categoryName": "Name",
"name": "Categories", "name": "Topics",
"postCount": "Posts" "postCount": "Posts"
}, },
"dashboard": { "dashboard": {
@ -98,7 +98,7 @@
} }
}, },
"common": { "common": {
"category": "Category ::: Categories", "category": "Topic ::: Topics",
"comment": "Comment ::: Comments", "comment": "Comment ::: Comments",
"letsTalk": "Let`s Talk", "letsTalk": "Let`s Talk",
"loading": "loading", "loading": "loading",
@ -113,7 +113,7 @@
"takeAction": "Take Action", "takeAction": "Take Action",
"user": "User ::: Users", "user": "User ::: Users",
"validations": { "validations": {
"categories": "at least one and at most three categories must be selected", "categories": "at least one and at most three topics must be selected",
"email": "must be a valid e-mail address", "email": "must be a valid e-mail address",
"url": "must be a valid URL" "url": "must be a valid URL"
}, },
@ -214,7 +214,7 @@
"amount-shouts": "{amount} recommendations", "amount-shouts": "{amount} recommendations",
"amount-views": "{amount} views", "amount-views": "{amount} views",
"categories": { "categories": {
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected" "infoSelectedNoOfMaxCategories": "{chosen} of {max} topics selected"
}, },
"category": { "category": {
"name": { "name": {
@ -349,7 +349,7 @@
}, },
"filter-menu": { "filter-menu": {
"all": "All", "all": "All",
"categories": "Categories of Content", "categories": "Topics",
"emotions": "Emotions", "emotions": "Emotions",
"filter-by": "Filter by ...", "filter-by": "Filter by ...",
"following": "Users I follow", "following": "Users I follow",
@ -478,7 +478,7 @@
"noDecision": "No decision!", "noDecision": "No decision!",
"numberOfUsers": "{count} users", "numberOfUsers": "{count} users",
"previousDecision": "Previous decision:", "previousDecision": "Previous decision:",
"reasonCategory": "Category", "reasonCategory": "Topic",
"reasonDescription": "Description", "reasonDescription": "Description",
"reportedOn": "Date", "reportedOn": "Date",
"status": "Current status", "status": "Current status",
@ -600,8 +600,8 @@
}, },
"reason": { "reason": {
"category": { "category": {
"invalid": "Please select a valid category", "invalid": "Please select a valid topic",
"label": "Select a category:", "label": "Select a topic:",
"options": { "options": {
"advert_products_services_commercial": "Advertising products and services with commercial intent.", "advert_products_services_commercial": "Advertising products and services with commercial intent.",
"criminal_behavior_violation_german_law": "Criminal behavior or violation of German law.", "criminal_behavior_violation_german_law": "Criminal behavior or violation of German law.",
@ -612,7 +612,7 @@
"other": "Other …", "other": "Other …",
"pornographic_content_links": "Posting or linking of clearly pornographic material." "pornographic_content_links": "Posting or linking of clearly pornographic material."
}, },
"placeholder": "Category …" "placeholder": "Topic …"
}, },
"description": { "description": {
"label": "Please explain: Why you like to report this?", "label": "Please explain: Why you like to report this?",