diff --git a/.github/file-filters.yml b/.github/file-filters.yml index d7f9cb6c0..8d2d93fac 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -1,4 +1,5 @@ backend: &backend + - '.github/workflows/test-backend.yml' - 'backend/**/*' - 'neo4j/**/*' @@ -6,4 +7,5 @@ docker: &docker - 'docker-compose.*' webapp: &webapp + - '.github/workflows/test-webapp.yml' - 'webapp/**/*' diff --git a/.github/workflows/cleanup-cache-at-pr-closing.yml b/.github/workflows/cleanup-cache-at-pr-closing.yml new file mode 100644 index 000000000..284702e76 --- /dev/null +++ b/.github/workflows/cleanup-cache-at-pr-closing.yml @@ -0,0 +1,42 @@ +############################################################################### +# A Github repo has max 10 GB of cache. +# https://github.blog/changelog/2021-11-23-github-actions-cache-size-is-now-increased-to-10gb-per-repository/ +# +# To avoid "cache thrashing" by their cache eviction policy it is recommended +# to apply a cache cleanup workflow at PR closing to dele cache leftovers of +# the current branch: +# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries +############################################################################### + +name: ocelot.social cache cleanup on pr closing + +on: + pull_request: + types: + - closed + +jobs: + clean-branch-cache: + name: Cleanup branch cache + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + REPO=${{ github.repository }} + BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 84d87c770..03e517826 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -1,7 +1,7 @@ name: ocelot.social backend test CI -on: [push] +on: push jobs: files-changed: @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v3.3.0 - - name: Check for frontend file changes + - name: Check for backend file changes uses: dorny/paths-filter@v2.11.1 id: changes with: @@ -34,12 +34,13 @@ jobs: run: | docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/ docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar - - - name: Upload Artifact - uses: actions/upload-artifact@v3 + + - name: Cache docker images + id: cache-neo4j + uses: actions/cache/save@v3.3.1 with: - name: docker-neo4j-image path: /tmp/neo4j.tar + key: ${{ github.run_id }}-backend-neo4j-cache build_test_backend: name: Docker Build Test - Backend @@ -54,12 +55,13 @@ jobs: run: | docker build --target test -t "ocelotsocialnetwork/backend:test" backend/ docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar - - - name: Upload Artifact - uses: actions/upload-artifact@v3 + + - name: Cache docker images + id: cache-backend + uses: actions/cache/save@v3.3.1 with: - name: docker-backend-test path: /tmp/backend.tar + key: ${{ github.run_id }}-backend-cache lint_backend: name: Lint Backend @@ -84,28 +86,29 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Download Docker Image (Neo4J) - uses: actions/download-artifact@v3 + - name: Restore Neo4J cache + uses: actions/cache/restore@v3.3.1 with: - name: docker-neo4j-image - path: /tmp + path: /tmp/neo4j.tar + key: ${{ github.run_id }}-backend-neo4j-cache + fail-on-cache-miss: true - - name: Load Docker Image - run: docker load < /tmp/neo4j.tar - - - name: Download Docker Image (Backend) - uses: actions/download-artifact@v3 + - name: Restore Backend cache + uses: actions/cache/restore@v3.3.1 with: - name: docker-backend-test - path: /tmp + path: /tmp/backend.tar + key: ${{ github.run_id }}-backend-cache + fail-on-cache-miss: true - - name: Load Docker Image - run: docker load < /tmp/backend.tar + - name: Load Docker Images + run: | + docker load < /tmp/neo4j.tar + docker load < /tmp/backend.tar - - name: backend | copy env files webapp - run: cp webapp/.env.template webapp/.env - - name: backend | copy env files backend - run: cp backend/.env.template backend/.env + - name: backend | copy env files + run: | + cp webapp/.env.template webapp/.env + cp backend/.env.template backend/.env - name: backend | docker-compose run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend @@ -118,3 +121,20 @@ jobs: - name: backend | Unit test incl. coverage check run: docker-compose exec -T backend yarn test + + cleanup: + name: Cleanup + if: ${{ needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true' }} + needs: [files-changed, unit_test_backend] + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Delete cache + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh extension install actions/gh-actions-cache + KEY="${{ github.run_id }}-backend-neo4j-cache" + gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm + KEY="${{ github.run_id }}-backend-cache" + gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 9d007c451..02d65ba9e 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,9 +1,54 @@ name: ocelot.social end-to-end test CI + on: push jobs: + docker_preparation: + name: Fullstack test preparation + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Copy env files + run: | + cp webapp/.env.template webapp/.env + cp backend/.env.template backend/.env + + - name: Build docker images + run: | + mkdir /tmp/images + docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/ + docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/images/neo4j.tar + docker build --target test -t "ocelotsocialnetwork/backend:test" backend/ + docker save "ocelotsocialnetwork/backend:test" > /tmp/images/backend.tar + docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/ + docker save "ocelotsocialnetwork/webapp:test" > /tmp/images/webapp.tar + + - name: Install cypress requirements + run: | + wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386" + cd backend + yarn install + yarn build + cd .. + yarn install + + - name: Cache docker images + id: cache + uses: actions/cache/save@v3.3.1 + with: + path: | + /opt/cucumber-json-formatter + /home/runner/.cache/Cypress + /home/runner/work/Ocelot-Social/Ocelot-Social + /tmp/images/ + key: ${{ github.run_id }}-e2e-preparation-cache + fullstack_tests: name: Fullstack tests + if: success() + needs: docker_preparation runs-on: ubuntu-latest env: jobs: 8 @@ -12,34 +57,56 @@ jobs: # run copies of the current job in parallel job: [1, 2, 3, 4, 5, 6, 7, 8] steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Restore cache + uses: actions/cache/restore@v3.3.1 + id: cache + with: + path: | + /opt/cucumber-json-formatter + /home/runner/.cache/Cypress + /home/runner/work/Ocelot-Social/Ocelot-Social + /tmp/images/ + key: ${{ github.run_id }}-e2e-preparation-cache + fail-on-cache-miss: true - - name: webapp | copy env file - run: cp webapp/.env.template webapp/.env - - - name: backend | copy env file - run: cp backend/.env.template backend/.env - - - name: boot up test system | docker-compose - run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend - - - name: cypress | Fullstack tests - id: e2e-tests + - name: Boot up test system | docker-compose run: | - cd backend - yarn install - yarn build - cd .. - yarn install - yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} ) + chmod +x /opt/cucumber-json-formatter + sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter + docker load < /tmp/images/neo4j.tar + docker load < /tmp/images/backend.tar + docker load < /tmp/images/webapp.tar + docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend + sleep 90s - ########################################################################## - # UPLOAD SCREENSHOTS - IF TESTS FAIL ##################################### - ########################################################################## - - name: Full stack tests | if any test failed, upload screenshots + - name: Full stack tests | run tests + id: e2e-tests + run: yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} ) + + - name: Full stack tests | if tests failed, compile html report + if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }} + run: | + cd cypress/ + node create-cucumber-html-report.js + + - name: Full stack tests | if tests failed, upload report + id: e2e-report if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }} uses: actions/upload-artifact@v3 with: - name: cypress-screenshots - path: cypress/screenshots/ + name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }} + path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report + + cleanup: + name: Cleanup + needs: [docker_preparation, fullstack_tests] + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Delete cache + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh extension install actions/gh-actions-cache + KEY="${{ github.run_id }}-e2e-preparation-cache" + gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm \ No newline at end of file diff --git a/.github/workflows/test-webapp.yml b/.github/workflows/test-webapp.yml index 9ca3023cc..2b1e144a5 100644 --- a/.github/workflows/test-webapp.yml +++ b/.github/workflows/test-webapp.yml @@ -1,7 +1,7 @@ name: ocelot.social webapp test CI -on: [push] +on: push jobs: files-changed: @@ -23,7 +23,7 @@ jobs: prepare: name: Prepare - if: needs.files-changed.outputs.webapp + if: needs.files-changed.outputs.webapp == 'true' needs: files-changed runs-on: ubuntu-latest steps: @@ -34,30 +34,30 @@ jobs: run: | scripts/translations/sort.sh scripts/translations/missing-keys.sh - + build_test_webapp: name: Docker Build Test - Webapp - if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp + if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true' needs: [files-changed, prepare] runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - - name: webapp | Build 'test' image + - name: Webapp | Build 'test' image run: | docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/ docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar - - name: Upload Artifact - uses: actions/upload-artifact@v3 + - name: Cache docker image + uses: actions/cache/save@v3.3.1 with: - name: docker-webapp-test path: /tmp/webapp.tar + key: ${{ github.run_id }}-webapp-cache lint_webapp: name: Lint Webapp - if: needs.files-changed.outputs.webapp + if: needs.files-changed.outputs.webapp == 'true' needs: files-changed runs-on: ubuntu-latest steps: @@ -69,7 +69,7 @@ jobs: unit_test_webapp: name: Unit Tests - Webapp - if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp + if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true' needs: [files-changed, build_test_webapp] runs-on: ubuntu-latest permissions: @@ -78,20 +78,19 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Download Docker Image (Webapp) - uses: actions/download-artifact@v3 + - name: Restore webapp cache + uses: actions/cache/restore@v3.3.1 with: - name: docker-webapp-test - path: /tmp + path: /tmp/webapp.tar + key: ${{ github.run_id }}-webapp-cache - name: Load Docker Image run: docker load < /tmp/webapp.tar - - name: backend | copy env files webapp - run: cp webapp/.env.template webapp/.env - - - name: backend | copy env files backend - run: cp backend/.env.template backend/.env + - name: Copy env files + run: | + cp webapp/.env.template webapp/.env + cp backend/.env.template backend/.env - name: backend | docker-compose run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp @@ -99,3 +98,18 @@ jobs: - name: webapp | Unit tests incl. coverage check run: docker-compose exec -T webapp yarn test + cleanup: + name: Cleanup + if: ${{ needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true' }} + needs: [files-changed, unit_test_webapp] + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Delete cache + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh extension install actions/gh-actions-cache + KEY="${{ github.run_id }}-webapp-cache" + gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm + diff --git a/backend/jest.config.js b/backend/jest.config.js index 264ad13c0..d1cc7bd3f 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -11,7 +11,7 @@ module.exports = { ], coverageThreshold: { global: { - lines: 70, + lines: 67, }, }, testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'], diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 338a30fdf..53cd4cea6 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -11,6 +11,8 @@ import { changeGroupMemberRoleMutation, } from '../graphql/groups' import { createPostMutation } from '../graphql/posts' +import { createRoomMutation } from '../graphql/rooms' +import { createMessageMutation } from '../graphql/messages' import { createCommentMutation } from '../graphql/comments' import { categories } from '../constants/categories' @@ -38,628 +40,579 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }) const { mutate } = createTestClient(server) - const [Hamburg, Berlin, Germany, Paris, France] = await Promise.all([ - Factory.build('location', { - id: 'region.5127278006398860', - name: 'Hamburg', - type: 'region', - lng: 10.0, - lat: 53.55, - nameES: 'Hamburgo', - nameFR: 'Hambourg', - nameIT: 'Amburgo', - nameEN: 'Hamburg', - namePT: 'Hamburgo', - nameDE: 'Hamburg', - nameNL: 'Hamburg', - namePL: 'Hamburg', - nameRU: 'Гамбург', - }), - Factory.build('location', { - id: 'region.14880313158564380', - type: 'region', - name: 'Berlin', - lng: 13.38333, - lat: 52.51667, - nameES: 'Berlín', - nameFR: 'Berlin', - nameIT: 'Berlino', - nameEN: 'Berlin', - namePT: 'Berlim', - nameDE: 'Berlin', - nameNL: 'Berlijn', - namePL: 'Berlin', - nameRU: 'Берлин', - }), - Factory.build('location', { - id: 'country.10743216036480410', - name: 'Germany', - type: 'country', - namePT: 'Alemanha', - nameDE: 'Deutschland', - nameES: 'Alemania', - nameNL: 'Duitsland', - namePL: 'Niemcy', - nameFR: 'Allemagne', - nameIT: 'Germania', - nameEN: 'Germany', - nameRU: 'Германия', - }), - Factory.build('location', { - id: 'region.9397217726497330', - name: 'Paris', - type: 'region', - lng: 2.35183, - lat: 48.85658, - nameES: 'París', - nameFR: 'Paris', - nameIT: 'Parigi', - nameEN: 'Paris', - namePT: 'Paris', - nameDE: 'Paris', - nameNL: 'Parijs', - namePL: 'Paryż', - nameRU: 'Париж', - }), - Factory.build('location', { - id: 'country.9759535382641660', - name: 'France', - type: 'country', - namePT: 'França', - nameDE: 'Frankreich', - nameES: 'Francia', - nameNL: 'Frankrijk', - namePL: 'Francja', - nameFR: 'France', - nameIT: 'Francia', - nameEN: 'France', - nameRU: 'Франция', - }), - ]) - await Promise.all([ - Berlin.relateTo(Germany, 'isIn'), - Hamburg.relateTo(Germany, 'isIn'), - Paris.relateTo(France, 'isIn'), - ]) + // locations + const Hamburg = await Factory.build('location', { + id: 'region.5127278006398860', + name: 'Hamburg', + type: 'region', + lng: 10.0, + lat: 53.55, + nameES: 'Hamburgo', + nameFR: 'Hambourg', + nameIT: 'Amburgo', + nameEN: 'Hamburg', + namePT: 'Hamburgo', + nameDE: 'Hamburg', + nameNL: 'Hamburg', + namePL: 'Hamburg', + nameRU: 'Гамбург', + }) + const Berlin = await Factory.build('location', { + id: 'region.14880313158564380', + type: 'region', + name: 'Berlin', + lng: 13.38333, + lat: 52.51667, + nameES: 'Berlín', + nameFR: 'Berlin', + nameIT: 'Berlino', + nameEN: 'Berlin', + namePT: 'Berlim', + nameDE: 'Berlin', + nameNL: 'Berlijn', + namePL: 'Berlin', + nameRU: 'Берлин', + }) + const Germany = await Factory.build('location', { + id: 'country.10743216036480410', + name: 'Germany', + type: 'country', + namePT: 'Alemanha', + nameDE: 'Deutschland', + nameES: 'Alemania', + nameNL: 'Duitsland', + namePL: 'Niemcy', + nameFR: 'Allemagne', + nameIT: 'Germania', + nameEN: 'Germany', + nameRU: 'Германия', + }) + const Paris = await Factory.build('location', { + id: 'region.9397217726497330', + name: 'Paris', + type: 'region', + lng: 2.35183, + lat: 48.85658, + nameES: 'París', + nameFR: 'Paris', + nameIT: 'Parigi', + nameEN: 'Paris', + namePT: 'Paris', + nameDE: 'Paris', + nameNL: 'Parijs', + namePL: 'Paryż', + nameRU: 'Париж', + }) + const France = await Factory.build('location', { + id: 'country.9759535382641660', + name: 'France', + type: 'country', + namePT: 'França', + nameDE: 'Frankreich', + nameES: 'Francia', + nameNL: 'Frankrijk', + namePL: 'Francja', + nameFR: 'France', + nameIT: 'Francia', + nameEN: 'France', + nameRU: 'Франция', + }) + await Berlin.relateTo(Germany, 'isIn') + await Hamburg.relateTo(Germany, 'isIn') + await Paris.relateTo(France, 'isIn') - const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([ - Factory.build('badge', { - id: 'indiegogo_en_racoon', - icon: '/img/badges/indiegogo_en_racoon.svg', - }), - Factory.build('badge', { - id: 'indiegogo_en_rabbit', - icon: '/img/badges/indiegogo_en_rabbit.svg', - }), - Factory.build('badge', { - id: 'indiegogo_en_wolf', - icon: '/img/badges/indiegogo_en_wolf.svg', - }), - Factory.build('badge', { - id: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', - }), - Factory.build('badge', { - id: 'indiegogo_en_turtle', - icon: '/img/badges/indiegogo_en_turtle.svg', - }), - Factory.build('badge', { - id: 'indiegogo_en_rhino', - icon: '/img/badges/indiegogo_en_rhino.svg', - }), - ]) + // badges + const racoon = await Factory.build('badge', { + id: 'indiegogo_en_racoon', + icon: '/img/badges/indiegogo_en_racoon.svg', + }) + const rabbit = await Factory.build('badge', { + id: 'indiegogo_en_rabbit', + icon: '/img/badges/indiegogo_en_rabbit.svg', + }) + const wolf = await Factory.build('badge', { + id: 'indiegogo_en_wolf', + icon: '/img/badges/indiegogo_en_wolf.svg', + }) + const bear = await Factory.build('badge', { + id: 'indiegogo_en_bear', + icon: '/img/badges/indiegogo_en_bear.svg', + }) + const turtle = await Factory.build('badge', { + id: 'indiegogo_en_turtle', + icon: '/img/badges/indiegogo_en_turtle.svg', + }) + const rhino = await Factory.build('badge', { + id: 'indiegogo_en_rhino', + icon: '/img/badges/indiegogo_en_rhino.svg', + }) - const [peterLustig, bobDerBaumeister, jennyRostock, huey, dewey, louie, dagobert] = - await Promise.all([ - Factory.build( - 'user', - { - id: 'u1', - name: 'Peter Lustig', - slug: 'peter-lustig', - role: 'admin', - }, - { - email: 'admin@example.org', - }, - ), - Factory.build( - 'user', - { - id: 'u2', - name: 'Bob der Baumeister', - slug: 'bob-der-baumeister', - role: 'moderator', - }, - { - email: 'moderator@example.org', - avatar: null, - }, - ), - Factory.build( - 'user', - { - id: 'u3', - name: 'Jenny Rostock', - slug: 'jenny-rostock', - role: 'user', - }, - { - email: 'user@example.org', - }, - ), - Factory.build( - 'user', - { - id: 'u4', - name: 'Huey', - slug: 'huey', - role: 'user', - }, - { - email: 'huey@example.org', - }, - ), - Factory.build( - 'user', - { - id: 'u5', - name: 'Dewey', - slug: 'dewey', - role: 'user', - }, - { - email: 'dewey@example.org', - avatar: null, - }, - ), - Factory.build( - 'user', - { - id: 'u6', - name: 'Louie', - slug: 'louie', - role: 'user', - }, - { - email: 'louie@example.org', - }, - ), - Factory.build( - 'user', - { - id: 'u7', - name: 'Dagobert', - slug: 'dagobert', - role: 'user', - }, - { - email: 'dagobert@example.org', - }, - ), - ]) - - await Promise.all([ - peterLustig.relateTo(Berlin, 'isIn'), - bobDerBaumeister.relateTo(Hamburg, 'isIn'), - jennyRostock.relateTo(Paris, 'isIn'), - huey.relateTo(Paris, 'isIn'), - ]) - - await Promise.all([ - peterLustig.relateTo(racoon, 'rewarded'), - peterLustig.relateTo(rhino, 'rewarded'), - peterLustig.relateTo(wolf, 'rewarded'), - bobDerBaumeister.relateTo(racoon, 'rewarded'), - bobDerBaumeister.relateTo(turtle, 'rewarded'), - jennyRostock.relateTo(bear, 'rewarded'), - dagobert.relateTo(rabbit, 'rewarded'), - - peterLustig.relateTo(bobDerBaumeister, 'friends'), - peterLustig.relateTo(jennyRostock, 'friends'), - bobDerBaumeister.relateTo(jennyRostock, 'friends'), - - peterLustig.relateTo(jennyRostock, 'following'), - peterLustig.relateTo(huey, 'following'), - bobDerBaumeister.relateTo(huey, 'following'), - jennyRostock.relateTo(huey, 'following'), - huey.relateTo(dewey, 'following'), - dewey.relateTo(huey, 'following'), - louie.relateTo(jennyRostock, 'following'), - - huey.relateTo(dagobert, 'muted'), - dewey.relateTo(dagobert, 'muted'), - louie.relateTo(dagobert, 'muted'), - - dagobert.relateTo(huey, 'blocked'), - dagobert.relateTo(dewey, 'blocked'), - dagobert.relateTo(louie, 'blocked'), - ]) - - await Promise.all( - categories.map(({ icon, name }, index) => { - return Factory.build('category', { - id: `cat${index + 1}`, - slug: name, - name, - icon, - }) - }), + // users + const peterLustig = await Factory.build( + 'user', + { + id: 'u1', + name: 'Peter Lustig', + slug: 'peter-lustig', + role: 'admin', + }, + { + email: 'admin@example.org', + }, + ) + const bobDerBaumeister = await Factory.build( + 'user', + { + id: 'u2', + name: 'Bob der Baumeister', + slug: 'bob-der-baumeister', + role: 'moderator', + }, + { + email: 'moderator@example.org', + avatar: null, + }, + ) + const jennyRostock = await Factory.build( + 'user', + { + id: 'u3', + name: 'Jenny Rostock', + slug: 'jenny-rostock', + role: 'user', + }, + { + email: 'user@example.org', + }, + ) + const huey = await Factory.build( + 'user', + { + id: 'u4', + name: 'Huey', + slug: 'huey', + role: 'user', + }, + { + email: 'huey@example.org', + }, + ) + const dewey = await Factory.build( + 'user', + { + id: 'u5', + name: 'Dewey', + slug: 'dewey', + role: 'user', + }, + { + email: 'dewey@example.org', + avatar: null, + }, + ) + const louie = await Factory.build( + 'user', + { + id: 'u6', + name: 'Louie', + slug: 'louie', + role: 'user', + }, + { + email: 'louie@example.org', + }, + ) + const dagobert = await Factory.build( + 'user', + { + id: 'u7', + name: 'Dagobert', + slug: 'dagobert', + role: 'user', + }, + { + email: 'dagobert@example.org', + }, ) - const [environment, nature, democracy, freedom] = await Promise.all([ - Factory.build('tag', { - id: 'Environment', - }), - Factory.build('tag', { - id: 'Nature', - }), - Factory.build('tag', { - id: 'Democracy', - }), - Factory.build('tag', { - id: 'Freedom', - }), - ]) + await peterLustig.relateTo(Berlin, 'isIn') + await bobDerBaumeister.relateTo(Hamburg, 'isIn') + await jennyRostock.relateTo(Paris, 'isIn') + await huey.relateTo(Paris, 'isIn') - // Create Groups + await peterLustig.relateTo(racoon, 'rewarded') + await peterLustig.relateTo(rhino, 'rewarded') + await peterLustig.relateTo(wolf, 'rewarded') + await bobDerBaumeister.relateTo(racoon, 'rewarded') + await bobDerBaumeister.relateTo(turtle, 'rewarded') + await jennyRostock.relateTo(bear, 'rewarded') + await dagobert.relateTo(rabbit, 'rewarded') + await peterLustig.relateTo(bobDerBaumeister, 'friends') + await peterLustig.relateTo(jennyRostock, 'friends') + await bobDerBaumeister.relateTo(jennyRostock, 'friends') + + await peterLustig.relateTo(jennyRostock, 'following') + await peterLustig.relateTo(huey, 'following') + await bobDerBaumeister.relateTo(huey, 'following') + await jennyRostock.relateTo(huey, 'following') + await huey.relateTo(dewey, 'following') + await dewey.relateTo(huey, 'following') + await louie.relateTo(jennyRostock, 'following') + + await huey.relateTo(dagobert, 'muted') + await dewey.relateTo(dagobert, 'muted') + await louie.relateTo(dagobert, 'muted') + + await dagobert.relateTo(huey, 'blocked') + await dagobert.relateTo(dewey, 'blocked') + await dagobert.relateTo(louie, 'blocked') + + // categories + let i = 0 + for (const category of categories) { + await Factory.build('category', { + id: `cat${i++}`, + slug: category.name, + naem: category.name, + icon: category.icon, + }) + } + + // tags + const environment = await Factory.build('tag', { + id: 'Environment', + }) + const nature = await Factory.build('tag', { + id: 'Nature', + }) + const democracy = await Factory.build('tag', { + id: 'Democracy', + }) + const freedom = await Factory.build('tag', { + id: 'Freedom', + }) + + // groups authenticatedUser = await peterLustig.toJson() - await Promise.all([ - mutate({ - mutation: createGroupMutation(), - variables: { - id: 'g0', - name: 'Investigative Journalism', - about: 'Investigative journalists share ideas and insights and can collaborate.', - description: `

