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 74ebd1c43..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,28 +57,27 @@ 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: Full stack tests | prepare + - name: Boot up test system | docker-compose 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" chmod +x /opt/cucumber-json-formatter sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter - cd backend - yarn install - yarn build - cd .. - yarn install + 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 - name: Full stack tests | run tests id: e2e-tests @@ -44,17 +88,25 @@ jobs: run: | cd cypress/ node create-cucumber-html-report.js - - - name: End-to-end tests | if tests failed, get pr number - id: pr - if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }} - uses: 8BitJonny/gh-get-current-pr@2.2.0 - - name: End-to-end tests | if tests failed, upload report + - 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: ocelot-e2e-test-report-pr${{ steps.pr.outputs.number }} + 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 c1aee47cf..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: @@ -34,7 +34,7 @@ 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 == 'true' @@ -44,16 +44,16 @@ jobs: - 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 @@ -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/src/db/seed.ts b/backend/src/db/seed.ts index a717ff7a6..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' @@ -267,17 +269,17 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await dagobert.relateTo(louie, 'blocked') // categories - await Promise.all( - categories.map(({ icon, name }, index) => { - return Factory.build('category', { - id: `cat${index + 1}`, - slug: name, - name, - icon, - }) - }), - ) + 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', }) @@ -293,361 +295,324 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] // 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() }) @@ -834,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() @@ -883,31 +849,30 @@ 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: any[] = [] @@ -1191,368 +1156,410 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] 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 ca5ffb952..2842c7230 100644 --- a/backend/src/graphql/messages.ts +++ b/backend/src/graphql/messages.ts @@ -6,6 +6,10 @@ export const createMessageMutation = () => { CreateMessage(roomId: $roomId, content: $content) { id content + senderId + username + avatar + date saved distributed seen @@ -17,12 +21,15 @@ export const createMessageMutation = () => { export const messageQuery = () => { return gql` query ($roomId: ID!, $first: Int, $offset: Int) { - Message(roomId: $roomId, first: $first, offset: $offset, orderBy: createdAt_desc) { + Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) { _id id indexId content senderId + author { + id + } username avatar date 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/helpers/walkRecursive.ts b/backend/src/helpers/walkRecursive.ts index f560cf9cb..f3be67575 100644 --- a/backend/src/helpers/walkRecursive.ts +++ b/backend/src/helpers/walkRecursive.ts @@ -9,10 +9,9 @@ function walkRecursive(data, fields, fieldName, callback, _key?) { if (!Array.isArray(fields)) { throw new Error('please provide an fields array for the walkRecursive helper') } - if (data && typeof data === 'string' && fields.includes(_key)) { - // well we found what we searched for, lets replace the value with our callback result - const key = _key.split('!') - if (key.length === 1 || key[1] !== fieldName) data = callback(data, key[0]) + const fieldDef = fields.find((f) => f.field === _key) + if (data && typeof data === 'string' && fieldDef) { + if (!fieldDef.excludes?.includes(fieldName)) data = callback(data, _key) } else if (data && Array.isArray(data)) { // go into the rabbit hole and dig through that array data.forEach((res, index) => { 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/helpers/cleanHtml.ts b/backend/src/middleware/helpers/cleanHtml.ts index ac71f6bdc..84497760d 100644 --- a/backend/src/middleware/helpers/cleanHtml.ts +++ b/backend/src/middleware/helpers/cleanHtml.ts @@ -30,6 +30,7 @@ const standardSanitizeHtmlOptions = { 'strike', 'span', 'blockquote', + 'usertag', ], allowedAttributes: { a: ['href', 'class', 'target', 'data-*', 'contenteditable'], 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 c07098a3c..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, diff --git a/backend/src/middleware/xssMiddleware.ts b/backend/src/middleware/xssMiddleware.ts index ede0cc199..c10997e8d 100644 --- a/backend/src/middleware/xssMiddleware.ts +++ b/backend/src/middleware/xssMiddleware.ts @@ -3,11 +3,11 @@ import { cleanHtml } from '../middleware/helpers/cleanHtml' // exclamation mark separetes field names, that should not be sanitized const fields = [ - 'content', - 'contentExcerpt', - 'reasonDescription', - 'description!embed', - 'descriptionExcerpt', + { field: 'content', excludes: ['CreateMessage', 'Message'] }, + { field: 'contentExcerpt' }, + { field: 'reasonDescription' }, + { field: 'description', excludes: ['embed'] }, + { field: 'descriptionExcerpt' }, ] export default { diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index d0f1d7871..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 { createRoomMutation, roomQuery } from '../../graphql/rooms' import { createMessageMutation, messageQuery, markMessagesAsSeen } from '../../graphql/messages' -import createServer from '../../server' +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,12 +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, + }), + }), + ], + }, + }) + }) }) }) diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index a9937aac4..c1381045f 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, context) => { + return payload.userId === context.user?.id + }, + ), + }, + }, Query: { Message: async (object, params, context, resolveInfo) => { const { roomId } = params @@ -20,33 +49,15 @@ export default { const undistributedMessagesIds = resolved .filter((msg) => !msg.distributed && msg.senderId !== context.user.id) .map((msg) => msg.id) - if (undistributedMessagesIds.length > 0) { - const session = context.driver.session() - const writeTxResultPromise = 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 - }) - try { - await writeTxResultPromise - } finally { - session.close() + const session = context.driver.session() + try { + if (undistributedMessagesIds.length > 0) { + await setMessagesAsDistributed(undistributedMessagesIds, session) } - // send subscription to author to updated the messages + } finally { + session.close() } - resolved.forEach((message) => { - message._id = message.id - if (message.senderId !== context.user.id) { - message.distributed = true - } - }) + // send subscription to author to updated the messages } return resolved.reverse() }, @@ -61,31 +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) - WITH MAX(m.indexId) as maxIndex, room, currentUser + 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(), indexId: CASE WHEN maxIndex IS NOT NULL THEN maxIndex + 1 ELSE 0 END, - content: $content, + content: LEFT($content,2000), 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) diff --git a/backend/src/schema/resolvers/notifications.ts b/backend/src/schema/resolvers/notifications.ts index e427de227..6a3e232cc 100644 --- a/backend/src/schema/resolvers/notifications.ts +++ b/backend/src/schema/resolvers/notifications.ts @@ -7,8 +7,8 @@ export default { notificationAdded: { subscribe: withFilter( () => pubsub.asyncIterator(NOTIFICATION_ADDED), - (payload, variables) => { - return payload.notificationAdded.to.id === variables.userId + (payload, variables, context) => { + return payload.notificationAdded.to.id === context.user?.id }, ), }, 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..5382c5ee7 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, context) => { + return payload.userId === context.user?.id + }, + ), + }, + }, 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/Message.gql b/backend/src/schema/types/type/Message.gql index 671c5523a..16e458151 100644 --- a/backend/src/schema/types/type/Message.gql +++ b/backend/src/schema/types/type/Message.gql @@ -3,8 +3,7 @@ # } enum _MessageOrdering { - createdAt_asc - createdAt_desc + indexId_desc } type Message { @@ -45,3 +44,7 @@ type Query { orderBy: [_MessageOrdering] ): [Message] } + +type Subscription { + chatMessageAdded: Message +} diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql index 62a1f3696..1f825decc 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -38,5 +38,5 @@ type Mutation { } type Subscription { - notificationAdded(userId: ID!): NOTIFIED + notificationAdded: NOTIFIED } diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index 2ce6556f6..60d54192c 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -5,6 +5,12 @@ # users_some: _UserFilter # } +# TODO change this to last message date +enum _RoomOrdering { + lastMessageAt_desc + createdAt_desc +} + type Room { id: ID! createdAt: String @@ -13,8 +19,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 +50,13 @@ type Mutation { } type Query { - Room: [Room] + Room( + id: ID + orderBy: [_RoomOrdering] + ): [Room] + UnreadRooms: Int +} + +type Subscription { + roomCountUpdated: 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/webapp/assets/_new/icons/svgs/align-center.svg b/webapp/assets/_new/icons/svgs/align-center.svg new file mode 100755 index 000000000..4232dff91 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/align-center.svg @@ -0,0 +1,5 @@ + + +align-center + + diff --git a/webapp/assets/_new/icons/svgs/align-justify.svg b/webapp/assets/_new/icons/svgs/align-justify.svg new file mode 100755 index 000000000..ce82c7f0a --- /dev/null +++ b/webapp/assets/_new/icons/svgs/align-justify.svg @@ -0,0 +1,5 @@ + + +align-justify + + diff --git a/webapp/assets/_new/icons/svgs/align-left.svg b/webapp/assets/_new/icons/svgs/align-left.svg new file mode 100755 index 000000000..c76e93dc9 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/align-left.svg @@ -0,0 +1,5 @@ + + +align-left + + diff --git a/webapp/assets/_new/icons/svgs/align-right.svg b/webapp/assets/_new/icons/svgs/align-right.svg new file mode 100755 index 000000000..24972ed83 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/align-right.svg @@ -0,0 +1,5 @@ + + +align-right + + diff --git a/webapp/assets/_new/icons/svgs/angle-left.svg b/webapp/assets/_new/icons/svgs/angle-left.svg new file mode 100755 index 000000000..826dd8e34 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/angle-left.svg @@ -0,0 +1,5 @@ + + +angle-left + + diff --git a/webapp/assets/_new/icons/svgs/angle-right.svg b/webapp/assets/_new/icons/svgs/angle-right.svg new file mode 100755 index 000000000..1df45a590 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/angle-right.svg @@ -0,0 +1,5 @@ + + +angle-right + + diff --git a/webapp/assets/_new/icons/svgs/archive.svg b/webapp/assets/_new/icons/svgs/archive.svg new file mode 100755 index 000000000..878713822 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/archive.svg @@ -0,0 +1,5 @@ + + +archive + + diff --git a/webapp/assets/_new/icons/svgs/arrow-up.svg b/webapp/assets/_new/icons/svgs/arrow-up.svg new file mode 100755 index 000000000..f48c186c0 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/arrow-up.svg @@ -0,0 +1,5 @@ + + +arrow-up + + diff --git a/webapp/assets/_new/icons/svgs/bar-chart.svg b/webapp/assets/_new/icons/svgs/bar-chart.svg new file mode 100755 index 000000000..0c853e737 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/bar-chart.svg @@ -0,0 +1,5 @@ + + +bar-chart + + diff --git a/webapp/assets/_new/icons/svgs/briefcase.svg b/webapp/assets/_new/icons/svgs/briefcase.svg new file mode 100755 index 000000000..c0f6552a0 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/briefcase.svg @@ -0,0 +1,5 @@ + + +briefcase + + diff --git a/webapp/assets/_new/icons/svgs/bug.svg b/webapp/assets/_new/icons/svgs/bug.svg new file mode 100755 index 000000000..66374d8bc --- /dev/null +++ b/webapp/assets/_new/icons/svgs/bug.svg @@ -0,0 +1,5 @@ + + +bug + + diff --git a/webapp/assets/_new/icons/svgs/calculator.svg b/webapp/assets/_new/icons/svgs/calculator.svg new file mode 100755 index 000000000..657b19919 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/calculator.svg @@ -0,0 +1,5 @@ + + +calculator + + diff --git a/webapp/assets/_new/icons/svgs/camera.svg b/webapp/assets/_new/icons/svgs/camera.svg new file mode 100755 index 000000000..793620544 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/camera.svg @@ -0,0 +1,5 @@ + + +camera + + diff --git a/webapp/assets/_new/icons/svgs/cart-plus.svg b/webapp/assets/_new/icons/svgs/cart-plus.svg new file mode 100755 index 000000000..84ea385bd --- /dev/null +++ b/webapp/assets/_new/icons/svgs/cart-plus.svg @@ -0,0 +1,5 @@ + + +cart-plus + + diff --git a/webapp/assets/_new/icons/svgs/certificate.svg b/webapp/assets/_new/icons/svgs/certificate.svg new file mode 100755 index 000000000..341b4af3e --- /dev/null +++ b/webapp/assets/_new/icons/svgs/certificate.svg @@ -0,0 +1,5 @@ + + +certificate + + diff --git a/webapp/assets/_new/icons/svgs/chain-broken.svg b/webapp/assets/_new/icons/svgs/chain-broken.svg new file mode 100755 index 000000000..4ba13f49c --- /dev/null +++ b/webapp/assets/_new/icons/svgs/chain-broken.svg @@ -0,0 +1,5 @@ + + +chain-broken + + diff --git a/webapp/assets/_new/icons/svgs/chain.svg b/webapp/assets/_new/icons/svgs/chain.svg new file mode 100755 index 000000000..9d390e126 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/chain.svg @@ -0,0 +1,5 @@ + + +chain + + diff --git a/webapp/assets/_new/icons/svgs/cloud-download.svg b/webapp/assets/_new/icons/svgs/cloud-download.svg new file mode 100755 index 000000000..fcc46456b --- /dev/null +++ b/webapp/assets/_new/icons/svgs/cloud-download.svg @@ -0,0 +1,5 @@ + + +cloud-download + + diff --git a/webapp/assets/_new/icons/svgs/cloud-upload.svg b/webapp/assets/_new/icons/svgs/cloud-upload.svg new file mode 100755 index 000000000..8a0c486b2 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/cloud-upload.svg @@ -0,0 +1,5 @@ + + +cloud-upload + + diff --git a/webapp/assets/_new/icons/svgs/cloud.svg b/webapp/assets/_new/icons/svgs/cloud.svg new file mode 100755 index 000000000..7ee840e92 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/cloud.svg @@ -0,0 +1,5 @@ + + +cloud + + diff --git a/webapp/assets/_new/icons/svgs/code.svg b/webapp/assets/_new/icons/svgs/code.svg new file mode 100755 index 000000000..2581fa884 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/code.svg @@ -0,0 +1,5 @@ + + +code + + diff --git a/webapp/assets/_new/icons/svgs/coffee.svg b/webapp/assets/_new/icons/svgs/coffee.svg new file mode 100755 index 000000000..302150d0a --- /dev/null +++ b/webapp/assets/_new/icons/svgs/coffee.svg @@ -0,0 +1,5 @@ + + +coffee + + diff --git a/webapp/assets/_new/icons/svgs/columns.svg b/webapp/assets/_new/icons/svgs/columns.svg new file mode 100755 index 000000000..f77576ed4 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/columns.svg @@ -0,0 +1,5 @@ + + +columns + + diff --git a/webapp/assets/_new/icons/svgs/compass.svg b/webapp/assets/_new/icons/svgs/compass.svg new file mode 100755 index 000000000..935cb5791 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/compass.svg @@ -0,0 +1,5 @@ + + +compass + + diff --git a/webapp/assets/_new/icons/svgs/credit-card.svg b/webapp/assets/_new/icons/svgs/credit-card.svg new file mode 100755 index 000000000..29c1fb96f --- /dev/null +++ b/webapp/assets/_new/icons/svgs/credit-card.svg @@ -0,0 +1,5 @@ + + +credit-card + + diff --git a/webapp/assets/_new/icons/svgs/crop.svg b/webapp/assets/_new/icons/svgs/crop.svg new file mode 100755 index 000000000..069de9d1e --- /dev/null +++ b/webapp/assets/_new/icons/svgs/crop.svg @@ -0,0 +1,5 @@ + + +crop + + diff --git a/webapp/assets/_new/icons/svgs/crosshairs.svg b/webapp/assets/_new/icons/svgs/crosshairs.svg new file mode 100755 index 000000000..6d8b9aa04 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/crosshairs.svg @@ -0,0 +1,5 @@ + + +crosshairs + + diff --git a/webapp/assets/_new/icons/svgs/cube.svg b/webapp/assets/_new/icons/svgs/cube.svg new file mode 100755 index 000000000..97fbdf121 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/cube.svg @@ -0,0 +1,5 @@ + + +cube + + diff --git a/webapp/assets/_new/icons/svgs/cubes.svg b/webapp/assets/_new/icons/svgs/cubes.svg new file mode 100755 index 000000000..aeb3d66d1 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/cubes.svg @@ -0,0 +1,5 @@ + + +cubes + + diff --git a/webapp/assets/_new/icons/svgs/cut.svg b/webapp/assets/_new/icons/svgs/cut.svg new file mode 100755 index 000000000..f04b85c3b --- /dev/null +++ b/webapp/assets/_new/icons/svgs/cut.svg @@ -0,0 +1,5 @@ + + +cut + + diff --git a/webapp/assets/_new/icons/svgs/dashboard.svg b/webapp/assets/_new/icons/svgs/dashboard.svg new file mode 100755 index 000000000..74358fd41 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/dashboard.svg @@ -0,0 +1,5 @@ + + +dashboard + + diff --git a/webapp/assets/_new/icons/svgs/diamond.svg b/webapp/assets/_new/icons/svgs/diamond.svg new file mode 100755 index 000000000..5cad085d9 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/diamond.svg @@ -0,0 +1,5 @@ + + +diamond + + diff --git a/webapp/assets/_new/icons/svgs/exchange.svg b/webapp/assets/_new/icons/svgs/exchange.svg new file mode 100755 index 000000000..fc8bfaa48 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/exchange.svg @@ -0,0 +1,5 @@ + + +exchange + + diff --git a/webapp/assets/_new/icons/svgs/exclamation-triangle.svg b/webapp/assets/_new/icons/svgs/exclamation-triangle.svg new file mode 100755 index 000000000..aa2f7bbac --- /dev/null +++ b/webapp/assets/_new/icons/svgs/exclamation-triangle.svg @@ -0,0 +1,5 @@ + + +exclamation-triangle + + diff --git a/webapp/assets/_new/icons/svgs/expand.svg b/webapp/assets/_new/icons/svgs/expand.svg new file mode 100755 index 000000000..dc21a7e24 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/expand.svg @@ -0,0 +1,5 @@ + + +expand + + diff --git a/webapp/assets/_new/icons/svgs/external-link.svg b/webapp/assets/_new/icons/svgs/external-link.svg new file mode 100755 index 000000000..3d7f95574 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/external-link.svg @@ -0,0 +1,5 @@ + + +external-link + + diff --git a/webapp/assets/_new/icons/svgs/eyedropper.svg b/webapp/assets/_new/icons/svgs/eyedropper.svg new file mode 100755 index 000000000..6cf4151ed --- /dev/null +++ b/webapp/assets/_new/icons/svgs/eyedropper.svg @@ -0,0 +1,5 @@ + + +eyedropper + + diff --git a/webapp/assets/_new/icons/svgs/facebook.svg b/webapp/assets/_new/icons/svgs/facebook.svg new file mode 100755 index 000000000..a9e41dfb3 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/facebook.svg @@ -0,0 +1,5 @@ + + +facebook + + diff --git a/webapp/assets/_new/icons/svgs/female.svg b/webapp/assets/_new/icons/svgs/female.svg new file mode 100755 index 000000000..2cbe5b470 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/female.svg @@ -0,0 +1,5 @@ + + +female + + diff --git a/webapp/assets/_new/icons/svgs/file-archive.svg b/webapp/assets/_new/icons/svgs/file-archive.svg new file mode 100755 index 000000000..94a1e7ab8 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-archive.svg @@ -0,0 +1,5 @@ + + +file-archive-o + + diff --git a/webapp/assets/_new/icons/svgs/file-audio.svg b/webapp/assets/_new/icons/svgs/file-audio.svg new file mode 100755 index 000000000..e253cbf94 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-audio.svg @@ -0,0 +1,5 @@ + + +file-audio-o + + diff --git a/webapp/assets/_new/icons/svgs/file-code.svg b/webapp/assets/_new/icons/svgs/file-code.svg new file mode 100755 index 000000000..c241a932b --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-code.svg @@ -0,0 +1,5 @@ + + +file-code-o + + diff --git a/webapp/assets/_new/icons/svgs/file-excel.svg b/webapp/assets/_new/icons/svgs/file-excel.svg new file mode 100755 index 000000000..77a7a73b0 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-excel.svg @@ -0,0 +1,5 @@ + + +file-excel-o + + diff --git a/webapp/assets/_new/icons/svgs/file-image.svg b/webapp/assets/_new/icons/svgs/file-image.svg new file mode 100755 index 000000000..ba296f51b --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-image.svg @@ -0,0 +1,5 @@ + + +file-image-o + + diff --git a/webapp/assets/_new/icons/svgs/file-movie.svg b/webapp/assets/_new/icons/svgs/file-movie.svg new file mode 100755 index 000000000..fd88fd23d --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-movie.svg @@ -0,0 +1,5 @@ + + +file-movie-o + + diff --git a/webapp/assets/_new/icons/svgs/file-pdf.svg b/webapp/assets/_new/icons/svgs/file-pdf.svg new file mode 100755 index 000000000..810128b55 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-pdf.svg @@ -0,0 +1,5 @@ + + +file-pdf-o + + diff --git a/webapp/assets/_new/icons/svgs/file-photo.svg b/webapp/assets/_new/icons/svgs/file-photo.svg new file mode 100755 index 000000000..c67616527 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-photo.svg @@ -0,0 +1,5 @@ + + +file-photo-o + + diff --git a/webapp/assets/_new/icons/svgs/file-picture.svg b/webapp/assets/_new/icons/svgs/file-picture.svg new file mode 100755 index 000000000..e16498317 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-picture.svg @@ -0,0 +1,5 @@ + + +file-picture-o + + diff --git a/webapp/assets/_new/icons/svgs/file-powerpoint.svg b/webapp/assets/_new/icons/svgs/file-powerpoint.svg new file mode 100755 index 000000000..8df8f896b --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-powerpoint.svg @@ -0,0 +1,5 @@ + + +file-powerpoint-o + + diff --git a/webapp/assets/_new/icons/svgs/file-sound.svg b/webapp/assets/_new/icons/svgs/file-sound.svg new file mode 100755 index 000000000..cb9b5dc76 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-sound.svg @@ -0,0 +1,5 @@ + + +file-sound-o + + diff --git a/webapp/assets/_new/icons/svgs/file-text.svg b/webapp/assets/_new/icons/svgs/file-text.svg new file mode 100755 index 000000000..38d75ef65 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-text.svg @@ -0,0 +1,5 @@ + + +file-text + + diff --git a/webapp/assets/_new/icons/svgs/file-video.svg b/webapp/assets/_new/icons/svgs/file-video.svg new file mode 100755 index 000000000..4a6db285b --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-video.svg @@ -0,0 +1,5 @@ + + +file-video-o + + diff --git a/webapp/assets/_new/icons/svgs/file-word.svg b/webapp/assets/_new/icons/svgs/file-word.svg new file mode 100755 index 000000000..c8447a696 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-word.svg @@ -0,0 +1,5 @@ + + +file-word-o + + diff --git a/webapp/assets/_new/icons/svgs/file-zip.svg b/webapp/assets/_new/icons/svgs/file-zip.svg new file mode 100755 index 000000000..c4eb66dd0 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/file-zip.svg @@ -0,0 +1,5 @@ + + +file-zip-o + + diff --git a/webapp/assets/_new/icons/svgs/files.svg b/webapp/assets/_new/icons/svgs/files.svg new file mode 100755 index 000000000..602a25658 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/files.svg @@ -0,0 +1,5 @@ + + +files-o + + diff --git a/webapp/assets/_new/icons/svgs/film.svg b/webapp/assets/_new/icons/svgs/film.svg new file mode 100755 index 000000000..2318d2f73 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/film.svg @@ -0,0 +1,5 @@ + + +film + + diff --git a/webapp/assets/_new/icons/svgs/fire.svg b/webapp/assets/_new/icons/svgs/fire.svg new file mode 100755 index 000000000..0d579c495 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/fire.svg @@ -0,0 +1,5 @@ + + +fire + + diff --git a/webapp/assets/_new/icons/svgs/flask.svg b/webapp/assets/_new/icons/svgs/flask.svg new file mode 100755 index 000000000..e1ca6bde6 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/flask.svg @@ -0,0 +1,5 @@ + + +flask + + diff --git a/webapp/assets/_new/icons/svgs/floppy.svg b/webapp/assets/_new/icons/svgs/floppy.svg new file mode 100755 index 000000000..f924d3bb5 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/floppy.svg @@ -0,0 +1,5 @@ + + +floppy-o + + diff --git a/webapp/assets/_new/icons/svgs/folder-open.svg b/webapp/assets/_new/icons/svgs/folder-open.svg new file mode 100755 index 000000000..63a941b71 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/folder-open.svg @@ -0,0 +1,5 @@ + + +folder-open + + diff --git a/webapp/assets/_new/icons/svgs/folder.svg b/webapp/assets/_new/icons/svgs/folder.svg new file mode 100755 index 000000000..287a37768 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/folder.svg @@ -0,0 +1,5 @@ + + +folder + + diff --git a/webapp/assets/_new/icons/svgs/frown.svg b/webapp/assets/_new/icons/svgs/frown.svg new file mode 100755 index 000000000..a03906c69 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/frown.svg @@ -0,0 +1,5 @@ + + +frown-o + + diff --git a/webapp/assets/_new/icons/svgs/gamepad.svg b/webapp/assets/_new/icons/svgs/gamepad.svg new file mode 100755 index 000000000..85a2b7434 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/gamepad.svg @@ -0,0 +1,5 @@ + + +gamepad + + diff --git a/webapp/assets/_new/icons/svgs/gear.svg b/webapp/assets/_new/icons/svgs/gear.svg new file mode 100755 index 000000000..f657c9494 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/gear.svg @@ -0,0 +1,5 @@ + + +gear + + diff --git a/webapp/assets/_new/icons/svgs/gears.svg b/webapp/assets/_new/icons/svgs/gears.svg new file mode 100755 index 000000000..f9727888d --- /dev/null +++ b/webapp/assets/_new/icons/svgs/gears.svg @@ -0,0 +1,5 @@ + + +gears + + diff --git a/webapp/assets/_new/icons/svgs/gift.svg b/webapp/assets/_new/icons/svgs/gift.svg new file mode 100755 index 000000000..355dc7b08 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/gift.svg @@ -0,0 +1,5 @@ + + +gift + + diff --git a/webapp/assets/_new/icons/svgs/github.svg b/webapp/assets/_new/icons/svgs/github.svg new file mode 100755 index 000000000..1d61e0788 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/github.svg @@ -0,0 +1,5 @@ + + +github + + diff --git a/webapp/assets/_new/icons/svgs/glass.svg b/webapp/assets/_new/icons/svgs/glass.svg new file mode 100755 index 000000000..c2a18e99f --- /dev/null +++ b/webapp/assets/_new/icons/svgs/glass.svg @@ -0,0 +1,5 @@ + + +glass + + diff --git a/webapp/assets/_new/icons/svgs/group.svg b/webapp/assets/_new/icons/svgs/group.svg new file mode 100755 index 000000000..efb1c6184 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/group.svg @@ -0,0 +1,5 @@ + + +group + + diff --git a/webapp/assets/_new/icons/svgs/hand-down.svg b/webapp/assets/_new/icons/svgs/hand-down.svg new file mode 100755 index 000000000..1a06a97fd --- /dev/null +++ b/webapp/assets/_new/icons/svgs/hand-down.svg @@ -0,0 +1,5 @@ + + +hand-o-down + + diff --git a/webapp/assets/_new/icons/svgs/hand-left.svg b/webapp/assets/_new/icons/svgs/hand-left.svg new file mode 100755 index 000000000..49fb68314 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/hand-left.svg @@ -0,0 +1,5 @@ + + +hand-o-left + + diff --git a/webapp/assets/_new/icons/svgs/hand-pointer.svg b/webapp/assets/_new/icons/svgs/hand-pointer.svg index bdd2c8fe0..e74339724 100644 --- a/webapp/assets/_new/icons/svgs/hand-pointer.svg +++ b/webapp/assets/_new/icons/svgs/hand-pointer.svg @@ -1,5 +1,5 @@ hand-pointer-o - + diff --git a/webapp/assets/_new/icons/svgs/hand-right.svg b/webapp/assets/_new/icons/svgs/hand-right.svg new file mode 100755 index 000000000..c165ea020 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/hand-right.svg @@ -0,0 +1,5 @@ + + +hand-o-right + + diff --git a/webapp/assets/_new/icons/svgs/hand-stop.svg b/webapp/assets/_new/icons/svgs/hand-stop.svg new file mode 100755 index 000000000..dbf393138 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/hand-stop.svg @@ -0,0 +1,5 @@ + + +hand-stop-o + + diff --git a/webapp/assets/_new/icons/svgs/hand-up.svg b/webapp/assets/_new/icons/svgs/hand-up.svg new file mode 100755 index 000000000..472c8435d --- /dev/null +++ b/webapp/assets/_new/icons/svgs/hand-up.svg @@ -0,0 +1,5 @@ + + +hand-o-up + + diff --git a/webapp/assets/_new/icons/svgs/headphones.svg b/webapp/assets/_new/icons/svgs/headphones.svg new file mode 100755 index 000000000..a197eca3a --- /dev/null +++ b/webapp/assets/_new/icons/svgs/headphones.svg @@ -0,0 +1,5 @@ + + +headphones + + diff --git a/webapp/assets/_new/icons/svgs/heart.svg b/webapp/assets/_new/icons/svgs/heart.svg new file mode 100755 index 000000000..64f1195b1 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/heart.svg @@ -0,0 +1,5 @@ + + +heart + + diff --git a/webapp/assets/_new/icons/svgs/history.svg b/webapp/assets/_new/icons/svgs/history.svg new file mode 100755 index 000000000..fd5fcc328 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/history.svg @@ -0,0 +1,5 @@ + + +history + + diff --git a/webapp/assets/_new/icons/svgs/hourglass.svg b/webapp/assets/_new/icons/svgs/hourglass.svg new file mode 100755 index 000000000..e5e5ea6a1 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/hourglass.svg @@ -0,0 +1,5 @@ + + +hourglass + + diff --git a/webapp/assets/_new/icons/svgs/inbox.svg b/webapp/assets/_new/icons/svgs/inbox.svg new file mode 100755 index 000000000..aba1d2eb0 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/inbox.svg @@ -0,0 +1,5 @@ + + +inbox + + diff --git a/webapp/assets/_new/icons/svgs/indent.svg b/webapp/assets/_new/icons/svgs/indent.svg new file mode 100755 index 000000000..35f087427 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/indent.svg @@ -0,0 +1,5 @@ + + +indent + + diff --git a/webapp/assets/_new/icons/svgs/info-circle.svg b/webapp/assets/_new/icons/svgs/info-circle.svg new file mode 100755 index 000000000..f971f5b3a --- /dev/null +++ b/webapp/assets/_new/icons/svgs/info-circle.svg @@ -0,0 +1,5 @@ + + +info-circle + + diff --git a/webapp/assets/_new/icons/svgs/keyboard.svg b/webapp/assets/_new/icons/svgs/keyboard.svg new file mode 100755 index 000000000..cf49166d4 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/keyboard.svg @@ -0,0 +1,5 @@ + + +keyboard-o + + diff --git a/webapp/assets/_new/icons/svgs/leaf.svg b/webapp/assets/_new/icons/svgs/leaf.svg new file mode 100755 index 000000000..52b1693ba --- /dev/null +++ b/webapp/assets/_new/icons/svgs/leaf.svg @@ -0,0 +1,5 @@ + + +leaf + + diff --git a/webapp/assets/_new/icons/svgs/level-up.svg b/webapp/assets/_new/icons/svgs/level-up.svg new file mode 100755 index 000000000..1c3c82192 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/level-up.svg @@ -0,0 +1,5 @@ + + +level-up + + diff --git a/webapp/assets/_new/icons/svgs/life-ring.svg b/webapp/assets/_new/icons/svgs/life-ring.svg new file mode 100755 index 000000000..64f4aa7e8 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/life-ring.svg @@ -0,0 +1,5 @@ + + +life-ring + + diff --git a/webapp/assets/_new/icons/svgs/list.svg b/webapp/assets/_new/icons/svgs/list.svg new file mode 100755 index 000000000..f07692281 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/list.svg @@ -0,0 +1,5 @@ + + +list + + diff --git a/webapp/assets/_new/icons/svgs/location-arrow.svg b/webapp/assets/_new/icons/svgs/location-arrow.svg new file mode 100755 index 000000000..942a37fa5 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/location-arrow.svg @@ -0,0 +1,5 @@ + + +location-arrow + + diff --git a/webapp/assets/_new/icons/svgs/magnet.svg b/webapp/assets/_new/icons/svgs/magnet.svg new file mode 100755 index 000000000..998495351 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/magnet.svg @@ -0,0 +1,5 @@ + + +magnet + + diff --git a/webapp/assets/_new/icons/svgs/male.svg b/webapp/assets/_new/icons/svgs/male.svg new file mode 100755 index 000000000..5c35184b4 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/male.svg @@ -0,0 +1,5 @@ + + +male + + diff --git a/webapp/assets/_new/icons/svgs/map-pin.svg b/webapp/assets/_new/icons/svgs/map-pin.svg new file mode 100755 index 000000000..dbba740b1 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/map-pin.svg @@ -0,0 +1,5 @@ + + +map-pin + + diff --git a/webapp/assets/_new/icons/svgs/map-signs.svg b/webapp/assets/_new/icons/svgs/map-signs.svg new file mode 100755 index 000000000..0d46cc195 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/map-signs.svg @@ -0,0 +1,5 @@ + + +map-signs + + diff --git a/webapp/assets/_new/icons/svgs/map.svg b/webapp/assets/_new/icons/svgs/map.svg new file mode 100755 index 000000000..157a29ea8 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/map.svg @@ -0,0 +1,5 @@ + + +map + + diff --git a/webapp/assets/_new/icons/svgs/microphone.svg b/webapp/assets/_new/icons/svgs/microphone.svg index 121342b70..9a051cebe 100644 --- a/webapp/assets/_new/icons/svgs/microphone.svg +++ b/webapp/assets/_new/icons/svgs/microphone.svg @@ -1,4 +1,3 @@ - microphone diff --git a/webapp/assets/_new/icons/svgs/mobile-phone.svg b/webapp/assets/_new/icons/svgs/mobile-phone.svg new file mode 100755 index 000000000..542ad25b8 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/mobile-phone.svg @@ -0,0 +1,5 @@ + + +mobile-phone + + diff --git a/webapp/assets/_new/icons/svgs/paperclip.svg b/webapp/assets/_new/icons/svgs/paperclip.svg new file mode 100755 index 000000000..099453e71 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/paperclip.svg @@ -0,0 +1,5 @@ + + +paperclip + + diff --git a/webapp/assets/_new/icons/svgs/paste.svg b/webapp/assets/_new/icons/svgs/paste.svg new file mode 100755 index 000000000..32084e808 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/paste.svg @@ -0,0 +1,5 @@ + + +paste + + diff --git a/webapp/assets/_new/icons/svgs/pause.svg b/webapp/assets/_new/icons/svgs/pause.svg new file mode 100755 index 000000000..ce3acef04 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/pause.svg @@ -0,0 +1,5 @@ + + +pause + + diff --git a/webapp/assets/_new/icons/svgs/pencil.svg b/webapp/assets/_new/icons/svgs/pencil.svg new file mode 100755 index 000000000..0c1b963f8 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/pencil.svg @@ -0,0 +1,5 @@ + + +pencil + + diff --git a/webapp/assets/_new/icons/svgs/phone.svg b/webapp/assets/_new/icons/svgs/phone.svg new file mode 100755 index 000000000..413c48f16 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/phone.svg @@ -0,0 +1,5 @@ + + +phone + + diff --git a/webapp/assets/_new/icons/svgs/photo.svg b/webapp/assets/_new/icons/svgs/photo.svg new file mode 100755 index 000000000..abc66b8f3 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/photo.svg @@ -0,0 +1,5 @@ + + +photo + + diff --git a/webapp/assets/_new/icons/svgs/pie-chart.svg b/webapp/assets/_new/icons/svgs/pie-chart.svg new file mode 100755 index 000000000..1d942226e --- /dev/null +++ b/webapp/assets/_new/icons/svgs/pie-chart.svg @@ -0,0 +1,5 @@ + + +pie-chart + + diff --git a/webapp/assets/_new/icons/svgs/play-circle.svg b/webapp/assets/_new/icons/svgs/play-circle.svg new file mode 100755 index 000000000..e8b842bf5 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/play-circle.svg @@ -0,0 +1,5 @@ + + +play-circle + + diff --git a/webapp/assets/_new/icons/svgs/play.svg b/webapp/assets/_new/icons/svgs/play.svg new file mode 100755 index 000000000..a0b1eafa2 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/play.svg @@ -0,0 +1,5 @@ + + +play + + diff --git a/webapp/assets/_new/icons/svgs/power-off.svg b/webapp/assets/_new/icons/svgs/power-off.svg new file mode 100755 index 000000000..7b362c167 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/power-off.svg @@ -0,0 +1,5 @@ + + +power-off + + diff --git a/webapp/assets/_new/icons/svgs/print.svg b/webapp/assets/_new/icons/svgs/print.svg new file mode 100755 index 000000000..a9fc409e2 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/print.svg @@ -0,0 +1,5 @@ + + +print + + diff --git a/webapp/assets/_new/icons/svgs/recycle.svg b/webapp/assets/_new/icons/svgs/recycle.svg new file mode 100755 index 000000000..9bbdb3ad3 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/recycle.svg @@ -0,0 +1,5 @@ + + +recycle + + diff --git a/webapp/assets/_new/icons/svgs/refresh.svg b/webapp/assets/_new/icons/svgs/refresh.svg new file mode 100755 index 000000000..1995ab604 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/refresh.svg @@ -0,0 +1,5 @@ + + +refresh + + diff --git a/webapp/assets/_new/icons/svgs/rocket.svg b/webapp/assets/_new/icons/svgs/rocket.svg new file mode 100755 index 000000000..f83674f15 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/rocket.svg @@ -0,0 +1,5 @@ + + +rocket + + diff --git a/webapp/assets/_new/icons/svgs/server.svg b/webapp/assets/_new/icons/svgs/server.svg new file mode 100755 index 000000000..fa00771ea --- /dev/null +++ b/webapp/assets/_new/icons/svgs/server.svg @@ -0,0 +1,5 @@ + + +server + + diff --git a/webapp/assets/_new/icons/svgs/share.svg b/webapp/assets/_new/icons/svgs/share.svg new file mode 100755 index 000000000..b2fee7622 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/share.svg @@ -0,0 +1,5 @@ + + +share + + diff --git a/webapp/assets/_new/icons/svgs/sort-alpha-asc.svg b/webapp/assets/_new/icons/svgs/sort-alpha-asc.svg new file mode 100755 index 000000000..f96023d0d --- /dev/null +++ b/webapp/assets/_new/icons/svgs/sort-alpha-asc.svg @@ -0,0 +1,5 @@ + + +sort-alpha-asc + + diff --git a/webapp/assets/_new/icons/svgs/sort-alpha-desc.svg b/webapp/assets/_new/icons/svgs/sort-alpha-desc.svg new file mode 100755 index 000000000..461e65611 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/sort-alpha-desc.svg @@ -0,0 +1,5 @@ + + +sort-alpha-desc + + diff --git a/webapp/assets/_new/icons/svgs/sort.svg b/webapp/assets/_new/icons/svgs/sort.svg new file mode 100755 index 000000000..d7fc07efd --- /dev/null +++ b/webapp/assets/_new/icons/svgs/sort.svg @@ -0,0 +1,5 @@ + + +sort + + diff --git a/webapp/assets/_new/icons/svgs/spinner.svg b/webapp/assets/_new/icons/svgs/spinner.svg new file mode 100755 index 000000000..b5d0cf71a --- /dev/null +++ b/webapp/assets/_new/icons/svgs/spinner.svg @@ -0,0 +1,5 @@ + + +spinner + + diff --git a/webapp/assets/_new/icons/svgs/star-half-o.svg b/webapp/assets/_new/icons/svgs/star-half-o.svg new file mode 100755 index 000000000..40070bbef --- /dev/null +++ b/webapp/assets/_new/icons/svgs/star-half-o.svg @@ -0,0 +1,5 @@ + + +star-half-o + + diff --git a/webapp/assets/_new/icons/svgs/star-o.svg b/webapp/assets/_new/icons/svgs/star-o.svg new file mode 100755 index 000000000..3bc7af681 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/star-o.svg @@ -0,0 +1,5 @@ + + +star-o + + diff --git a/webapp/assets/_new/icons/svgs/star.svg b/webapp/assets/_new/icons/svgs/star.svg new file mode 100755 index 000000000..5b4236991 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/star.svg @@ -0,0 +1,5 @@ + + +star + + diff --git a/webapp/assets/_new/icons/svgs/subscript.svg b/webapp/assets/_new/icons/svgs/subscript.svg new file mode 100755 index 000000000..07663bcc6 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/subscript.svg @@ -0,0 +1,5 @@ + + +subscript + + diff --git a/webapp/assets/_new/icons/svgs/sun.svg b/webapp/assets/_new/icons/svgs/sun.svg new file mode 100755 index 000000000..bddbcebcc --- /dev/null +++ b/webapp/assets/_new/icons/svgs/sun.svg @@ -0,0 +1,5 @@ + + +sun-o + + diff --git a/webapp/assets/_new/icons/svgs/superscript.svg b/webapp/assets/_new/icons/svgs/superscript.svg new file mode 100755 index 000000000..08938a2b5 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/superscript.svg @@ -0,0 +1,5 @@ + + +superscript + + diff --git a/webapp/assets/_new/icons/svgs/table.svg b/webapp/assets/_new/icons/svgs/table.svg new file mode 100755 index 000000000..bdb7c8e7b --- /dev/null +++ b/webapp/assets/_new/icons/svgs/table.svg @@ -0,0 +1,5 @@ + + +table + + diff --git a/webapp/assets/_new/icons/svgs/tablet.svg b/webapp/assets/_new/icons/svgs/tablet.svg new file mode 100755 index 000000000..7142d4644 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/tablet.svg @@ -0,0 +1,5 @@ + + +tablet + + diff --git a/webapp/assets/_new/icons/svgs/tag.svg b/webapp/assets/_new/icons/svgs/tag.svg new file mode 100755 index 000000000..875a3be33 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/tag.svg @@ -0,0 +1,5 @@ + + +tag + + diff --git a/webapp/assets/_new/icons/svgs/tags.svg b/webapp/assets/_new/icons/svgs/tags.svg new file mode 100755 index 000000000..f248cc080 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/tags.svg @@ -0,0 +1,5 @@ + + +tags + + diff --git a/webapp/assets/_new/icons/svgs/terminal.svg b/webapp/assets/_new/icons/svgs/terminal.svg new file mode 100755 index 000000000..9f336dc12 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/terminal.svg @@ -0,0 +1,5 @@ + + +terminal + + diff --git a/webapp/assets/_new/icons/svgs/ticket.svg b/webapp/assets/_new/icons/svgs/ticket.svg new file mode 100755 index 000000000..5e7cfb92c --- /dev/null +++ b/webapp/assets/_new/icons/svgs/ticket.svg @@ -0,0 +1,5 @@ + + +ticket + + diff --git a/webapp/assets/_new/icons/svgs/toggle-off.svg b/webapp/assets/_new/icons/svgs/toggle-off.svg new file mode 100755 index 000000000..c69ce1b5f --- /dev/null +++ b/webapp/assets/_new/icons/svgs/toggle-off.svg @@ -0,0 +1,5 @@ + + +toggle-off + + diff --git a/webapp/assets/_new/icons/svgs/toggle-on.svg b/webapp/assets/_new/icons/svgs/toggle-on.svg new file mode 100755 index 000000000..280c7d249 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/toggle-on.svg @@ -0,0 +1,5 @@ + + +toggle-on + + diff --git a/webapp/assets/_new/icons/svgs/undo.svg b/webapp/assets/_new/icons/svgs/undo.svg new file mode 100755 index 000000000..707b60150 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/undo.svg @@ -0,0 +1,5 @@ + + +undo + + diff --git a/webapp/assets/_new/icons/svgs/upload.svg b/webapp/assets/_new/icons/svgs/upload.svg new file mode 100755 index 000000000..83dfe5bcf --- /dev/null +++ b/webapp/assets/_new/icons/svgs/upload.svg @@ -0,0 +1,5 @@ + + +upload + + diff --git a/webapp/assets/_new/icons/svgs/video-camera.svg b/webapp/assets/_new/icons/svgs/video-camera.svg new file mode 100755 index 000000000..b6a9cc0df --- /dev/null +++ b/webapp/assets/_new/icons/svgs/video-camera.svg @@ -0,0 +1,5 @@ + + +video-camera + + diff --git a/webapp/assets/_new/icons/svgs/volume-down.svg b/webapp/assets/_new/icons/svgs/volume-down.svg new file mode 100755 index 000000000..f8b25dc25 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/volume-down.svg @@ -0,0 +1,5 @@ + + +volume-down + + diff --git a/webapp/assets/_new/icons/svgs/volume-off.svg b/webapp/assets/_new/icons/svgs/volume-off.svg new file mode 100755 index 000000000..daaeb19f5 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/volume-off.svg @@ -0,0 +1,5 @@ + + +volume-off + + diff --git a/webapp/assets/_new/icons/svgs/volume-up.svg b/webapp/assets/_new/icons/svgs/volume-up.svg new file mode 100755 index 000000000..03816d0d3 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/volume-up.svg @@ -0,0 +1,5 @@ + + +volume-up + + diff --git a/webapp/assets/_new/icons/svgs/wheelchair.svg b/webapp/assets/_new/icons/svgs/wheelchair.svg new file mode 100755 index 000000000..3854969b8 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/wheelchair.svg @@ -0,0 +1,5 @@ + + +wheelchair + + diff --git a/webapp/assets/_new/icons/svgs/wifi.svg b/webapp/assets/_new/icons/svgs/wifi.svg new file mode 100755 index 000000000..0df70f686 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/wifi.svg @@ -0,0 +1,5 @@ + + +wifi + + diff --git a/webapp/assets/_new/icons/svgs/youtube-play.svg b/webapp/assets/_new/icons/svgs/youtube-play.svg new file mode 100755 index 000000000..80370a4e6 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/youtube-play.svg @@ -0,0 +1,5 @@ + + +youtube-play + + diff --git a/webapp/assets/_new/styles/export.scss b/webapp/assets/_new/styles/export.scss index 88b42bfc9..e29c014e2 100644 --- a/webapp/assets/_new/styles/export.scss +++ b/webapp/assets/_new/styles/export.scss @@ -27,4 +27,11 @@ chatMessageBgOthers: $chat-message-bg-others; chatNewMessageColor: $chat-new-message-color; + + chatMessageTimestamp: $chat-message-timestamp; + chatMessageCheckmarkSeen: $chat-message-checkmark-seen; + chatMessageCheckmark: $chat-message-checkmark; + + chatRoomBackgroundCounterBadge: $chat-room-background-counter-badge; + chatRoomColorCounterBadge: $chat-room-color-counter-badge; } \ No newline at end of file diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index e001ffa84..dd3a042d1 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -417,3 +417,8 @@ $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; +$chat-room-color-counter-badge: $text-color-inverse; +$chat-room-background-counter-badge: $color-secondary; diff --git a/webapp/components/Chat/AddChatRoomByUserSearch.vue b/webapp/components/Chat/AddChatRoomByUserSearch.vue new file mode 100644 index 000000000..8ab21f06f --- /dev/null +++ b/webapp/components/Chat/AddChatRoomByUserSearch.vue @@ -0,0 +1,67 @@ + + + + + + diff --git a/webapp/components/Chat/Chat.vue b/webapp/components/Chat/Chat.vue index 95bf5da95..eb0ce4433 100644 --- a/webapp/components/Chat/Chat.vue +++ b/webapp/components/Chat/Chat.vue @@ -4,7 +4,7 @@ -
-
- -
+
+ + + + + + + +
+ + + +
+
+
@@ -44,7 +68,7 @@
@@ -58,18 +82,28 @@ diff --git a/webapp/components/CommentForm/CommentForm.vue b/webapp/components/CommentForm/CommentForm.vue index 5f6a2420d..6d9b59de6 100644 --- a/webapp/components/CommentForm/CommentForm.vue +++ b/webapp/components/CommentForm/CommentForm.vue @@ -8,7 +8,6 @@ :disabled="disabled && !update" @click="handleCancel" data-test="cancel-button" - danger > {{ $t('actions.cancel') }} diff --git a/webapp/components/ContentMenu/GroupContentMenu.vue b/webapp/components/ContentMenu/GroupContentMenu.vue index 7a7737320..1ca1b5b33 100644 --- a/webapp/components/ContentMenu/GroupContentMenu.vue +++ b/webapp/components/ContentMenu/GroupContentMenu.vue @@ -58,14 +58,14 @@ export default { routes.push({ label: this.$t('group.contentMenu.visitGroupPage'), icon: 'home', - name: 'group-id-slug', + path: `/groups/${this.group.id}`, params: { id: this.group.id, slug: this.group.slug }, }) } if (this.group.myRole === 'owner') { routes.push({ label: this.$t('admin.settings.name'), - path: `/group/edit/${this.group.id}`, + path: `/groups/edit/${this.group.id}`, icon: 'edit', }) } diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index 0067dab72..0a5eba0cd 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -1,190 +1,190 @@ diff --git a/webapp/components/FilterMenu/OrderByFilter.spec.js b/webapp/components/FilterMenu/OrderByFilter.spec.js index 10b52449a..46cffe5f0 100644 --- a/webapp/components/FilterMenu/OrderByFilter.spec.js +++ b/webapp/components/FilterMenu/OrderByFilter.spec.js @@ -35,7 +35,7 @@ describe('OrderByFilter', () => { it('sets "newest-button" attribute `filled`', () => { expect( wrapper - .find('.order-by-filter .filter-list [data-test="newest-button"] .base-button') + .find('.order-by-filter .filter-list .base-button[data-test="newest-button"]') .classes('--filled'), ).toBe(true) }) @@ -43,7 +43,7 @@ describe('OrderByFilter', () => { it('don\'t sets "oldest-button" attribute `filled`', () => { expect( wrapper - .find('.order-by-filter .filter-list [data-test="oldest-button"] .base-button') + .find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]') .classes('--filled'), ).toBe(false) }) @@ -58,7 +58,7 @@ describe('OrderByFilter', () => { it('don\'t sets "newest-button" attribute `filled`', () => { expect( wrapper - .find('.order-by-filter .filter-list [data-test="newest-button"] .base-button') + .find('.order-by-filter .filter-list .base-button[data-test="newest-button"]') .classes('--filled'), ).toBe(false) }) @@ -66,7 +66,7 @@ describe('OrderByFilter', () => { it('sets "oldest-button" attribute `filled`', () => { expect( wrapper - .find('.order-by-filter .filter-list [data-test="oldest-button"] .base-button') + .find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]') .classes('--filled'), ).toBe(true) }) @@ -75,7 +75,7 @@ describe('OrderByFilter', () => { describe('click "newest-button"', () => { it('calls TOGGLE_ORDER with "createdAt_desc"', () => { wrapper - .find('.order-by-filter .filter-list [data-test="newest-button"] .base-button') + .find('.order-by-filter .filter-list .base-button[data-test="newest-button"]') .trigger('click') expect(mutations['posts/TOGGLE_ORDER']).toHaveBeenCalledWith({}, 'createdAt_desc') }) @@ -84,7 +84,7 @@ describe('OrderByFilter', () => { describe('click "oldest-button"', () => { it('calls TOGGLE_ORDER with "createdAt_asc"', () => { wrapper - .find('.order-by-filter .filter-list [data-test="oldest-button"] .base-button') + .find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]') .trigger('click') expect(mutations['posts/TOGGLE_ORDER']).toHaveBeenCalledWith({}, 'createdAt_asc') }) diff --git a/webapp/components/FilterMenu/OrderByFilter.vue b/webapp/components/FilterMenu/OrderByFilter.vue index 8ef248f6d..57c173152 100644 --- a/webapp/components/FilterMenu/OrderByFilter.vue +++ b/webapp/components/FilterMenu/OrderByFilter.vue @@ -2,24 +2,30 @@ @@ -28,13 +34,11 @@ + + diff --git a/webapp/components/Group/AddGroupMember.vue b/webapp/components/Group/AddGroupMember.vue index ff049ca4d..cc3e3b1e8 100644 --- a/webapp/components/Group/AddGroupMember.vue +++ b/webapp/components/Group/AddGroupMember.vue @@ -3,33 +3,7 @@

{{ $t('group.addUser') }}

- - - +
+ + diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index ce97cd53a..cef3a5d45 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -85,7 +85,7 @@ content: $t('chat.userProfileButton.tooltip', { name: userName }), placement: 'bottom-start', }" - @click="showChat({ showChat: true, roomID: user.id })" + @click="showOrChangeChat(user.id)" > {{ $t('chat.userProfileButton.label') }} @@ -182,7 +182,7 @@