diff --git a/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.js b/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.js new file mode 100644 index 0000000000..a808a5582b --- /dev/null +++ b/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.js @@ -0,0 +1,40 @@ +import { mergeWith, isArray } from 'lodash' + +const getMyGroupIds = async (context) => { + const { user } = context + if (!(user && user.id)) return [] + const session = context.driver.session() + + const readTxResultPromise = await session.readTransaction(async (transaction) => { + const cypher = ` + MATCH (group:Group)<-[membership:MEMBER_OF]-(:User { id: $userId }) + WHERE membership.role IN ['usual', 'admin', 'owner'] + RETURN collect(group.id) AS myGroupIds` + const getMyGroupIdsResponse = await transaction.run(cypher, { userId: user.id }) + return getMyGroupIdsResponse.records.map((record) => record.get('myGroupIds')) + }) + try { + const [myGroupIds] = readTxResultPromise + return myGroupIds + } finally { + session.close() + } +} + +export const filterPostsOfMyGroups = async (params, context) => { + if (!(params.filter && params.filter.postsInMyGroups)) return params + delete params.filter.postsInMyGroups + const myGroupIds = await getMyGroupIds(context) + params.filter = mergeWith( + params.filter, + { + group: { id_in: myGroupIds }, + }, + (objValue, srcValue) => { + if (isArray(objValue)) { + return objValue.concat(srcValue) + } + }, + ) + return params +} diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 78515e6418..d806f3803f 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -6,6 +6,7 @@ import { mergeImage, deleteImage } from './images/images' import Resolver from './helpers/Resolver' import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { filterInvisiblePosts } from './helpers/filterInvisiblePosts' +import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups' import CONFIG from '../../config' const maintainPinnedPosts = (params) => { @@ -21,12 +22,14 @@ const maintainPinnedPosts = (params) => { export default { Query: { Post: async (object, params, context, resolveInfo) => { + params = await filterPostsOfMyGroups(params, context) params = await filterInvisiblePosts(params, context) params = await filterForMutedUsers(params, context) params = await maintainPinnedPosts(params) return neo4jgraphql(object, params, context, resolveInfo) }, profilePagePosts: async (object, params, context, resolveInfo) => { + params = await filterPostsOfMyGroups(params, context) params = await filterInvisiblePosts(params, context) params = await filterForMutedUsers(params, context) return neo4jgraphql(object, params, context, resolveInfo) diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 5bf5820f0d..1eaf7a708c 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -1678,5 +1678,59 @@ describe('Posts in Groups', () => { }) }) }) + + describe('filter posts in my groups', () => { + describe('without any posts in groups', () => { + beforeAll(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('finds no posts', async () => { + const result = await query({ + query: filterPosts(), + variables: { filter: { postsInMyGroups: true } }, + }) + expect(result.data.Post).toHaveLength(0) + expect(result).toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + }) + + describe('with posts in groups', () => { + beforeAll(async () => { + // member of hidden-group and closed-group + authenticatedUser = await allGroupsUser.toJson() + }) + + it('finds two posts', async () => { + const result = await query({ + query: filterPosts(), + variables: { filter: { postsInMyGroups: true } }, + }) + expect(result.data.Post).toHaveLength(2) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) }) }) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 9eac00b0ba..6fc7a3215f 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -82,6 +82,7 @@ input _PostFilter { emotions_single: _PostEMOTEDFilter emotions_every: _PostEMOTEDFilter group: _GroupFilter + postsInMyGroups: Boolean } enum _PostOrdering { diff --git a/webapp/components/FilterMenu/FollowingFilter.spec.js b/webapp/components/FilterMenu/FollowingFilter.spec.js index 4d4a827e58..0f51b305cf 100644 --- a/webapp/components/FilterMenu/FollowingFilter.spec.js +++ b/webapp/components/FilterMenu/FollowingFilter.spec.js @@ -9,12 +9,14 @@ let wrapper describe('FollowingFilter', () => { const mutations = { 'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(), + 'posts/TOGGLE_FILTER_BY_MY_GROUPS': jest.fn(), } const getters = { 'auth/user': () => { return { id: 'u34' } }, 'posts/filteredByUsersFollowed': jest.fn(), + 'posts/filteredByPostsInMyGroups': jest.fn(), } const mocks = { @@ -34,12 +36,18 @@ describe('FollowingFilter', () => { describe('mount', () => { it('sets "filter-by-followed" button attribute `filled`', () => { getters['posts/filteredByUsersFollowed'] = jest.fn(() => true) + getters['posts/filteredByPostsInMyGroups'] = jest.fn(() => true) const wrapper = Wrapper() expect( wrapper .find('.following-filter .filter-list .follower-item .base-button') .classes('--filled'), ).toBe(true) + expect( + wrapper + .find('.following-filter .filter-list .posts-in-my-groups-item .base-button') + .classes('--filled'), + ).toBe(true) }) describe('click "filter-by-followed" button', () => { @@ -48,5 +56,14 @@ describe('FollowingFilter', () => { expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34') }) }) + + describe('click "filter-by-my-groups" button', () => { + it('calls TOGGLE_FILTER_BY_MY_GROUPS', () => { + wrapper + .find('.following-filter .filter-list .posts-in-my-groups-item .base-button') + .trigger('click') + expect(mutations['posts/TOGGLE_FILTER_BY_MY_GROUPS']).toHaveBeenCalled() + }) + }) }) }) diff --git a/webapp/components/FilterMenu/FollowingFilter.vue b/webapp/components/FilterMenu/FollowingFilter.vue index 7c2c2a2822..9a488acd6d 100644 --- a/webapp/components/FilterMenu/FollowingFilter.vue +++ b/webapp/components/FilterMenu/FollowingFilter.vue @@ -10,6 +10,15 @@ @click="toggleFilteredByFollowed(currentUser.id)" /> +
  • + +
  • @@ -28,12 +37,14 @@ export default { computed: { ...mapGetters({ filteredByUsersFollowed: 'posts/filteredByUsersFollowed', + filteredByPostsInMyGroups: 'posts/filteredByPostsInMyGroups', currentUser: 'auth/user', }), }, methods: { ...mapMutations({ toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED', + toggleFilteredByMyGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS', }), }, } diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 9ea7330681..1db5a3dcd9 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -270,9 +270,11 @@ "filterFollow": "Beiträge von Nutzern filtern, denen ich folge", "filterMasonryGrid": { "myFriends": "Nutzer denen ich folge", + "myGroups": "Meine Gruppen", "myTopics": "Meine Themen", "noFilter": "Beiträge filtern" }, + "filterMyGroups": "Beiträge in meinen Gruppen", "inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.", "languageSelectLabel": "Sprache Deines Beitrags", "languageSelectText": "Sprache wählen", @@ -380,6 +382,7 @@ "filter-by": "Filtern nach ...", "following": "Nutzer denen ich folge", "languages": "Sprachen", + "my-groups": "Meinen Gruppen", "order": { "newest": { "hint": "Sortiere die Neuesten nach vorn", diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 9e45396b85..f7a7a54ad6 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -270,9 +270,11 @@ "filterFollow": "Filter contributions from users I follow", "filterMasonryGrid": { "myFriends": "Users I follow", + "myGroups": "My groups", "myTopics": "My topics", "noFilter": "Filter posts" }, + "filterMyGroups": "Contributions in my groups", "inappropriatePicture": "This image may be inappropriate for some people.", "languageSelectLabel": "Language of your contribution", "languageSelectText": "Select Language", @@ -380,6 +382,7 @@ "filter-by": "Filter by ...", "following": "Users I follow", "languages": "Languages", + "my-groups": "My groups", "order": { "newest": { "hint": "Sort posts by the newest first", diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index d5f1ac3d04..ddadec9a82 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -30,7 +30,11 @@
    + + + {{ $t('contribution.filterMasonryGrid.myGroups') }} + + + +
    @@ -203,6 +221,7 @@ export default { methods: { ...mapMutations({ resetByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED', + resetByGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS', resetCategories: 'posts/RESET_CATEGORIES', toggleCategory: 'posts/TOGGLE_CATEGORY', }), diff --git a/webapp/store/posts.js b/webapp/store/posts.js index 5d9891eff0..587867b0eb 100644 --- a/webapp/store/posts.js +++ b/webapp/store/posts.js @@ -30,6 +30,18 @@ export const mutations = { } } }, + TOGGLE_FILTER_BY_MY_GROUPS(state) { + const filter = clone(state.filter) + if (get(filter, 'postsInMyGroups')) { + delete filter.postsInMyGroups + state.filter = filter + } else { + state.filter = { + ...filter, + postsInMyGroups: true, + } + } + }, RESET_CATEGORIES(state) { const filter = clone(state.filter) delete filter.categories_some @@ -84,6 +96,9 @@ export const getters = { filteredByUsersFollowed(state) { return !!get(state.filter, 'author.followedBy_some.id') }, + filteredByPostsInMyGroups(state) { + return !!get(state.filter, 'postsInMyGroups') + }, filteredByEmotions(state) { return get(state.filter, 'emotions_some.emotion_in') || [] }, diff --git a/webapp/store/posts.spec.js b/webapp/store/posts.spec.js index ed728bdd8f..665b147aea 100644 --- a/webapp/store/posts.spec.js +++ b/webapp/store/posts.spec.js @@ -56,6 +56,18 @@ describe('getters', () => { }) }) + describe('filteredByPostsInMyGroups', () => { + it('returns true if filter is set', () => { + state = { filter: { postsInMyGroups: true } } + expect(getters.filteredByPostsInMyGroups(state)).toBe(true) + }) + + it('returns false if filter is not set', () => { + state = { filter: { categories_some: { id_in: [23] } } } + expect(getters.filteredByUsersFollowed(state)).toBe(false) + }) + }) + describe('filteredByEmotions', () => { it('returns an emotions array if filter is set', () => { state = { filter: { emotions_some: { emotion_in: ['sad'] } } } @@ -230,6 +242,35 @@ describe('mutations', () => { }) }) + describe('TOGGLE_FILTER_BY_MY_GROUPS', () => { + beforeEach(() => { + testMutation = () => { + mutations.TOGGLE_FILTER_BY_MY_GROUPS(state) + return getters.filter(state) + } + }) + + describe('given empty filter', () => { + beforeEach(() => { + state = { filter: {} } + }) + + it('sets postsInMyGroups filter to true', () => { + expect(testMutation()).toEqual({ postsInMyGroups: true }) + }) + }) + + describe('already filtered', () => { + beforeEach(() => { + state = { filter: { postsInMyGroups: true } } + }) + + it('removes postsInMyGroups filter', () => { + expect(testMutation()).toEqual({}) + }) + }) + }) + describe('TOGGLE_ORDER', () => { beforeEach(() => { testMutation = (key) => {