English:

This group is hidden.

What is our group for?

This group was created to allow investigative journalists to share and collaborate.

How does it work?

Here you can internally share posts and comments about them.


Deutsch:

Diese Gruppe ist verborgen.

Wofür ist unsere Gruppe?

Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.

Wie funktioniert das?

Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.

`, - groupType: 'hidden', - actionRadius: 'global', - categoryIds: ['cat6', 'cat12', 'cat16'], - locationName: 'Hamburg, Germany', - }, - }), - ]) - await Promise.all([ - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g0', - userId: 'u2', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g0', - userId: 'u4', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g0', - userId: 'u6', - }, - }), - ]) - await Promise.all([ - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g0', - userId: 'u2', - roleInGroup: 'usual', - }, - }), - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g0', - userId: 'u4', - roleInGroup: 'admin', - }, - }), - ]) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'g0', + name: 'Investigative Journalism', + about: 'Investigative journalists share ideas and insights and can collaborate.', + description: `

English:

This group is hidden.

What is our group for?

This group was created to allow investigative journalists to share and collaborate.

How does it work?

Here you can internally share posts and comments about them.


Deutsch:

Diese Gruppe ist verborgen.

Wofür ist unsere Gruppe?

Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.

Wie funktioniert das?

Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.

`, + groupType: 'hidden', + actionRadius: 'global', + categoryIds: ['cat6', 'cat12', 'cat16'], + locationName: 'Hamburg, Germany', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g0', + userId: 'u2', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g0', + userId: 'u4', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g0', + userId: 'u6', + }, + }) + + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g0', + userId: 'u2', + roleInGroup: 'usual', + }, + }) + + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g0', + userId: 'u4', + roleInGroup: 'admin', + }, + }) // post into group - await Promise.all([ - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p0-g0', - groupId: 'g0', - title: `What happend in Shanghai?`, - content: 'A sack of rise dropped in Shanghai. Should we further investigate?', - categoryIds: ['cat6'], - }, - }), - ]) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p0-g0', + groupId: 'g0', + title: `What happend in Shanghai?`, + content: 'A sack of rise dropped in Shanghai. Should we further investigate?', + categoryIds: ['cat6'], + }, + }) + authenticatedUser = await bobDerBaumeister.toJson() - await Promise.all([ - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p1-g0', - groupId: 'g0', - title: `The man on the moon`, - content: 'We have to further investigate about the stories of a man living on the moon.', - categoryIds: ['cat12', 'cat16'], - }, - }), - ]) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p1-g0', + groupId: 'g0', + title: `The man on the moon`, + content: 'We have to further investigate about the stories of a man living on the moon.', + categoryIds: ['cat12', 'cat16'], + }, + }) authenticatedUser = await jennyRostock.toJson() - await Promise.all([ - mutate({ - mutation: createGroupMutation(), - variables: { - id: 'g1', - name: 'School For Citizens', - about: 'Our children shall receive education for life.', - description: `

English

Our goal

Only those who enjoy learning and do not lose their curiosity can obtain a good education for life and continue to learn with joy throughout their lives.

Curiosity

For this we need a school that takes up the curiosity of the children, the people, and satisfies it through a lot of experience.


Deutsch

Unser Ziel

Nur wer Spaß am Lernen hat und seine Neugier nicht verliert, kann gute Bildung für's Leben erlangen und sein ganzes Leben mit Freude weiter lernen.

Neugier

Dazu benötigen wir eine Schule, die die Neugier der Kinder, der Menschen, aufnimmt und durch viel Erfahrung befriedigt.

`, - groupType: 'closed', - actionRadius: 'national', - categoryIds: ['cat8', 'cat14'], - locationName: 'France', - }, - }), - ]) - await Promise.all([ - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g1', - userId: 'u1', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g1', - userId: 'u2', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g1', - userId: 'u5', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g1', - userId: 'u6', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g1', - userId: 'u7', - }, - }), - ]) - await Promise.all([ - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g1', - userId: 'u1', - roleInGroup: 'usual', - }, - }), - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g1', - userId: 'u5', - roleInGroup: 'admin', - }, - }), - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g1', - userId: 'u6', - roleInGroup: 'owner', - }, - }), - ]) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'g1', + name: 'School For Citizens', + about: 'Our children shall receive education for life.', + description: `

English

Our goal

Only those who enjoy learning and do not lose their curiosity can obtain a good education for life and continue to learn with joy throughout their lives.

Curiosity

For this we need a school that takes up the curiosity of the children, the people, and satisfies it through a lot of experience.


Deutsch

Unser Ziel

Nur wer Spaß am Lernen hat und seine Neugier nicht verliert, kann gute Bildung für's Leben erlangen und sein ganzes Leben mit Freude weiter lernen.

Neugier

Dazu benötigen wir eine Schule, die die Neugier der Kinder, der Menschen, aufnimmt und durch viel Erfahrung befriedigt.

`, + groupType: 'closed', + actionRadius: 'national', + categoryIds: ['cat8', 'cat14'], + locationName: 'France', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g1', + userId: 'u1', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g1', + userId: 'u2', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g1', + userId: 'u5', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g1', + userId: 'u6', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g1', + userId: 'u7', + }, + }) + + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g1', + userId: 'u1', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g1', + userId: 'u5', + roleInGroup: 'admin', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g1', + userId: 'u6', + roleInGroup: 'owner', + }, + }) // post into group - await Promise.all([ - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p0-g1', - groupId: 'g1', - title: `Can we use ocelot for education?`, - content: 'I like the concept of this school. Can we use our software in this?', - categoryIds: ['cat8'], - }, - }), - ]) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p0-g1', + groupId: 'g1', + title: `Can we use ocelot for education?`, + content: 'I like the concept of this school. Can we use our software in this?', + categoryIds: ['cat8'], + }, + }) authenticatedUser = await peterLustig.toJson() - await Promise.all([ - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p1-g1', - groupId: 'g1', - title: `Can we push this idea out of France?`, - content: 'This idea is too inportant to have the scope only on France.', - categoryIds: ['cat14'], - }, - }), - ]) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p1-g1', + groupId: 'g1', + title: `Can we push this idea out of France?`, + content: 'This idea is too inportant to have the scope only on France.', + categoryIds: ['cat14'], + }, + }) authenticatedUser = await bobDerBaumeister.toJson() - await Promise.all([ - mutate({ - mutation: createGroupMutation(), - variables: { - id: 'g2', - name: 'Yoga Practice', - about: 'We do yoga around the clock.', - description: `

What Is yoga?

Yoga is not just about practicing asanas. It's about how we do it.

And practicing asanas doesn't have to be yoga, it can be more athletic than yogic.

What makes practicing asanas yogic?

The important thing is:

`, - groupType: 'public', - actionRadius: 'interplanetary', - categoryIds: ['cat4', 'cat5', 'cat17'], - }, - }), - ]) - await Promise.all([ - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g2', - userId: 'u3', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g2', - userId: 'u4', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g2', - userId: 'u5', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g2', - userId: 'u6', - }, - }), - mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'g2', - userId: 'u7', - }, - }), - ]) - await Promise.all([ - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g2', - userId: 'u3', - roleInGroup: 'usual', - }, - }), - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g2', - userId: 'u4', - roleInGroup: 'pending', - }, - }), - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g2', - userId: 'u5', - roleInGroup: 'admin', - }, - }), - mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'g2', - userId: 'u6', - roleInGroup: 'usual', - }, - }), - ]) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'g2', + name: 'Yoga Practice', + about: 'We do yoga around the clock.', + description: `

What Is yoga?

Yoga is not just about practicing asanas. It's about how we do it.

And practicing asanas doesn't have to be yoga, it can be more athletic than yogic.

What makes practicing asanas yogic?

The important thing is:

`, + groupType: 'public', + actionRadius: 'interplanetary', + categoryIds: ['cat4', 'cat5', 'cat17'], + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g2', + userId: 'u3', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g2', + userId: 'u4', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g2', + userId: 'u5', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g2', + userId: 'u6', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g2', + userId: 'u7', + }, + }) + + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g2', + userId: 'u3', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g2', + userId: 'u4', + roleInGroup: 'pending', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g2', + userId: 'u5', + roleInGroup: 'admin', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g2', + userId: 'u6', + roleInGroup: 'usual', + }, + }) authenticatedUser = await louie.toJson() - await Promise.all([ - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p0-g2', - groupId: 'g2', - title: `I am a Noob`, - content: 'I am new to Yoga and did not join this group so far.', - categoryIds: ['cat4'], - }, - }), - ]) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p0-g2', + groupId: 'g2', + title: `I am a Noob`, + content: 'I am new to Yoga and did not join this group so far.', + categoryIds: ['cat4'], + }, + }) // Create Events (by peter lustig) authenticatedUser = await peterLustig.toJson() const now = new Date() - await Promise.all([ - mutate({ - mutation: createPostMutation(), - variables: { - id: 'e0', - title: 'Illegaler Kindergeburtstag', - content: 'Elli hat nächste Woche Geburtstag. Wir feiern das!', - categoryIds: ['cat4'], - postType: 'Event', - eventInput: { - eventStart: new Date( - now.getFullYear(), - now.getMonth(), - now.getDate() + 7, - ).toISOString(), - eventVenue: 'Ellis Kinderzimmer', - eventLocationName: 'Deutschland', - }, + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'e0', + title: 'Illegaler Kindergeburtstag', + content: 'Elli hat nächste Woche Geburtstag. Wir feiern das!', + categoryIds: ['cat4'], + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7).toISOString(), + eventVenue: 'Ellis Kinderzimmer', + eventLocationName: 'Deutschland', }, - }), - mutate({ - mutation: createPostMutation(), - variables: { - id: 'e1', - title: 'Wir Schützen den Stuttgarter Schlossgarten', - content: 'Kein Baum wird gefällt werden!', - categoryIds: ['cat5'], - postType: 'Event', - eventInput: { - eventStart: new Date( - now.getFullYear(), - now.getMonth(), - now.getDate() + 1, - ).toISOString(), - eventVenue: 'Schlossgarten', - eventLocationName: 'Stuttgart', - }, + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'e1', + title: 'Wir Schützen den Stuttgarter Schlossgarten', + content: 'Kein Baum wird gefällt werden!', + categoryIds: ['cat5'], + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString(), + eventVenue: 'Schlossgarten', + eventLocationName: 'Stuttgart', }, - }), - mutate({ - mutation: createPostMutation(), - variables: { - id: 'e2', - title: 'IT 4 Change Treffen', - content: 'Wir sitzen eine Woche zusammen rum und glotzen uns blöde an.', - categoryIds: ['cat5'], - postType: 'Event', - eventInput: { - eventStart: new Date( - now.getFullYear(), - now.getMonth(), - now.getDate() + 1, - ).toISOString(), - eventEnd: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 4).toISOString(), - eventVenue: 'Ferienlager', - eventLocationName: 'Bahra, Sachsen', - }, + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'e2', + title: 'IT 4 Change Treffen', + content: 'Wir sitzen eine Woche zusammen rum und glotzen uns blöde an.', + categoryIds: ['cat5'], + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString(), + eventEnd: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 4).toISOString(), + eventVenue: 'Ferienlager', + eventLocationName: 'Bahra, Sachsen', }, - }), - ]) + }, + }) let passedEvent = await neode.find('Post', 'e1') await passedEvent.update({ eventStart: new Date(2010, 8, 30, 10).toISOString() }) @@ -668,166 +621,164 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 3).toISOString(), }) - // Create Posts (Articles) - - const [p0, p1, p3, p4, p5, p6, p9, p10, p11, p13, p14, p15] = await Promise.all([ - Factory.build( - 'post', - { - id: 'p0', - language: sample(languages), - }, - { - categoryIds: ['cat16'], - author: peterLustig, - image: Factory.build('image', { - url: faker.image.unsplash.food(300, 169), - sensitive: true, - aspectRatio: 300 / 169, - }), - }, - ), - Factory.build( - 'post', - { - id: 'p1', - language: sample(languages), - }, - { - categoryIds: ['cat1'], - author: bobDerBaumeister, - image: Factory.build('image', { - url: faker.image.unsplash.technology(300, 1500), - aspectRatio: 300 / 1500, - }), - }, - ), - Factory.build( - 'post', - { - id: 'p3', - language: sample(languages), - }, - { - categoryIds: ['cat3'], - author: huey, - }, - ), - Factory.build( - 'post', - { - id: 'p4', - language: sample(languages), - }, - { - categoryIds: ['cat4'], - author: dewey, - }, - ), - Factory.build( - 'post', - { - id: 'p5', - language: sample(languages), - }, - { - categoryIds: ['cat5'], - author: louie, - }, - ), - Factory.build( - 'post', - { - id: 'p6', - language: sample(languages), - }, - { - categoryIds: ['cat6'], - author: peterLustig, - image: Factory.build('image', { - url: faker.image.unsplash.buildings(300, 857), - aspectRatio: 300 / 857, - }), - }, - ), - Factory.build( - 'post', - { - id: 'p9', - language: sample(languages), - }, - { - categoryIds: ['cat9'], - author: huey, - }, - ), - Factory.build( - 'post', - { - id: 'p10', - }, - { - categoryIds: ['cat10'], - author: dewey, - image: Factory.build('image', { - sensitive: true, - }), - }, - ), - Factory.build( - 'post', - { - id: 'p11', - language: sample(languages), - }, - { - categoryIds: ['cat11'], - author: louie, - image: Factory.build('image', { - url: faker.image.unsplash.people(300, 901), - aspectRatio: 300 / 901, - }), - }, - ), - Factory.build( - 'post', - { - id: 'p13', - language: sample(languages), - }, - { - categoryIds: ['cat13'], - author: bobDerBaumeister, - }, - ), - Factory.build( - 'post', - { - id: 'p14', - language: sample(languages), - }, - { - categoryIds: ['cat14'], - author: jennyRostock, - image: Factory.build('image', { - url: faker.image.unsplash.objects(300, 200), - aspectRatio: 300 / 450, - }), - }, - ), - Factory.build( - 'post', - { - id: 'p15', - language: sample(languages), - }, - { - categoryIds: ['cat15'], - author: huey, - }, - ), - ]) + // posts (articles) + const p0 = await Factory.build( + 'post', + { + id: 'p0', + language: sample(languages), + }, + { + categoryIds: ['cat16'], + author: peterLustig, + image: Factory.build('image', { + url: faker.image.unsplash.food(300, 169), + sensitive: true, + aspectRatio: 300 / 169, + }), + }, + ) + const p1 = await Factory.build( + 'post', + { + id: 'p1', + language: sample(languages), + }, + { + categoryIds: ['cat1'], + author: bobDerBaumeister, + image: Factory.build('image', { + url: faker.image.unsplash.technology(300, 1500), + aspectRatio: 300 / 1500, + }), + }, + ) + const p3 = await Factory.build( + 'post', + { + id: 'p3', + language: sample(languages), + }, + { + categoryIds: ['cat3'], + author: huey, + }, + ) + const p4 = await Factory.build( + 'post', + { + id: 'p4', + language: sample(languages), + }, + { + categoryIds: ['cat4'], + author: dewey, + }, + ) + const p5 = await Factory.build( + 'post', + { + id: 'p5', + language: sample(languages), + }, + { + categoryIds: ['cat5'], + author: louie, + }, + ) + const p6 = await Factory.build( + 'post', + { + id: 'p6', + language: sample(languages), + }, + { + categoryIds: ['cat6'], + author: peterLustig, + image: Factory.build('image', { + url: faker.image.unsplash.buildings(300, 857), + aspectRatio: 300 / 857, + }), + }, + ) + const p9 = await Factory.build( + 'post', + { + id: 'p9', + language: sample(languages), + }, + { + categoryIds: ['cat9'], + author: huey, + }, + ) + const p10 = await Factory.build( + 'post', + { + id: 'p10', + }, + { + categoryIds: ['cat10'], + author: dewey, + image: Factory.build('image', { + sensitive: true, + }), + }, + ) + const p11 = await Factory.build( + 'post', + { + id: 'p11', + language: sample(languages), + }, + { + categoryIds: ['cat11'], + author: louie, + image: Factory.build('image', { + url: faker.image.unsplash.people(300, 901), + aspectRatio: 300 / 901, + }), + }, + ) + const p13 = await Factory.build( + 'post', + { + id: 'p13', + language: sample(languages), + }, + { + categoryIds: ['cat13'], + author: bobDerBaumeister, + }, + ) + const p14 = await Factory.build( + 'post', + { + id: 'p14', + language: sample(languages), + }, + { + categoryIds: ['cat14'], + author: jennyRostock, + image: Factory.build('image', { + url: faker.image.unsplash.objects(300, 200), + aspectRatio: 300 / 450, + }), + }, + ) + const p15 = await Factory.build( + 'post', + { + id: 'p15', + language: sample(languages), + }, + { + categoryIds: ['cat15'], + author: huey, + }, + ) + // invite code await Factory.build( 'inviteCode', { @@ -848,48 +799,49 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] const hashtagAndMention1 = 'The new physics of #QuantenFlussTheorie can explain #QuantumGravity! @peter-lustig got that already. ;-)' - await Promise.all([ - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p2', - title: `Nature Philosophy Yoga`, - content: hashtag1, - categoryIds: ['cat2'], - }, - }), - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p7', - title: 'This is post #7', - content: `${mention1} ${faker.lorem.paragraph()}`, - categoryIds: ['cat7'], - }, - }), - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p8', - image: faker.image.unsplash.nature(), - title: `Quantum Flow Theory explains Quantum Gravity`, - content: hashtagAndMention1, - categoryIds: ['cat8'], - }, - }), - mutate({ - mutation: createPostMutation(), - variables: { - id: 'p12', - title: 'This is post #12', - content: `${mention2} ${faker.lorem.paragraph()}`, - categoryIds: ['cat12'], - }, - }), - ]) - const [p2, p7, p8, p12] = await Promise.all( - ['p2', 'p7', 'p8', 'p12'].map((id) => neode.find('Post', id)), - ) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p2', + title: `Nature Philosophy Yoga`, + content: hashtag1, + categoryIds: ['cat2'], + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p7', + title: 'This is post #7', + content: `${mention1} ${faker.lorem.paragraph()}`, + categoryIds: ['cat7'], + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p8', + image: faker.image.unsplash.nature(), + title: `Quantum Flow Theory explains Quantum Gravity`, + content: hashtagAndMention1, + categoryIds: ['cat8'], + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'p12', + title: 'This is post #12', + content: `${mention2} ${faker.lorem.paragraph()}`, + categoryIds: ['cat12'], + }, + }) + + const p2 = await neode.find('Post', 'p2') + const p7 = await neode.find('Post', 'p7') + const p8 = await neode.find('Post', 'p8') + const p12 = await neode.find('Post', 'p12') + authenticatedUser = null authenticatedUser = await dewey.toJson() @@ -897,35 +849,35 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] 'I heard @jenny-rostock has practiced it for 3 years now.' const mentionInComment2 = 'Did @peter-lustig tell you?' - await Promise.all([ - mutate({ - mutation: createCommentMutation, - variables: { - id: 'c4', - postId: 'p2', - content: mentionInComment1, - }, - }), - mutate({ - mutation: createCommentMutation, - variables: { - id: 'c4-1', - postId: 'p2', - content: mentionInComment2, - }, - }), - mutate({ - mutation: createCommentMutation, - variables: { - postId: 'p14', - content: faker.lorem.paragraph(), - }, - }), // should send a notification - ]) + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'c4', + postId: 'p2', + content: mentionInComment1, + }, + }) + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'c4-1', + postId: 'p2', + content: mentionInComment2, + }, + }) + await mutate({ + mutation: createCommentMutation, + variables: { + postId: 'p14', + content: faker.lorem.paragraph(), + }, + }) // should send a notification + authenticatedUser = null - const comments = await Promise.all([ - Factory.build( + const comments: any[] = [] + comments.push( + await Factory.build( 'comment', { id: 'c1', @@ -935,7 +887,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p1', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c2', @@ -945,7 +897,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p1', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c3', @@ -955,7 +907,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p3', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c5', @@ -965,7 +917,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p3', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c6', @@ -975,7 +927,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p4', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c7', @@ -985,7 +937,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p2', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c8', @@ -995,7 +947,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p15', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c9', @@ -1005,7 +957,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p15', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c10', @@ -1015,7 +967,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p15', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c11', @@ -1025,7 +977,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p15', }, ), - Factory.build( + await Factory.build( 'comment', { id: 'c12', @@ -1035,84 +987,81 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p15', }, ), - ]) + ) const trollingComment = comments[0] - await Promise.all([ - democracy.relateTo(p3, 'post'), - democracy.relateTo(p11, 'post'), - democracy.relateTo(p15, 'post'), - democracy.relateTo(p7, 'post'), - environment.relateTo(p1, 'post'), - environment.relateTo(p5, 'post'), - environment.relateTo(p9, 'post'), - environment.relateTo(p13, 'post'), - freedom.relateTo(p0, 'post'), - freedom.relateTo(p4, 'post'), - freedom.relateTo(p8, 'post'), - freedom.relateTo(p12, 'post'), - nature.relateTo(p2, 'post'), - nature.relateTo(p6, 'post'), - nature.relateTo(p10, 'post'), - nature.relateTo(p14, 'post'), - peterLustig.relateTo(p15, 'emoted', { emotion: 'surprised' }), - bobDerBaumeister.relateTo(p15, 'emoted', { emotion: 'surprised' }), - jennyRostock.relateTo(p15, 'emoted', { emotion: 'surprised' }), - huey.relateTo(p15, 'emoted', { emotion: 'surprised' }), - dewey.relateTo(p15, 'emoted', { emotion: 'surprised' }), - louie.relateTo(p15, 'emoted', { emotion: 'surprised' }), - dagobert.relateTo(p15, 'emoted', { emotion: 'surprised' }), - bobDerBaumeister.relateTo(p14, 'emoted', { emotion: 'cry' }), - jennyRostock.relateTo(p13, 'emoted', { emotion: 'angry' }), - huey.relateTo(p12, 'emoted', { emotion: 'funny' }), - dewey.relateTo(p11, 'emoted', { emotion: 'surprised' }), - louie.relateTo(p10, 'emoted', { emotion: 'cry' }), - dewey.relateTo(p9, 'emoted', { emotion: 'happy' }), - huey.relateTo(p8, 'emoted', { emotion: 'angry' }), - jennyRostock.relateTo(p7, 'emoted', { emotion: 'funny' }), - bobDerBaumeister.relateTo(p6, 'emoted', { emotion: 'surprised' }), - peterLustig.relateTo(p5, 'emoted', { emotion: 'cry' }), - bobDerBaumeister.relateTo(p4, 'emoted', { emotion: 'happy' }), - jennyRostock.relateTo(p3, 'emoted', { emotion: 'angry' }), - huey.relateTo(p2, 'emoted', { emotion: 'funny' }), - dewey.relateTo(p1, 'emoted', { emotion: 'surprised' }), - louie.relateTo(p0, 'emoted', { emotion: 'cry' }), - ]) + await democracy.relateTo(p3, 'post') + await democracy.relateTo(p11, 'post') + await democracy.relateTo(p15, 'post') + await democracy.relateTo(p7, 'post') + await environment.relateTo(p1, 'post') + await environment.relateTo(p5, 'post') + await environment.relateTo(p9, 'post') + await environment.relateTo(p13, 'post') + await freedom.relateTo(p0, 'post') + await freedom.relateTo(p4, 'post') + await freedom.relateTo(p8, 'post') + await freedom.relateTo(p12, 'post') + await nature.relateTo(p2, 'post') + await nature.relateTo(p6, 'post') + await nature.relateTo(p10, 'post') + await nature.relateTo(p14, 'post') + await peterLustig.relateTo(p15, 'emoted', { emotion: 'surprised' }) + await bobDerBaumeister.relateTo(p15, 'emoted', { emotion: 'surprised' }) + await jennyRostock.relateTo(p15, 'emoted', { emotion: 'surprised' }) + await huey.relateTo(p15, 'emoted', { emotion: 'surprised' }) + await dewey.relateTo(p15, 'emoted', { emotion: 'surprised' }) + await louie.relateTo(p15, 'emoted', { emotion: 'surprised' }) + await dagobert.relateTo(p15, 'emoted', { emotion: 'surprised' }) + await bobDerBaumeister.relateTo(p14, 'emoted', { emotion: 'cry' }) + await jennyRostock.relateTo(p13, 'emoted', { emotion: 'angry' }) + await huey.relateTo(p12, 'emoted', { emotion: 'funny' }) + await dewey.relateTo(p11, 'emoted', { emotion: 'surprised' }) + await louie.relateTo(p10, 'emoted', { emotion: 'cry' }) + await dewey.relateTo(p9, 'emoted', { emotion: 'happy' }) + await huey.relateTo(p8, 'emoted', { emotion: 'angry' }) + await jennyRostock.relateTo(p7, 'emoted', { emotion: 'funny' }) + await bobDerBaumeister.relateTo(p6, 'emoted', { emotion: 'surprised' }) + await peterLustig.relateTo(p5, 'emoted', { emotion: 'cry' }) + await bobDerBaumeister.relateTo(p4, 'emoted', { emotion: 'happy' }) + await jennyRostock.relateTo(p3, 'emoted', { emotion: 'angry' }) + await huey.relateTo(p2, 'emoted', { emotion: 'funny' }) + await dewey.relateTo(p1, 'emoted', { emotion: 'surprised' }) + await louie.relateTo(p0, 'emoted', { emotion: 'cry' }) - await Promise.all([ - peterLustig.relateTo(p1, 'shouted'), - peterLustig.relateTo(p6, 'shouted'), - bobDerBaumeister.relateTo(p0, 'shouted'), - bobDerBaumeister.relateTo(p6, 'shouted'), - jennyRostock.relateTo(p6, 'shouted'), - jennyRostock.relateTo(p7, 'shouted'), - huey.relateTo(p8, 'shouted'), - huey.relateTo(p9, 'shouted'), - dewey.relateTo(p10, 'shouted'), - peterLustig.relateTo(p2, 'shouted'), - peterLustig.relateTo(p6, 'shouted'), - bobDerBaumeister.relateTo(p0, 'shouted'), - bobDerBaumeister.relateTo(p6, 'shouted'), - jennyRostock.relateTo(p6, 'shouted'), - jennyRostock.relateTo(p7, 'shouted'), - huey.relateTo(p8, 'shouted'), - huey.relateTo(p9, 'shouted'), - louie.relateTo(p10, 'shouted'), - ]) - const reports = await Promise.all([ - Factory.build('report'), - Factory.build('report'), - Factory.build('report'), - Factory.build('report'), - ]) + await peterLustig.relateTo(p1, 'shouted') + await peterLustig.relateTo(p6, 'shouted') + await bobDerBaumeister.relateTo(p0, 'shouted') + await bobDerBaumeister.relateTo(p6, 'shouted') + await jennyRostock.relateTo(p6, 'shouted') + await jennyRostock.relateTo(p7, 'shouted') + await huey.relateTo(p8, 'shouted') + await huey.relateTo(p9, 'shouted') + await dewey.relateTo(p10, 'shouted') + await peterLustig.relateTo(p2, 'shouted') + await peterLustig.relateTo(p6, 'shouted') + await bobDerBaumeister.relateTo(p0, 'shouted') + await bobDerBaumeister.relateTo(p6, 'shouted') + await jennyRostock.relateTo(p6, 'shouted') + await jennyRostock.relateTo(p7, 'shouted') + await huey.relateTo(p8, 'shouted') + await huey.relateTo(p9, 'shouted') + await louie.relateTo(p10, 'shouted') + + const reports: any[] = [] + reports.push( + await Factory.build('report'), + await Factory.build('report'), + await Factory.build('report'), + await Factory.build('report'), + ) const reportAgainstDagobert = reports[0] const reportAgainstTrollingPost = reports[1] const reportAgainstTrollingComment = reports[2] const reportAgainstDewey = reports[3] // report resource first time - await reportAgainstDagobert.relateTo(jennyRostock, 'filed', { resourceId: 'u7', reasonCategory: 'discrimination_etc', @@ -1139,27 +1088,25 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await reportAgainstDewey.relateTo(dewey, 'belongsTo') // report resource a second time - await Promise.all([ - reportAgainstDagobert.relateTo(louie, 'filed', { - resourceId: 'u7', - reasonCategory: 'discrimination_etc', - reasonDescription: 'this user is attacking me for who I am!', - }), - reportAgainstDagobert.relateTo(dagobert, 'belongsTo'), - reportAgainstTrollingPost.relateTo(peterLustig, 'filed', { - resourceId: 'p2', - reasonCategory: 'discrimination_etc', - reasonDescription: 'This post is bigoted', - }), - reportAgainstTrollingPost.relateTo(p2, 'belongsTo'), + await reportAgainstDagobert.relateTo(louie, 'filed', { + resourceId: 'u7', + reasonCategory: 'discrimination_etc', + reasonDescription: 'this user is attacking me for who I am!', + }) + await reportAgainstDagobert.relateTo(dagobert, 'belongsTo') + await reportAgainstTrollingPost.relateTo(peterLustig, 'filed', { + resourceId: 'p2', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', + }) + await reportAgainstTrollingPost.relateTo(p2, 'belongsTo') - reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'filed', { - resourceId: 'c1', - reasonCategory: 'pornographic_content_links', - reasonDescription: 'This comment is porno!!!', - }), - reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), - ]) + await reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'filed', { + resourceId: 'c1', + reasonCategory: 'pornographic_content_links', + reasonDescription: 'This comment is porno!!!', + }) + await reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo') const disableVariables = { resourceId: 'undefined-resource', @@ -1168,409 +1115,451 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] } // review resource first time - await Promise.all([ - reportAgainstDagobert.relateTo(bobDerBaumeister, 'reviewed', { - ...disableVariables, - resourceId: 'u7', - }), - dagobert.update({ disabled: true, updatedAt: new Date().toISOString() }), - reportAgainstTrollingPost.relateTo(peterLustig, 'reviewed', { - ...disableVariables, - resourceId: 'p2', - }), - p2.update({ disabled: true, updatedAt: new Date().toISOString() }), - reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'reviewed', { - ...disableVariables, - resourceId: 'c1', - }), - trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }), - ]) + await reportAgainstDagobert.relateTo(bobDerBaumeister, 'reviewed', { + ...disableVariables, + resourceId: 'u7', + }) + await dagobert.update({ disabled: true, updatedAt: new Date().toISOString() }) + await reportAgainstTrollingPost.relateTo(peterLustig, 'reviewed', { + ...disableVariables, + resourceId: 'p2', + }) + await p2.update({ disabled: true, updatedAt: new Date().toISOString() }) + await reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'reviewed', { + ...disableVariables, + resourceId: 'c1', + }) + await trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }) // second review of resource and close report - await Promise.all([ - reportAgainstDagobert.relateTo(peterLustig, 'reviewed', { - resourceId: 'u7', - disable: false, - closed: true, - }), - dagobert.update({ disabled: false, updatedAt: new Date().toISOString(), closed: true }), - reportAgainstTrollingPost.relateTo(bobDerBaumeister, 'reviewed', { - resourceId: 'p2', - disable: true, - closed: true, - }), - p2.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }), - reportAgainstTrollingComment.relateTo(peterLustig, 'reviewed', { - ...disableVariables, - resourceId: 'c1', - disable: true, - closed: true, - }), - trollingComment.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }), - ]) + await reportAgainstDagobert.relateTo(peterLustig, 'reviewed', { + resourceId: 'u7', + disable: false, + closed: true, + }) + await dagobert.update({ disabled: false, updatedAt: new Date().toISOString(), closed: true }) + await reportAgainstTrollingPost.relateTo(bobDerBaumeister, 'reviewed', { + resourceId: 'p2', + disable: true, + closed: true, + }) + await p2.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }) + await reportAgainstTrollingComment.relateTo(peterLustig, 'reviewed', { + ...disableVariables, + resourceId: 'c1', + disable: true, + closed: true, + }) + await trollingComment.update({ + disabled: true, + updatedAt: new Date().toISOString(), + closed: true, + }) - const additionalUsers = await Promise.all( - [...Array(30).keys()].map(() => Factory.build('user')), - ) + const additionalUsers: any[] = [] + for (let i = 0; i < 30; i++) { + const user = await Factory.build('user') + await jennyRostock.relateTo(user, 'following') + await user.relateTo(jennyRostock, 'following') + additionalUsers.push(user) + } - await Promise.all( - additionalUsers.map(async (user) => { - await jennyRostock.relateTo(user, 'following') - await user.relateTo(jennyRostock, 'following') - }), - ) + // Jenny users + for (let i = 0; i < 30; i++) { + await Factory.build('user', { name: `Jenny${i}` }) + } - await Promise.all( - [...Array(30).keys()].map((index) => Factory.build('user', { name: `Jenny${index}` })), - ) + // Jenny posts + for (let i = 0; i < 30; i++) { + await Factory.build( + 'post', + { content: `Jenny ${faker.lorem.sentence()}` }, + { + categoryIds: ['cat1'], + author: jennyRostock, + image: Factory.build('image', { + url: faker.image.unsplash.objects(), + }), + }, + ) + } - await Promise.all( - [...Array(30).keys()].map(() => - Factory.build( - 'post', - { content: `Jenny ${faker.lorem.sentence()}` }, - { - categoryIds: ['cat1'], - author: jennyRostock, - image: Factory.build('image', { - url: faker.image.unsplash.objects(), - }), - }, - ), - ), - ) + // comments on p2 jenny + for (let i = 0; i < 6; i++) { + await Factory.build( + 'comment', + {}, + { + author: jennyRostock, + postId: 'p2', + }, + ) + } - await Promise.all( - [...Array(30).keys()].map(() => - Factory.build( - 'post', - {}, - { - categoryIds: ['cat1'], - author: jennyRostock, - image: Factory.build('image', { - url: faker.image.unsplash.objects(), - }), - }, - ), - ), - ) + // comments on p15 jenny + for (let i = 0; i < 4; i++) { + await Factory.build( + 'comment', + {}, + { + author: jennyRostock, + postId: 'p15', + }, + ) + } - await Promise.all( - [...Array(6).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: jennyRostock, - postId: 'p2', - }, - ), - ), - ) + // comments on p4 jenny + for (let i = 0; i < 2; i++) { + await Factory.build( + 'comment', + {}, + { + author: jennyRostock, + postId: 'p4', + }, + ) + } - await Promise.all( - [...Array(4).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: jennyRostock, - postId: 'p15', - }, - ), - ), - ) + // Posts Peter Lustig + for (let i = 0; i < 21; i++) { + await Factory.build( + 'post', + {}, + { + categoryIds: ['cat1'], + author: peterLustig, + image: Factory.build('image', { + url: faker.image.unsplash.buildings(), + }), + }, + ) + } - await Promise.all( - [...Array(2).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: jennyRostock, - postId: 'p4', - }, - ), - ), - ) + // comments p4 peter + for (let i = 0; i < 3; i++) { + await Factory.build( + 'comment', + {}, + { + author: peterLustig, + postId: 'p4', + }, + ) + } - await Promise.all( - [...Array(21).keys()].map(() => - Factory.build( - 'post', - {}, - { - categoryIds: ['cat1'], - author: peterLustig, - image: Factory.build('image', { - url: faker.image.unsplash.buildings(), - }), - }, - ), - ), - ) + // comments p14 peter + for (let i = 0; i < 3; i++) { + await Factory.build( + 'comment', + {}, + { + author: peterLustig, + postId: 'p14', + }, + ) + } - await Promise.all( - [...Array(3).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: peterLustig, - postId: 'p4', - }, - ), - ), - ) + // comments p0 peter + for (let i = 0; i < 3; i++) { + await Factory.build( + 'comment', + {}, + { + author: peterLustig, + postId: 'p0', + }, + ) + } - await Promise.all( - [...Array(3).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: peterLustig, - postId: 'p14', - }, - ), - ), - ) + // Posts dewey + for (let i = 0; i < 11; i++) { + await Factory.build( + 'post', + {}, + { + categoryIds: ['cat1'], + author: dewey, + image: Factory.build('image', { + url: faker.image.unsplash.food(), + }), + }, + ) + } - await Promise.all( - [...Array(6).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: peterLustig, - postId: 'p0', - }, - ), - ), - ) + // Comments p2 dewey + for (let i = 0; i < 7; i++) { + await Factory.build( + 'comment', + {}, + { + author: dewey, + postId: 'p2', + }, + ) + } - await Promise.all( - [...Array(11).keys()].map(() => - Factory.build( - 'post', - {}, - { - categoryIds: ['cat1'], - author: dewey, - image: Factory.build('image', { - url: faker.image.unsplash.food(), - }), - }, - ), - ), - ) + // Comments p6 dewey + for (let i = 0; i < 5; i++) { + await Factory.build( + 'comment', + {}, + { + author: dewey, + postId: 'p6', + }, + ) + } - await Promise.all( - [...Array(7).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: dewey, - postId: 'p2', - }, - ), - ), - ) + // Comments p9 dewey + for (let i = 0; i < 2; i++) { + await Factory.build( + 'comment', + {}, + { + author: dewey, + postId: 'p9', + }, + ) + } - await Promise.all( - [...Array(5).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: dewey, - postId: 'p6', - }, - ), - ), - ) + // Posts louie + for (let i = 0; i < 16; i++) { + await Factory.build( + 'post', + {}, + { + categoryIds: ['cat1'], + author: louie, + image: Factory.build('image', { + url: faker.image.unsplash.technology(), + }), + }, + ) + } - await Promise.all( - [...Array(2).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: dewey, - postId: 'p9', - }, - ), - ), - ) + // Comments p1 louie + for (let i = 0; i < 4; i++) { + await Factory.build( + 'comment', + {}, + { + postId: 'p1', + author: louie, + }, + ) + } - await Promise.all( - [...Array(16).keys()].map(() => - Factory.build( - 'post', - {}, - { - categoryIds: ['cat1'], - author: louie, - image: Factory.build('image', { - url: faker.image.unsplash.technology(), - }), - }, - ), - ), - ) + // Comments p10 louie + for (let i = 0; i < 8; i++) { + await Factory.build( + 'comment', + {}, + { + author: louie, + postId: 'p10', + }, + ) + } - await Promise.all( - [...Array(4).keys()].map(() => - Factory.build( - 'comment', - {}, - { - postId: 'p1', - author: louie, - }, - ), - ), - ) + // Comments p13 louie + for (let i = 0; i < 5; i++) { + await Factory.build( + 'comment', + {}, + { + author: louie, + postId: 'p13', + }, + ) + } - await Promise.all( - [...Array(8).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: louie, - postId: 'p10', - }, - ), - ), - ) + // Posts Bob der Baumeister + for (let i = 0; i < 45; i++) { + await Factory.build( + 'post', + {}, + { + categoryIds: ['cat1'], + author: bobDerBaumeister, + image: Factory.build('image', { + url: faker.image.unsplash.people(), + }), + }, + ) + } - await Promise.all( - [...Array(5).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: louie, - postId: 'p13', - }, - ), - ), - ) + // Comments p2 bob + for (let i = 0; i < 2; i++) { + await Factory.build( + 'comment', + {}, + { + author: bobDerBaumeister, + postId: 'p2', + }, + ) + } - await Promise.all( - [...Array(45).keys()].map(() => - Factory.build( - 'post', - {}, - { - categoryIds: ['cat1'], - author: bobDerBaumeister, - image: Factory.build('image', { - url: faker.image.unsplash.people(), - }), - }, - ), - ), - ) + // Comments p12 bob + for (let i = 0; i < 3; i++) { + await Factory.build( + 'comment', + {}, + { + author: bobDerBaumeister, + postId: 'p12', + }, + ) + } - await Promise.all( - [...Array(2).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: bobDerBaumeister, - postId: 'p2', - }, - ), - ), - ) + // Comments p13 bob + for (let i = 0; i < 7; i++) { + await Factory.build( + 'comment', + {}, + { + author: bobDerBaumeister, + postId: 'p13', + }, + ) + } - await Promise.all( - [...Array(3).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: bobDerBaumeister, - postId: 'p12', - }, - ), - ), - ) + // Posts huey + for (let i = 0; i < 8; i++) { + await Factory.build( + 'post', + {}, + { + categoryIds: ['cat1'], + author: huey, + image: Factory.build('image', { + url: faker.image.unsplash.nature(), + }), + }, + ) + } - await Promise.all( - [...Array(7).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: bobDerBaumeister, - postId: 'p13', - }, - ), - ), - ) + // Comments p0 huey + for (let i = 0; i < 6; i++) { + await Factory.build( + 'comment', + {}, + { + author: huey, + postId: 'p0', + }, + ) + } - await Promise.all( - [...Array(8).keys()].map(() => - Factory.build( - 'post', - {}, - { - categoryIds: ['cat1'], - author: huey, - image: Factory.build('image', { - url: faker.image.unsplash.nature(), - }), - }, - ), - ), - ) + // Comments p13 huey + for (let i = 0; i < 8; i++) { + await Factory.build( + 'comment', + {}, + { + author: huey, + postId: 'p13', + }, + ) + } - await Promise.all( - [...Array(6).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: huey, - postId: 'p0', - }, - ), - ), - ) - - await Promise.all( - [...Array(8).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: huey, - postId: 'p13', - }, - ), - ), - ) - - await Promise.all( - [...Array(8).keys()].map(() => - Factory.build( - 'comment', - {}, - { - author: huey, - postId: 'p15', - }, - ), - ), - ) + // Comments p15 huey + for (let i = 0; i < 8; i++) { + await Factory.build( + 'comment', + {}, + { + author: huey, + postId: 'p15', + }, + ) + } await Factory.build('donations') + + // Chat + authenticatedUser = await huey.toJson() + const { data: roomHueyPeter } = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: (await peterLustig.toJson()).id, + }, + }) + + for (let i = 0; i < 30; i++) { + authenticatedUser = await huey.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: roomHueyPeter?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + authenticatedUser = await peterLustig.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: roomHueyPeter?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + } + + authenticatedUser = await huey.toJson() + const { data: roomHueyJenny } = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: (await jennyRostock.toJson()).id, + }, + }) + for (let i = 0; i < 1000; i++) { + authenticatedUser = await huey.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: roomHueyJenny?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + authenticatedUser = await jennyRostock.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: roomHueyJenny?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + } + + for (const user of additionalUsers) { + authenticatedUser = await jennyRostock.toJson() + const { data: room } = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: (await user.toJson()).id, + }, + }) + + for (let i = 0; i < 29; i++) { + authenticatedUser = await jennyRostock.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: room?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + authenticatedUser = await user.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: room?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + } + } + /* eslint-disable-next-line no-console */ console.log('Seeded Data...') await driver.close() diff --git a/backend/src/graphql/messages.ts b/backend/src/graphql/messages.ts index 4d2220f18..2842c7230 100644 --- a/backend/src/graphql/messages.ts +++ b/backend/src/graphql/messages.ts @@ -6,6 +6,13 @@ export const createMessageMutation = () => { CreateMessage(roomId: $roomId, content: $content) { id content + senderId + username + avatar + date + saved + distributed + seen } } ` @@ -13,16 +20,31 @@ export const createMessageMutation = () => { export const messageQuery = () => { return gql` - query ($roomId: ID!) { - Message(roomId: $roomId) { + query ($roomId: ID!, $first: Int, $offset: Int) { + Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) { _id id + indexId content senderId + author { + id + } username avatar date + saved + distributed + seen } } ` } + +export const markMessagesAsSeen = () => { + return gql` + mutation ($messageIds: [String!]) { + MarkMessagesAsSeen(messageIds: $messageIds) + } + ` +} diff --git a/backend/src/graphql/rooms.ts b/backend/src/graphql/rooms.ts index 109bf1d55..7612641f3 100644 --- a/backend/src/graphql/rooms.ts +++ b/backend/src/graphql/rooms.ts @@ -6,18 +6,10 @@ export const createRoomMutation = () => { CreateRoom(userId: $userId) { id roomId - } - } - ` -} - -export const roomQuery = () => { - return gql` - query { - Room { - id - roomId roomName + lastMessageAt + unreadCount + #avatar users { _id id @@ -30,3 +22,46 @@ export const roomQuery = () => { } ` } + +export const roomQuery = () => { + return gql` + query Room($first: Int, $offset: Int, $id: ID) { + Room(first: $first, offset: $offset, id: $id, orderBy: lastMessageAt_desc) { + id + roomId + roomName + avatar + lastMessageAt + unreadCount + lastMessage { + _id + id + content + senderId + username + avatar + date + saved + distributed + seen + } + users { + _id + id + name + avatar { + url + } + } + } + } + ` +} + +export const unreadRoomsQuery = () => { + return gql` + query { + UnreadRooms + } + ` +} diff --git a/backend/src/middleware/chatMiddleware.ts b/backend/src/middleware/chatMiddleware.ts new file mode 100644 index 000000000..8ae252e13 --- /dev/null +++ b/backend/src/middleware/chatMiddleware.ts @@ -0,0 +1,60 @@ +import { isArray } from 'lodash' + +const setRoomProps = (room) => { + if (room.users) { + room.users.forEach((user) => { + user._id = user.id + }) + } + if (room.lastMessage) { + room.lastMessage._id = room.lastMessage.id + } +} + +const setMessageProps = (message, context) => { + message._id = message.id + if (message.senderId !== context.user.id) { + message.distributed = true + } +} + +const roomProperties = async (resolve, root, args, context, info) => { + const resolved = await resolve(root, args, context, info) + if (resolved) { + if (isArray(resolved)) { + resolved.forEach((room) => { + setRoomProps(room) + }) + } else { + setRoomProps(resolved) + } + } + return resolved +} + +const messageProperties = async (resolve, root, args, context, info) => { + const resolved = await resolve(root, args, context, info) + if (resolved) { + if (isArray(resolved)) { + resolved.forEach((message) => { + setMessageProps(message, context) + }) + } else { + setMessageProps(resolved, context) + } + } + return resolved +} + +export default { + Query: { + Room: roomProperties, + Message: messageProperties, + }, + Mutation: { + CreateRoom: roomProperties, + }, + Subscription: { + chatMessageAdded: messageProperties, + }, +} diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 813bbe9a7..08c872db7 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -14,6 +14,7 @@ import login from './login/loginMiddleware' import sentry from './sentryMiddleware' import languages from './languages/languages' import userInteractions from './userInteractions' +import chatMiddleware from './chatMiddleware' export default (schema) => { const middlewares = { @@ -31,6 +32,7 @@ export default (schema) => { orderBy, languages, userInteractions, + chatMiddleware, } let order = [ @@ -49,6 +51,7 @@ export default (schema) => { 'softDelete', 'includedFields', 'orderBy', + 'chatMiddleware', ] // add permisions middleware at the first position (unless we're seeding) diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 81ba93e3c..f87f4b079 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -408,6 +408,7 @@ export default shield( getInviteCode: isAuthenticated, // and inviteRegistration Room: isAuthenticated, Message: isAuthenticated, + UnreadRooms: isAuthenticated, }, Mutation: { '*': deny, @@ -463,6 +464,7 @@ export default shield( saveCategorySettings: isAuthenticated, CreateRoom: isAuthenticated, CreateMessage: isAuthenticated, + MarkMessagesAsSeen: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index a43bd3226..83d9fdc6b 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -1,13 +1,15 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' import { getNeode, getDriver } from '../../db/neo4j' -import { createRoomMutation } from '../../graphql/rooms' -import { createMessageMutation, messageQuery } from '../../graphql/messages' -import createServer from '../../server' +import { createRoomMutation, roomQuery } from '../../graphql/rooms' +import { createMessageMutation, messageQuery, markMessagesAsSeen } from '../../graphql/messages' +import createServer, { pubsub } from '../../server' const driver = getDriver() const neode = getNeode() +const pubsubSpy = jest.spyOn(pubsub, 'publish') + let query let mutate let authenticatedUser @@ -22,6 +24,9 @@ beforeAll(async () => { driver, neode, user: authenticatedUser, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, } }, }) @@ -55,6 +60,10 @@ describe('Message', () => { }) describe('create message', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + describe('unauthenticated', () => { it('throws authorization error', async () => { await expect( @@ -77,7 +86,7 @@ describe('Message', () => { }) describe('room does not exist', () => { - it('returns null', async () => { + it('returns null and does not publish subscription', async () => { await expect( mutate({ mutation: createMessageMutation(), @@ -92,6 +101,7 @@ describe('Message', () => { CreateMessage: null, }, }) + expect(pubsubSpy).not.toBeCalled() }) }) @@ -107,7 +117,7 @@ describe('Message', () => { }) describe('user chats in room', () => { - it('returns the message', async () => { + it('returns the message and publishes subscriptions', async () => { await expect( mutate({ mutation: createMessageMutation(), @@ -122,9 +132,92 @@ describe('Message', () => { CreateMessage: { id: expect.any(String), content: 'Some nice message to other chatting user', + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, }, }, }) + expect(pubsubSpy).toBeCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'other-chatting-user', + }) + expect(pubsubSpy).toBeCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to other chatting user', + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'other-chatting-user', + }) + }) + + describe('room is updated as well', () => { + it('has last message set', async () => { + const result = await query({ query: roomQuery() }) + await expect(result).toMatchObject({ + errors: undefined, + data: { + Room: [ + expect.objectContaining({ + lastMessageAt: expect.any(String), + unreadCount: 0, + lastMessage: expect.objectContaining({ + _id: result.data.Room[0].lastMessage.id, + id: expect.any(String), + content: 'Some nice message to other chatting user', + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + }), + ], + }, + }) + }) + }) + + describe('unread count for other user', () => { + it('has unread count = 1', async () => { + authenticatedUser = await otherChattingUser.toJson() + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: undefined, + data: { + Room: [ + expect.objectContaining({ + lastMessageAt: expect.any(String), + unreadCount: 1, + lastMessage: expect.objectContaining({ + _id: expect.any(String), + id: expect.any(String), + content: 'Some nice message to other chatting user', + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + }), + ], + }, + }) + }) }) }) @@ -212,11 +305,15 @@ describe('Message', () => { { id: expect.any(String), _id: result.data.Message[0].id, + indexId: 0, content: 'Some nice message to other chatting user', senderId: 'chatting-user', username: 'Chatting User', avatar: expect.any(String), date: expect.any(String), + saved: true, + distributed: true, + seen: false, }, ], }, @@ -253,17 +350,65 @@ describe('Message', () => { ).resolves.toMatchObject({ errors: undefined, data: { - Message: expect.arrayContaining([ + Message: [ expect.objectContaining({ id: expect.any(String), + indexId: 0, content: 'Some nice message to other chatting user', senderId: 'chatting-user', username: 'Chatting User', avatar: expect.any(String), date: expect.any(String), + saved: true, + distributed: true, + seen: false, }), expect.objectContaining({ id: expect.any(String), + indexId: 1, + content: 'A nice response message to chatting user', + senderId: 'other-chatting-user', + username: 'Other Chatting User', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: true, + seen: false, + }), + expect.objectContaining({ + id: expect.any(String), + indexId: 2, + content: 'And another nice message to other chatting user', + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + ], + }, + }) + }) + + it('returns the messages paginated', async () => { + await expect( + query({ + query: messageQuery(), + variables: { + roomId, + first: 2, + offset: 0, + }, + }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [ + expect.objectContaining({ + id: expect.any(String), + indexId: 1, content: 'A nice response message to chatting user', senderId: 'other-chatting-user', username: 'Other Chatting User', @@ -272,13 +417,40 @@ describe('Message', () => { }), expect.objectContaining({ id: expect.any(String), + indexId: 2, content: 'And another nice message to other chatting user', senderId: 'chatting-user', username: 'Chatting User', avatar: expect.any(String), date: expect.any(String), }), - ]), + ], + }, + }) + + await expect( + query({ + query: messageQuery(), + variables: { + roomId, + first: 2, + offset: 2, + }, + }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [ + expect.objectContaining({ + id: expect.any(String), + indexId: 0, + content: 'Some nice message to other chatting user', + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), + }), + ], }, }) }) @@ -308,4 +480,74 @@ describe('Message', () => { }) }) }) + + describe('marks massges as seen', () => { + describe('unauthenticated', () => { + beforeAll(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect( + mutate({ + mutation: markMessagesAsSeen(), + variables: { + messageIds: ['some-id'], + }, + }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + const messageIds: string[] = [] + beforeAll(async () => { + authenticatedUser = await otherChattingUser.toJson() + const msgs = await query({ + query: messageQuery(), + variables: { + roomId, + }, + }) + msgs.data.Message.forEach((m) => messageIds.push(m.id)) + }) + + it('returns true', async () => { + await expect( + mutate({ + mutation: markMessagesAsSeen(), + variables: { + messageIds, + }, + }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + MarkMessagesAsSeen: true, + }, + }) + }) + + it('has seen prop set to true', async () => { + await expect( + query({ + query: messageQuery(), + variables: { + roomId, + }, + }), + ).resolves.toMatchObject({ + data: { + Message: [ + expect.objectContaining({ seen: true }), + expect.objectContaining({ seen: false }), + expect.objectContaining({ seen: true }), + ], + }, + }) + }) + }) + }) }) diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index b93cffe06..078584c9d 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -1,7 +1,36 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import Resolver from './helpers/Resolver' +import { getUnreadRoomsCount } from './rooms' +import { pubsub, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '../../server' +import { withFilter } from 'graphql-subscriptions' + +const setMessagesAsDistributed = async (undistributedMessagesIds, session) => { + return session.writeTransaction(async (transaction) => { + const setDistributedCypher = ` + MATCH (m:Message) WHERE m.id IN $undistributedMessagesIds + SET m.distributed = true + RETURN m { .* } + ` + const setDistributedTxResponse = await transaction.run(setDistributedCypher, { + undistributedMessagesIds, + }) + const messages = await setDistributedTxResponse.records.map((record) => record.get('m')) + return messages + }) +} + export default { + Subscription: { + chatMessageAdded: { + subscribe: withFilter( + () => pubsub.asyncIterator(CHAT_MESSAGE_ADDED), + (payload, variables) => { + return payload.userId === variables.userId + }, + ), + }, + }, Query: { Message: async (object, params, context, resolveInfo) => { const { roomId } = params @@ -13,13 +42,24 @@ export default { id: context.user.id, }, } + const resolved = await neo4jgraphql(object, params, context, resolveInfo) + if (resolved) { - resolved.forEach((message) => { - message._id = message.id - }) + const undistributedMessagesIds = resolved + .filter((msg) => !msg.distributed && msg.senderId !== context.user.id) + .map((msg) => msg.id) + const session = context.driver.session() + try { + if (undistributedMessagesIds.length > 0) { + await setMessagesAsDistributed(undistributedMessagesIds, session) + } + } finally { + session.close() + } + // send subscription to author to updated the messages } - return resolved + return resolved.reverse() }, }, Mutation: { @@ -32,25 +72,59 @@ export default { const writeTxResultPromise = session.writeTransaction(async (transaction) => { const createMessageCypher = ` MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) + OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image) + OPTIONAL MATCH (m:Message)-[:INSIDE]->(room) + OPTIONAL MATCH (room)<-[:CHATS_IN]-(recipientUser:User) + WHERE NOT recipientUser.id = $currentUserId + WITH MAX(m.indexId) as maxIndex, room, currentUser, image, recipientUser CREATE (currentUser)-[:CREATED]->(message:Message { createdAt: toString(datetime()), id: apoc.create.uuid(), - content: $content + indexId: CASE WHEN maxIndex IS NOT NULL THEN maxIndex + 1 ELSE 0 END, + content: $content, + saved: true, + distributed: false, + seen: false })-[:INSIDE]->(room) - RETURN message { .* } + SET room.lastMessageAt = toString(datetime()) + RETURN message { + .*, + indexId: toString(message.indexId), + recipientId: recipientUser.id, + senderId: currentUser.id, + username: currentUser.name, + avatar: image.url, + date: message.createdAt + } ` const createMessageTxResponse = await transaction.run(createMessageCypher, { currentUserId, roomId, content, }) + const [message] = await createMessageTxResponse.records.map((record) => record.get('message'), ) + return message }) try { const message = await writeTxResultPromise + if (message) { + const roomCountUpdated = await getUnreadRoomsCount(message.recipientId, session) + + // send subscriptions + void pubsub.publish(ROOM_COUNT_UPDATED, { + roomCountUpdated, + userId: message.recipientId, + }) + void pubsub.publish(CHAT_MESSAGE_ADDED, { + chatMessageAdded: message, + userId: message.recipientId, + }) + } + return message } catch (error) { throw new Error(error) @@ -58,6 +132,32 @@ export default { session.close() } }, + MarkMessagesAsSeen: async (_parent, params, context, _resolveInfo) => { + const { messageIds } = params + const currentUserId = context.user.id + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const setSeenCypher = ` + MATCH (m:Message)<-[:CREATED]-(user:User) + WHERE m.id IN $messageIds AND NOT user.id = $currentUserId + SET m.seen = true + RETURN m { .* } + ` + const setSeenTxResponse = await transaction.run(setSeenCypher, { + messageIds, + currentUserId, + }) + const messages = await setSeenTxResponse.records.map((record) => record.get('m')) + return messages + }) + try { + await writeTxResultPromise + // send subscription to author to updated the messages + return true + } finally { + session.close() + } + }, }, Message: { ...Resolver('Message', { diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 03c3d4456..2e26dc1e3 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -1,7 +1,8 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' import { getNeode, getDriver } from '../../db/neo4j' -import { createRoomMutation, roomQuery } from '../../graphql/rooms' +import { createRoomMutation, roomQuery, unreadRoomsQuery } from '../../graphql/rooms' +import { createMessageMutation } from '../../graphql/messages' import createServer from '../../server' const driver = getDriver() @@ -21,6 +22,9 @@ beforeAll(async () => { driver, neode, user: authenticatedUser, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, } }, }) @@ -34,6 +38,8 @@ afterAll(async () => { }) describe('Room', () => { + let roomId: string + beforeAll(async () => { ;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ Factory.build('user', { @@ -48,6 +54,14 @@ describe('Room', () => { id: 'not-chatting-user', name: 'Not Chatting User', }), + Factory.build('user', { + id: 'second-chatting-user', + name: 'Second Chatting User', + }), + Factory.build('user', { + id: 'third-chatting-user', + name: 'Third Chatting User', + }), ]) }) @@ -68,8 +82,6 @@ describe('Room', () => { }) describe('authenticated', () => { - let roomId: string - beforeAll(async () => { authenticatedUser = await chattingUser.toJson() }) @@ -122,6 +134,26 @@ describe('Room', () => { CreateRoom: { id: expect.any(String), roomId: result.data.CreateRoom.id, + roomName: 'Other Chatting User', + unreadCount: 0, + users: expect.arrayContaining([ + { + _id: 'chatting-user', + id: 'chatting-user', + name: 'Chatting User', + avatar: { + url: expect.any(String), + }, + }, + { + _id: 'other-chatting-user', + id: 'other-chatting-user', + name: 'Other Chatting User', + avatar: { + url: expect.any(String), + }, + }, + ]), }, }, }) @@ -219,6 +251,7 @@ describe('Room', () => { id: expect.any(String), roomId: result.data.Room[0].id, roomName: 'Chatting User', + unreadCount: 0, users: expect.arrayContaining([ { _id: 'chatting-user', @@ -260,4 +293,319 @@ describe('Room', () => { }) }) }) + + describe('unread rooms query', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + let otherRoomId: string + + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + const result = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'not-chatting-user', + }, + }) + otherRoomId = result.data.CreateRoom.roomId + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: otherRoomId, + content: 'Message to not chatting user', + }, + }) + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: '1st message to other chatting user', + }, + }) + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: '2nd message to other chatting user', + }, + }) + authenticatedUser = await otherChattingUser.toJson() + const result2 = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'not-chatting-user', + }, + }) + otherRoomId = result2.data.CreateRoom.roomId + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: otherRoomId, + content: 'Other message to not chatting user', + }, + }) + }) + + describe('as chatting user', () => { + it('has 0 unread rooms', async () => { + authenticatedUser = await chattingUser.toJson() + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) + }) + + describe('as other chatting user', () => { + it('has 1 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 1, + }, + }) + }) + }) + + describe('as not chatting user', () => { + it('has 2 unread rooms', async () => { + authenticatedUser = await notChattingUser.toJson() + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 2, + }, + }) + }) + }) + }) + }) + + describe('query several rooms', () => { + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'second-chatting-user', + }, + }) + await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'third-chatting-user', + }, + }) + }) + + it('returns the rooms paginated', async () => { + await expect( + query({ query: roomQuery(), variables: { first: 3, offset: 0 } }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + Room: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + roomId: expect.any(String), + roomName: 'Third Chatting User', + lastMessageAt: null, + unreadCount: 0, + lastMessage: null, + users: expect.arrayContaining([ + expect.objectContaining({ + _id: 'chatting-user', + id: 'chatting-user', + name: 'Chatting User', + avatar: { + url: expect.any(String), + }, + }), + expect.objectContaining({ + _id: 'third-chatting-user', + id: 'third-chatting-user', + name: 'Third Chatting User', + avatar: { + url: expect.any(String), + }, + }), + ]), + }), + expect.objectContaining({ + id: expect.any(String), + roomId: expect.any(String), + roomName: 'Second Chatting User', + lastMessageAt: null, + unreadCount: 0, + lastMessage: null, + users: expect.arrayContaining([ + expect.objectContaining({ + _id: 'chatting-user', + id: 'chatting-user', + name: 'Chatting User', + avatar: { + url: expect.any(String), + }, + }), + expect.objectContaining({ + _id: 'second-chatting-user', + id: 'second-chatting-user', + name: 'Second Chatting User', + avatar: { + url: expect.any(String), + }, + }), + ]), + }), + expect.objectContaining({ + id: expect.any(String), + roomId: expect.any(String), + roomName: 'Other Chatting User', + lastMessageAt: expect.any(String), + unreadCount: 0, + lastMessage: { + _id: expect.any(String), + id: expect.any(String), + content: '2nd message to other chatting user', + senderId: 'chatting-user', + username: 'Chatting User', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }, + users: expect.arrayContaining([ + expect.objectContaining({ + _id: 'chatting-user', + id: 'chatting-user', + name: 'Chatting User', + avatar: { + url: expect.any(String), + }, + }), + expect.objectContaining({ + _id: 'other-chatting-user', + id: 'other-chatting-user', + name: 'Other Chatting User', + avatar: { + url: expect.any(String), + }, + }), + ]), + }), + ]), + }, + }) + await expect( + query({ query: roomQuery(), variables: { first: 3, offset: 3 } }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + Room: [ + expect.objectContaining({ + id: expect.any(String), + roomId: expect.any(String), + roomName: 'Not Chatting User', + users: expect.arrayContaining([ + { + _id: 'chatting-user', + id: 'chatting-user', + name: 'Chatting User', + avatar: { + url: expect.any(String), + }, + }, + { + _id: 'not-chatting-user', + id: 'not-chatting-user', + name: 'Not Chatting User', + avatar: { + url: expect.any(String), + }, + }, + ]), + }), + ], + }, + }) + }) + }) + + describe('query single room', () => { + let result: any = null + + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + result = await query({ query: roomQuery() }) + }) + + describe('as chatter of room', () => { + it('returns the room', async () => { + expect( + await query({ + query: roomQuery(), + variables: { first: 2, offset: 0, id: result.data.Room[0].id }, + }), + ).toMatchObject({ + errors: undefined, + data: { + Room: [ + { + id: expect.any(String), + roomId: expect.any(String), + roomName: result.data.Room[0].roomName, + users: expect.any(Array), + }, + ], + }, + }) + }) + + describe('as not chatter of room', () => { + beforeAll(async () => { + authenticatedUser = await notChattingUser.toJson() + }) + + it('returns no room', async () => { + authenticatedUser = await notChattingUser.toJson() + expect( + await query({ + query: roomQuery(), + variables: { first: 2, offset: 0, id: result.data.Room[0].id }, + }), + ).toMatchObject({ + errors: undefined, + data: { + Room: [], + }, + }) + }) + }) + }) + }) }) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index d5015a03b..5e931a446 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,29 +1,50 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import Resolver from './helpers/Resolver' +import { pubsub, ROOM_COUNT_UPDATED } from '../../server' +import { withFilter } from 'graphql-subscriptions' + +export const getUnreadRoomsCount = async (userId, session) => { + return session.readTransaction(async (transaction) => { + const unreadRoomsCypher = ` + MATCH (:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) + WHERE NOT sender.id = $userId AND NOT message.seen + RETURN toString(COUNT(DISTINCT room)) AS count + ` + const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId }) + return unreadRoomsTxResponse.records.map((record) => record.get('count'))[0] + }) +} export default { + Subscription: { + roomCountUpdated: { + subscribe: withFilter( + () => pubsub.asyncIterator(ROOM_COUNT_UPDATED), + (payload, variables) => { + return payload.userId === variables.userId + }, + ), + }, + }, Query: { Room: async (object, params, context, resolveInfo) => { if (!params.filter) params.filter = {} params.filter.users_some = { id: context.user.id, } - const resolved = await neo4jgraphql(object, params, context, resolveInfo) - if (resolved) { - resolved.forEach((room) => { - if (room.users) { - // buggy, you must query the username for this to function correctly - room.roomName = room.users.filter((user) => user.id !== context.user.id)[0].name - room.avatar = - room.users.filter((user) => user.id !== context.user.id)[0].avatar?.url || - 'default-avatar' - room.users.forEach((user) => { - user._id = user.id - }) - } - }) + return neo4jgraphql(object, params, context, resolveInfo) + }, + UnreadRooms: async (object, params, context, resolveInfo) => { + const { + user: { id: currentUserId }, + } = context + const session = context.driver.session() + try { + const count = await getUnreadRoomsCount(currentUserId, session) + return count + } finally { + session.close() } - return resolved }, }, Mutation: { @@ -44,7 +65,17 @@ export default { ON CREATE SET room.createdAt = toString(datetime()), room.id = apoc.create.uuid() - RETURN room { .* } + WITH room, user, currentUser + OPTIONAL MATCH (room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) + WHERE NOT sender.id = $currentUserId AND NOT message.seen + WITH room, user, currentUser, message, + user.name AS roomName + RETURN room { + .*, + users: [properties(currentUser), properties(user)], + roomName: roomName, + unreadCount: toString(COUNT(DISTINCT message)) + } ` const createRommTxResponse = await transaction.run(createRoomCypher, { userId, @@ -68,6 +99,7 @@ export default { }, Room: { ...Resolver('Room', { + undefinedToNull: ['lastMessageAt'], hasMany: { users: '<-[:CHATS_IN]-(related:User)', }, diff --git a/backend/src/schema/types/type/Location.gql b/backend/src/schema/types/type/Location.gql index fad24cc26..9cb5c970a 100644 --- a/backend/src/schema/types/type/Location.gql +++ b/backend/src/schema/types/type/Location.gql @@ -25,4 +25,3 @@ type LocationMapBox { type Query { queryLocations(place: String!, lang: String!): [LocationMapBox]! } - diff --git a/backend/src/schema/types/type/Message.gql b/backend/src/schema/types/type/Message.gql index 4a3346079..71d175e1c 100644 --- a/backend/src/schema/types/type/Message.gql +++ b/backend/src/schema/types/type/Message.gql @@ -2,8 +2,13 @@ # room: _RoomFilter # } +enum _MessageOrdering { + indexId_desc +} + type Message { id: ID! + indexId: Int! createdAt: String updatedAt: String @@ -16,6 +21,10 @@ type Message { username: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.name") avatar: String @cypher(statement: "MATCH (this)<-[:CREATED]-(:User)-[:AVATAR_IMAGE]->(image:Image) RETURN image.url") date: String! @cypher(statement: "RETURN this.createdAt") + + saved: Boolean + distributed: Boolean + seen: Boolean } type Mutation { @@ -23,8 +32,19 @@ type Mutation { roomId: ID! content: String! ): Message + + MarkMessagesAsSeen(messageIds: [String!]): Boolean } type Query { - Message(roomId: ID!): [Message] + Message( + roomId: ID!, + first: Int + offset: Int + orderBy: [_MessageOrdering] + ): [Message] +} + +type Subscription { + chatMessageAdded(userId: ID!): Message } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 0a7277515..7e6d1d0e7 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -84,8 +84,8 @@ input _PostFilter { group: _GroupFilter postsInMyGroups: Boolean postType_in: [PostType] - eventStart_gte: String - eventEnd_gte: String + eventStart_gte: String + eventEnd_gte: String } enum _PostOrdering { diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index 2ce6556f6..0cf5b22c8 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -5,6 +5,11 @@ # users_some: _UserFilter # } +# TODO change this to last message date +enum _RoomOrdering { + lastMessageAt_desc +} + type Room { id: ID! createdAt: String @@ -13,8 +18,28 @@ type Room { users: [User]! @relation(name: "CHATS_IN", direction: "IN") roomId: String! @cypher(statement: "RETURN this.id") - roomName: String! ## @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user[0].name") - avatar: String! ## @cypher match not own user in users array + roomName: String! @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user.name") + avatar: String @cypher(statement: """ + MATCH (this)<-[:CHATS_IN]-(user:User) + WHERE NOT user.id = $cypherParams.currentUserId + OPTIONAL MATCH (user)-[:AVATAR_IMAGE]->(image:Image) + RETURN image.url + """) + + lastMessageAt: String + + lastMessage: Message @cypher(statement: """ + MATCH (this)<-[:INSIDE]-(message:Message) + WITH message ORDER BY message.indexId DESC LIMIT 1 + RETURN message + """) + + unreadCount: Int @cypher(statement: """ + MATCH (this)<-[:INSIDE]-(message:Message)<-[:CREATED]-(user:User) + WHERE NOT user.id = $cypherParams.currentUserId + AND NOT message.seen + RETURN count(message) + """) } type Mutation { @@ -24,5 +49,13 @@ type Mutation { } type Query { - Room: [Room] + Room( + id: ID + orderBy: [_RoomOrdering] + ): [Room] + UnreadRooms: Int +} + +type Subscription { + roomCountUpdated(userId: ID!): Int } diff --git a/backend/src/server.ts b/backend/src/server.ts index b4d63c007..0522f5fc8 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -14,6 +14,8 @@ import bodyParser from 'body-parser' import { graphqlUploadExpress } from 'graphql-upload' export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED' +export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED' +export const ROOM_COUNT_UPDATED = 'ROOM_COUNT_UPDATED' const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG let prodPubsub, devPubsub const options = { diff --git a/cypress/create-cucumber-html-report.js b/cypress/create-cucumber-html-report.js new file mode 100644 index 000000000..9720f4281 --- /dev/null +++ b/cypress/create-cucumber-html-report.js @@ -0,0 +1,12 @@ +const report = require("multiple-cucumber-html-reporter"); + +const reportTitle = "Ocelot webapp end-to-end test report" + +report.generate({ + jsonDir: "reports/json_logs", + reportPath: "./reports/cucumber_html_report", + pageTitle: reportTitle, + reportName: reportTitle, + pageFooter: "
", + hideMetadata: true +}); \ No newline at end of file diff --git a/cypress/cypress.config.js b/cypress/cypress.config.js index 2d2cefc47..b1d80575e 100644 --- a/cypress/cypress.config.js +++ b/cypress/cypress.config.js @@ -21,13 +21,7 @@ async function setupNodeEvents(on, config) { return testStore[name] }, }); - - on("after:run", (results) => { - if (results) { - console.log(results.status); - } - }); - + return config; } @@ -42,10 +36,7 @@ module.exports = defineConfig({ baseUrl: "http://localhost:3000", specPattern: "cypress/e2e/**/*.feature", supportFile: "cypress/support/e2e.js", - retries: { - runMode: 2, - openMode: 0, - }, + retries: 0, video: false, setupNodeEvents, }, diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 16fa6dc01..92e8bf1f7 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -13,7 +13,6 @@ // Cypress.Commands.add('login', (email, password) => { ... }) /* globals Cypress cy */ -import "cypress-file-upload"; import { GraphQLClient, request } from 'graphql-request' import CONFIG from '../../backend/build/src/config' diff --git a/cypress/support/step_definitions/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js b/cypress/support/step_definitions/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js index 019cc956a..478851f92 100644 --- a/cypress/support/step_definitions/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js +++ b/cypress/support/step_definitions/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js @@ -1,28 +1,27 @@ import { Then } from "@badeball/cypress-cucumber-preprocessor"; Then("I should be able to {string} a teaser image", condition => { - // cy.reload() + let postTeaserImage = "" + switch(condition){ - case 'change': - cy.get('.delete-image-button') + case "change": + postTeaserImage = "humanconnection.png" + cy.get(".delete-image-button") .click() - cy.fixture('humanconnection.png').as('postTeaserImage').then(function() { - cy.get("#postdropzone").upload( - { fileContent: this.postTeaserImage, fileName: 'humanconnection.png', mimeType: "image/png" }, - { subjectType: "drag-n-drop", force: true } - ).wait(750); - }) + cy.get("#postdropzone").selectFile( + { contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" }, + { action: "drag-drop", force: true } + ).wait(750); break; - case 'add': - cy.fixture('onourjourney.png').as('postTeaserImage').then(function() { - cy.get("#postdropzone").upload( - { fileContent: this.postTeaserImage, fileName: 'onourjourney.png', mimeType: "image/png" }, - { subjectType: "drag-n-drop", force: true } - ).wait(750); - }) + case "add": + postTeaserImage = "onourjourney.png" + cy.get("#postdropzone").selectFile( + { contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" }, + { action: "drag-drop", force: true } + ).wait(750); break; - case 'remove': - cy.get('.delete-image-button') + case "remove": + cy.get(".delete-image-button") .click() break; } diff --git a/cypress/support/step_definitions/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js b/cypress/support/step_definitions/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js index 27be5a99d..b1b2401e2 100644 --- a/cypress/support/step_definitions/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js +++ b/cypress/support/step_definitions/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js @@ -2,13 +2,11 @@ import { Then } from "@badeball/cypress-cucumber-preprocessor"; Then("I should be able to change my profile picture", () => { const avatarUpload = "onourjourney.png"; - - cy.fixture(avatarUpload, "base64").then(fileContent => { - cy.get("#customdropzone").upload( - { fileContent, fileName: avatarUpload, mimeType: "image/png" }, - { subjectType: "drag-n-drop", force: true } - ); - }); + + cy.get("#customdropzone").selectFile( + { contents: `cypress/fixtures/${avatarUpload}`, fileName: avatarUpload, mimeType: "image/png" }, + { action: "drag-drop" } + ); cy.get(".profile-page-avatar img") .should("have.attr", "src") .and("contains", "onourjourney"); diff --git a/deployment/TODO-next-update.md b/deployment/TODO-next-update.md index 8630275b7..8e30d1f47 100644 --- a/deployment/TODO-next-update.md +++ b/deployment/TODO-next-update.md @@ -2,6 +2,10 @@ When you overtake this deploy and rebrand repo to your network you have to recognize the following changes and doings: +## Version >= 2.7.0 with 'ocelotDockerVersionTag' 2.7.0-470 + +- You have to rename all `.js` files to `.ts` in `branding/constants` + ## Version >= 2.4.0 with 'ocelotDockerVersionTag' 2.4.0-298 - You have to set `SHOW_CONTENT_FILTER_HEADER_MENU` and `SHOW_CONTENT_FILTER_MASONRY_GRID` in `branding/constants/filter.js` originally in main code file `webapp/constants/filter.js` to your preferred value. diff --git a/deployment/configurations/stage.ocelot.social b/deployment/configurations/stage.ocelot.social index 350237c62..fdc2e52fa 160000 --- a/deployment/configurations/stage.ocelot.social +++ b/deployment/configurations/stage.ocelot.social @@ -1 +1 @@ -Subproject commit 350237c62dcff1a5c34f1e8d718f89b05ce3d33f +Subproject commit fdc2e52fa444b300e1c4736600bc0e9ae3314222 diff --git a/package.json b/package.json index 9f8399db7..afe65426a 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,25 @@ "url": "https://github.com/Ocelot-Social-Community/Ocelot-Social.git" }, "cypress-cucumber-preprocessor": { - "nonGlobalStepDefinitions": true + "stepDefinitions": "cypress/support/step_definitions/**/*.js", + "json": { + "enabled": true, + "output": "cypress/reports/json_logs/cucumber_log.json", + "formatter": "cucumber-json-formatter" + }, + "messages": { + "enabled": true, + "output": "cypress/reports/json_logs/messages.ndjson" + }, + "html": { + "enabled": false + } }, "scripts": { "db:seed": "cd backend && yarn run db:seed", "db:reset": "cd backend && yarn run db:reset", - "cypress:run": "cypress run --browser electron --config-file ./cypress/cypress.config.js", - "cypress:open": "cypress open --browser electron --config-file ./cypress/cypress.config.js", + "cypress:run": "cypress run --e2e --browser electron --config-file ./cypress/cypress.config.js", + "cypress:open": "cypress open --e2e --browser electron --config-file ./cypress/cypress.config.js", "cucumber:setup": "cd backend && yarn run dev", "cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit", "release": "yarn version --no-git-tag-version --no-commit-hooks --no-commit && auto-changelog --latest-version $(node -p -e \"require('./package.json').version\") && cd backend && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp/maintenance/source && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../../../package.json').version\")" @@ -33,7 +45,6 @@ "cross-env": "^7.0.3", "cucumber": "^6.0.5", "cypress": "^12.17.0", - "cypress-file-upload": "^3.5.3", "cypress-network-idle": "^1.14.2", "date-fns": "^2.25.0", "dotenv": "^8.2.0", @@ -42,6 +53,7 @@ "import": "^0.0.6", "jsonwebtoken": "^8.5.1", "mock-socket": "^9.0.3", + "multiple-cucumber-html-reporter": "^3.4.0", "neo4j-driver": "^4.3.4", "neode": "^0.4.8", "rosie": "^2.1.0", diff --git a/webapp/assets/_new/icons/svgs/chat-bubble.svg b/webapp/assets/_new/icons/svgs/chat-bubble.svg new file mode 100644 index 000000000..377b52f2f --- /dev/null +++ b/webapp/assets/_new/icons/svgs/chat-bubble.svg @@ -0,0 +1,4 @@ + +chat-bubble + + \ No newline at end of file diff --git a/webapp/assets/_new/styles/export.scss b/webapp/assets/_new/styles/export.scss new file mode 100644 index 000000000..5b866d6b7 --- /dev/null +++ b/webapp/assets/_new/styles/export.scss @@ -0,0 +1,34 @@ + +:export { + colorPrimary: $color-primary; + colorPrimaryActive: $color-primary-active; + colorPrimaryLight: $color-primary-light; + + borderColorSoft: $border-color-soft; + + borderRadiusBase: $border-radius-base; + + textColorBase: $text-color-base; + textColorSoft: $text-color-soft; + textColorInverse: $text-color-inverse; + + boxShadowBase: $box-shadow-base; + + backgroundColorBase: $background-color-base; + backgroundColorSoft: $background-color-soft; + backgroundColorSoftest: $background-color-softest; + backgroundColorPrimary: $background-color-primary; + + colorNeutral30: $color-neutral-30; + + chatMessageColor: $chat-message-color; + + chatMessageBgMe: $chat-message-bg-me; + chatMessageBgOthers: $chat-message-bg-others; + + chatNewMessageColor: $chat-new-message-color; + + chatMessageTimestamp: $chat-message-timestamp; + chatMessageCheckmarkSeen: $chat-message-checkmark-seen; + chatMessageCheckmark: $chat-message-checkmark; + } \ No newline at end of file diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index 22e0214ff..ef5086240 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -406,4 +406,17 @@ $color-toast-green: $color-success; $color-ribbon-event: $background-color-third; $color-ribbon-event-active: $background-color-third-active; $color-ribbon-article: $background-color-secondary; -$color-ribbon-article-active: $background-color-secondary-active; \ No newline at end of file +$color-ribbon-article-active: $background-color-secondary-active; + +/** + * @tokens Chat Color + */ + +$chat-message-bg-me: $color-primary-light; +$chat-message-color: $text-color-base; +$chat-message-bg-others: $color-neutral-80; +$chat-sidemenu-bg: $color-secondary-active; +$chat-new-message-color: $color-secondary-active; +$chat-message-timestamp: $text-color-soft; +$chat-message-checkmark-seen: $text-color-secondary; +$chat-message-checkmark: $text-color-soft; diff --git a/webapp/components/Chat/Chat.vue b/webapp/components/Chat/Chat.vue index 2b9514bf3..d7864ebef 100644 --- a/webapp/components/Chat/Chat.vue +++ b/webapp/components/Chat/Chat.vue @@ -4,40 +4,84 @@ + > +
+
+ +
+
+ +
+
+
+ {{ getInitialsName(selectedRoom.roomName) }} +
+
+ +
+
+
+ {{ getInitialsName(room.roomName) }} +
+
+
- diff --git a/webapp/components/HeaderMenu/HeaderMenu.vue b/webapp/components/HeaderMenu/HeaderMenu.vue index bef73186b..f4d48220e 100644 --- a/webapp/components/HeaderMenu/HeaderMenu.vue +++ b/webapp/components/HeaderMenu/HeaderMenu.vue @@ -74,6 +74,10 @@ + diff --git a/webapp/pages/map.vue b/webapp/pages/map.vue index 1a34cbda1..2bf1817f7 100644 --- a/webapp/pages/map.vue +++ b/webapp/pages/map.vue @@ -5,16 +5,15 @@ {{ $t('map.pageTitle') }}
- my position - {{ $t('position.my') }} - user - {{ $t('position.user') }} - group - {{ $t('position.group') }} + + + {{ $t('map.legend.' + type.id) }} +    +
@@ -66,6 +65,7 @@ import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css' import { mapGetters } from 'vuex' import { profileUserQuery, mapUserQuery } from '~/graphql/User' import { groupQuery } from '~/graphql/groups' +import { filterPosts } from '~/graphql/PostQuery.js' import mobile from '~/mixins/mobile' import Empty from '~/components/Empty/Empty' import MapStylesButtons from '~/components/Map/MapStylesButtons' @@ -95,19 +95,40 @@ export default { currentUserCoordinates: null, users: null, groups: null, + posts: null, markers: { - icons: [ + types: [ { - id: 'marker-blue', - name: 'mapbox-marker-icon-20px-blue.png', + id: 'theUser', + icon: { + id: 'marker-orange', + legendName: 'mapbox-marker-icon-orange.svg', + mapName: 'mapbox-marker-icon-20px-orange.png', + }, }, { - id: 'marker-orange', - name: 'mapbox-marker-icon-20px-orange.png', + id: 'user', + icon: { + id: 'marker-green', + legendName: 'mapbox-marker-icon-green.svg', + mapName: 'mapbox-marker-icon-20px-green.png', + }, }, { - id: 'marker-green', - name: 'mapbox-marker-icon-20px-green.png', + id: 'group', + icon: { + id: 'marker-red', + legendName: 'mapbox-marker-icon-red.svg', + mapName: 'mapbox-marker-icon-20px-red.png', + }, + }, + { + id: 'event', + icon: { + id: 'marker-purple', + legendName: 'mapbox-marker-icon-purple.svg', + mapName: 'mapbox-marker-icon-20px-purple.png', + }, }, ], isImagesLoaded: false, @@ -137,7 +158,8 @@ export default { this.markers.isImagesLoaded && this.currentUser && this.users && - this.groups + this.groups && + this.posts ) }, styles() { @@ -236,17 +258,27 @@ export default { // Copy coordinates array. const coordinates = e.features[0].geometry.coordinates.slice() - const markerTypeLabel = - e.features[0].properties.type === 'group' - ? this.$t('map.markerTypes.group') - : e.features[0].properties.type === 'user' - ? this.$t('map.markerTypes.user') - : this.$t('map.markerTypes.theUser') - const markerProfileLinkTitle = - (e.features[0].properties.type === 'group' ? '&' : '@') + e.features[0].properties.slug - const markerProfileLink = - (e.features[0].properties.type === 'group' ? '/group' : '/profile') + - `/${e.features[0].properties.id}/${e.features[0].properties.slug}` + const markerTypeLabel = this.$t(`map.markerTypes.${e.features[0].properties.type}`) + const markerProfile = { + theUser: { + linkTitle: '@' + e.features[0].properties.slug, + link: `/profile/${e.features[0].properties.id}/${e.features[0].properties.slug}`, + }, + user: { + linkTitle: '@' + e.features[0].properties.slug, + link: `/profile/${e.features[0].properties.id}/${e.features[0].properties.slug}`, + }, + group: { + linkTitle: '&' + e.features[0].properties.slug, + link: `/group/${e.features[0].properties.id}/${e.features[0].properties.slug}`, + }, + event: { + linkTitle: e.features[0].properties.slug, + link: `/post/${e.features[0].properties.id}/${e.features[0].properties.slug}`, + }, + } + const markerProfileLinkTitle = markerProfile[e.features[0].properties.type].linkTitle + const markerProfileLink = markerProfile[e.features[0].properties.type].link let description = `
@@ -258,11 +290,11 @@ export default {
` description += - e.features[0].properties.about && e.features[0].properties.about.length > 0 + e.features[0].properties.description && e.features[0].properties.description.length > 0 ? `
- ${e.features[0].properties.about} + ${e.features[0].properties.description}
` : '' @@ -305,15 +337,18 @@ export default { }, loadMarkersIconsAndAddMarkers() { Promise.all( - this.markers.icons.map( + this.markers.types.map( (marker) => new Promise((resolve, reject) => { // our images have to be in the 'static/img/*' folder otherwise they are not reachable via URL - this.map.loadImage('img/mapbox/marker-icons/' + marker.name, (error, image) => { - if (error) throw error - this.map.addImage(marker.id, image) - resolve() - }) + this.map.loadImage( + 'img/mapbox/marker-icons/' + marker.icon.mapName, + (error, image) => { + if (error) throw error + this.map.addImage(marker.icon.id, image) + resolve() + }, + ) }), ), ).then(() => { @@ -337,7 +372,7 @@ export default { id: user.id, slug: user.slug, name: user.name, - about: user.about ? user.about : undefined, + description: user.about ? user.about : undefined, }, geometry: { type: 'Point', @@ -346,27 +381,6 @@ export default { }) } }) - // add markers for "groups" - this.groups.forEach((group) => { - if (group.location) { - this.markers.geoJSON.push({ - type: 'Feature', - properties: { - type: 'group', - iconName: 'marker-blue', - iconRotate: 0.0, - id: group.id, - slug: group.slug, - name: group.name, - about: group.about ? group.about : undefined, - }, - geometry: { - type: 'Point', - coordinates: this.getCoordinates(group.location), - }, - }) - } - }) // add marker for "currentUser" if (this.currentUserCoordinates) { this.markers.geoJSON.push({ @@ -378,7 +392,7 @@ export default { id: this.currentUser.id, slug: this.currentUser.slug, name: this.currentUser.name, - about: this.currentUser.about ? this.currentUser.about : undefined, + description: this.currentUser.about ? this.currentUser.about : undefined, }, geometry: { type: 'Point', @@ -386,6 +400,48 @@ export default { }, }) } + // add markers for "groups" + this.groups.forEach((group) => { + if (group.location) { + this.markers.geoJSON.push({ + type: 'Feature', + properties: { + type: 'group', + iconName: 'marker-red', + iconRotate: 0.0, + id: group.id, + slug: group.slug, + name: group.name, + description: group.about ? group.about : undefined, + }, + geometry: { + type: 'Point', + coordinates: this.getCoordinates(group.location), + }, + }) + } + }) + // add markers for "posts", post type "Event" with location coordinates + this.posts.forEach((post) => { + if (post.postType.includes('Event') && post.eventLocation) { + this.markers.geoJSON.push({ + type: 'Feature', + properties: { + type: 'event', + iconName: 'marker-purple', + iconRotate: 0.0, + id: post.id, + slug: post.slug, + name: post.title, + description: post.contentExcerpt, + }, + geometry: { + type: 'Point', + coordinates: this.getCoordinates(post.eventLocation), + }, + }) + } + }) this.markers.isGeoJSON = true } @@ -483,6 +539,24 @@ export default { }, fetchPolicy: 'cache-and-network', }, + Post: { + query() { + return filterPosts(this.$i18n) + }, + variables() { + return { + filter: { + postType_in: ['Event'], + eventStart_gte: new Date(), + // would be good to just query for events with defined "eventLocation". couldn't get it working + }, + } + }, + update({ Post }) { + this.posts = Post + }, + fetchPolicy: 'cache-and-network', + }, }, } diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index 62d2f1be7..cef3a5d45 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -80,13 +80,14 @@ @update="updateFollow" /> - + {{ $t('chat.userProfileButton.label') }}