mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' into 6500-refactor-filter-menu
This commit is contained in:
commit
b190c56b52
2
.github/file-filters.yml
vendored
2
.github/file-filters.yml
vendored
@ -1,4 +1,5 @@
|
|||||||
backend: &backend
|
backend: &backend
|
||||||
|
- '.github/workflows/test-backend.yml'
|
||||||
- 'backend/**/*'
|
- 'backend/**/*'
|
||||||
- 'neo4j/**/*'
|
- 'neo4j/**/*'
|
||||||
|
|
||||||
@ -6,4 +7,5 @@ docker: &docker
|
|||||||
- 'docker-compose.*'
|
- 'docker-compose.*'
|
||||||
|
|
||||||
webapp: &webapp
|
webapp: &webapp
|
||||||
|
- '.github/workflows/test-webapp.yml'
|
||||||
- 'webapp/**/*'
|
- 'webapp/**/*'
|
||||||
|
|||||||
42
.github/workflows/cleanup-cache-at-pr-closing.yml
vendored
Normal file
42
.github/workflows/cleanup-cache-at-pr-closing.yml
vendored
Normal file
@ -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 }}
|
||||||
74
.github/workflows/test-backend.yml
vendored
74
.github/workflows/test-backend.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: ocelot.social backend test CI
|
name: ocelot.social backend test CI
|
||||||
|
|
||||||
|
|
||||||
on: [push]
|
on: push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
files-changed:
|
files-changed:
|
||||||
@ -13,7 +13,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.3.0
|
- 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
|
uses: dorny/paths-filter@v2.11.1
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
@ -34,12 +34,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
|
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
|
||||||
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
|
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Cache docker images
|
||||||
uses: actions/upload-artifact@v3
|
id: cache-neo4j
|
||||||
|
uses: actions/cache/save@v3.3.1
|
||||||
with:
|
with:
|
||||||
name: docker-neo4j-image
|
|
||||||
path: /tmp/neo4j.tar
|
path: /tmp/neo4j.tar
|
||||||
|
key: ${{ github.run_id }}-backend-neo4j-cache
|
||||||
|
|
||||||
build_test_backend:
|
build_test_backend:
|
||||||
name: Docker Build Test - Backend
|
name: Docker Build Test - Backend
|
||||||
@ -54,12 +55,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
|
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
|
||||||
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
|
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Cache docker images
|
||||||
uses: actions/upload-artifact@v3
|
id: cache-backend
|
||||||
|
uses: actions/cache/save@v3.3.1
|
||||||
with:
|
with:
|
||||||
name: docker-backend-test
|
|
||||||
path: /tmp/backend.tar
|
path: /tmp/backend.tar
|
||||||
|
key: ${{ github.run_id }}-backend-cache
|
||||||
|
|
||||||
lint_backend:
|
lint_backend:
|
||||||
name: Lint Backend
|
name: Lint Backend
|
||||||
@ -84,28 +86,29 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download Docker Image (Neo4J)
|
- name: Restore Neo4J cache
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/cache/restore@v3.3.1
|
||||||
with:
|
with:
|
||||||
name: docker-neo4j-image
|
path: /tmp/neo4j.tar
|
||||||
path: /tmp
|
key: ${{ github.run_id }}-backend-neo4j-cache
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
- name: Load Docker Image
|
- name: Restore Backend cache
|
||||||
run: docker load < /tmp/neo4j.tar
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
|
||||||
- name: Download Docker Image (Backend)
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
with:
|
||||||
name: docker-backend-test
|
path: /tmp/backend.tar
|
||||||
path: /tmp
|
key: ${{ github.run_id }}-backend-cache
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
- name: Load Docker Image
|
- name: Load Docker Images
|
||||||
run: docker load < /tmp/backend.tar
|
run: |
|
||||||
|
docker load < /tmp/neo4j.tar
|
||||||
|
docker load < /tmp/backend.tar
|
||||||
|
|
||||||
- name: backend | copy env files webapp
|
- name: backend | copy env files
|
||||||
run: cp webapp/.env.template webapp/.env
|
run: |
|
||||||
- name: backend | copy env files backend
|
cp webapp/.env.template webapp/.env
|
||||||
run: cp backend/.env.template backend/.env
|
cp backend/.env.template backend/.env
|
||||||
|
|
||||||
- name: backend | docker-compose
|
- name: backend | docker-compose
|
||||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend
|
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
|
- name: backend | Unit test incl. coverage check
|
||||||
run: docker-compose exec -T backend yarn test
|
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
|
||||||
|
|||||||
117
.github/workflows/test-e2e.yml
vendored
117
.github/workflows/test-e2e.yml
vendored
@ -1,9 +1,54 @@
|
|||||||
name: ocelot.social end-to-end test CI
|
name: ocelot.social end-to-end test CI
|
||||||
|
|
||||||
on: push
|
on: push
|
||||||
|
|
||||||
jobs:
|
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:
|
fullstack_tests:
|
||||||
name: Fullstack tests
|
name: Fullstack tests
|
||||||
|
if: success()
|
||||||
|
needs: docker_preparation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
jobs: 8
|
jobs: 8
|
||||||
@ -12,34 +57,56 @@ jobs:
|
|||||||
# run copies of the current job in parallel
|
# run copies of the current job in parallel
|
||||||
job: [1, 2, 3, 4, 5, 6, 7, 8]
|
job: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Restore cache
|
||||||
uses: actions/checkout@v3
|
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
|
- name: Boot up test system | docker-compose
|
||||||
run: cp webapp/.env.template webapp/.env
|
|
||||||
|
|
||||||
- name: backend | copy env file
|
|
||||||
run: cp backend/.env.template backend/.env
|
|
||||||
|
|
||||||
- name: boot up test system | docker-compose
|
|
||||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
|
|
||||||
|
|
||||||
- name: cypress | Fullstack tests
|
|
||||||
id: e2e-tests
|
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
chmod +x /opt/cucumber-json-formatter
|
||||||
yarn install
|
sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter
|
||||||
yarn build
|
docker load < /tmp/images/neo4j.tar
|
||||||
cd ..
|
docker load < /tmp/images/backend.tar
|
||||||
yarn install
|
docker load < /tmp/images/webapp.tar
|
||||||
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
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
|
||||||
# UPLOAD SCREENSHOTS - IF TESTS FAIL #####################################
|
id: e2e-tests
|
||||||
##########################################################################
|
run: yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
||||||
- name: Full stack tests | if any test failed, upload screenshots
|
|
||||||
|
- name: Full stack tests | if tests failed, compile html report
|
||||||
|
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||||
|
run: |
|
||||||
|
cd cypress/
|
||||||
|
node create-cucumber-html-report.js
|
||||||
|
|
||||||
|
- name: Full stack tests | if tests failed, upload report
|
||||||
|
id: e2e-report
|
||||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: cypress-screenshots
|
name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }}
|
||||||
path: cypress/screenshots/
|
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
|
||||||
52
.github/workflows/test-webapp.yml
vendored
52
.github/workflows/test-webapp.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: ocelot.social webapp test CI
|
name: ocelot.social webapp test CI
|
||||||
|
|
||||||
|
|
||||||
on: [push]
|
on: push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
files-changed:
|
files-changed:
|
||||||
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
prepare:
|
prepare:
|
||||||
name: Prepare
|
name: Prepare
|
||||||
if: needs.files-changed.outputs.webapp
|
if: needs.files-changed.outputs.webapp == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -34,30 +34,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
scripts/translations/sort.sh
|
scripts/translations/sort.sh
|
||||||
scripts/translations/missing-keys.sh
|
scripts/translations/missing-keys.sh
|
||||||
|
|
||||||
build_test_webapp:
|
build_test_webapp:
|
||||||
name: Docker Build Test - Webapp
|
name: Docker Build Test - Webapp
|
||||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp
|
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
|
||||||
needs: [files-changed, prepare]
|
needs: [files-changed, prepare]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: webapp | Build 'test' image
|
- name: Webapp | Build 'test' image
|
||||||
run: |
|
run: |
|
||||||
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
|
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
|
||||||
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
|
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Cache docker image
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/cache/save@v3.3.1
|
||||||
with:
|
with:
|
||||||
name: docker-webapp-test
|
|
||||||
path: /tmp/webapp.tar
|
path: /tmp/webapp.tar
|
||||||
|
key: ${{ github.run_id }}-webapp-cache
|
||||||
|
|
||||||
lint_webapp:
|
lint_webapp:
|
||||||
name: Lint Webapp
|
name: Lint Webapp
|
||||||
if: needs.files-changed.outputs.webapp
|
if: needs.files-changed.outputs.webapp == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -69,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
unit_test_webapp:
|
unit_test_webapp:
|
||||||
name: Unit Tests - Webapp
|
name: Unit Tests - Webapp
|
||||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp
|
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
|
||||||
needs: [files-changed, build_test_webapp]
|
needs: [files-changed, build_test_webapp]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@ -78,20 +78,19 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download Docker Image (Webapp)
|
- name: Restore webapp cache
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/cache/restore@v3.3.1
|
||||||
with:
|
with:
|
||||||
name: docker-webapp-test
|
path: /tmp/webapp.tar
|
||||||
path: /tmp
|
key: ${{ github.run_id }}-webapp-cache
|
||||||
|
|
||||||
- name: Load Docker Image
|
- name: Load Docker Image
|
||||||
run: docker load < /tmp/webapp.tar
|
run: docker load < /tmp/webapp.tar
|
||||||
|
|
||||||
- name: backend | copy env files webapp
|
- name: Copy env files
|
||||||
run: cp webapp/.env.template webapp/.env
|
run: |
|
||||||
|
cp webapp/.env.template webapp/.env
|
||||||
- name: backend | copy env files backend
|
cp backend/.env.template backend/.env
|
||||||
run: cp backend/.env.template backend/.env
|
|
||||||
|
|
||||||
- name: backend | docker-compose
|
- name: backend | docker-compose
|
||||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
|
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
|
- name: webapp | Unit tests incl. coverage check
|
||||||
run: docker-compose exec -T webapp yarn test
|
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
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 70,
|
lines: 67,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,13 @@ export const createMessageMutation = () => {
|
|||||||
CreateMessage(roomId: $roomId, content: $content) {
|
CreateMessage(roomId: $roomId, content: $content) {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
|
senderId
|
||||||
|
username
|
||||||
|
avatar
|
||||||
|
date
|
||||||
|
saved
|
||||||
|
distributed
|
||||||
|
seen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -13,16 +20,31 @@ export const createMessageMutation = () => {
|
|||||||
|
|
||||||
export const messageQuery = () => {
|
export const messageQuery = () => {
|
||||||
return gql`
|
return gql`
|
||||||
query ($roomId: ID!) {
|
query ($roomId: ID!, $first: Int, $offset: Int) {
|
||||||
Message(roomId: $roomId) {
|
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) {
|
||||||
_id
|
_id
|
||||||
id
|
id
|
||||||
|
indexId
|
||||||
content
|
content
|
||||||
senderId
|
senderId
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
}
|
||||||
username
|
username
|
||||||
avatar
|
avatar
|
||||||
date
|
date
|
||||||
|
saved
|
||||||
|
distributed
|
||||||
|
seen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const markMessagesAsSeen = () => {
|
||||||
|
return gql`
|
||||||
|
mutation ($messageIds: [String!]) {
|
||||||
|
MarkMessagesAsSeen(messageIds: $messageIds)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
@ -6,18 +6,10 @@ export const createRoomMutation = () => {
|
|||||||
CreateRoom(userId: $userId) {
|
CreateRoom(userId: $userId) {
|
||||||
id
|
id
|
||||||
roomId
|
roomId
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const roomQuery = () => {
|
|
||||||
return gql`
|
|
||||||
query {
|
|
||||||
Room {
|
|
||||||
id
|
|
||||||
roomId
|
|
||||||
roomName
|
roomName
|
||||||
|
lastMessageAt
|
||||||
|
unreadCount
|
||||||
|
#avatar
|
||||||
users {
|
users {
|
||||||
_id
|
_id
|
||||||
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
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
60
backend/src/middleware/chatMiddleware.ts
Normal file
60
backend/src/middleware/chatMiddleware.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import login from './login/loginMiddleware'
|
|||||||
import sentry from './sentryMiddleware'
|
import sentry from './sentryMiddleware'
|
||||||
import languages from './languages/languages'
|
import languages from './languages/languages'
|
||||||
import userInteractions from './userInteractions'
|
import userInteractions from './userInteractions'
|
||||||
|
import chatMiddleware from './chatMiddleware'
|
||||||
|
|
||||||
export default (schema) => {
|
export default (schema) => {
|
||||||
const middlewares = {
|
const middlewares = {
|
||||||
@ -31,6 +32,7 @@ export default (schema) => {
|
|||||||
orderBy,
|
orderBy,
|
||||||
languages,
|
languages,
|
||||||
userInteractions,
|
userInteractions,
|
||||||
|
chatMiddleware,
|
||||||
}
|
}
|
||||||
|
|
||||||
let order = [
|
let order = [
|
||||||
@ -49,6 +51,7 @@ export default (schema) => {
|
|||||||
'softDelete',
|
'softDelete',
|
||||||
'includedFields',
|
'includedFields',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
|
'chatMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
// add permisions middleware at the first position (unless we're seeding)
|
// add permisions middleware at the first position (unless we're seeding)
|
||||||
|
|||||||
@ -408,6 +408,7 @@ export default shield(
|
|||||||
getInviteCode: isAuthenticated, // and inviteRegistration
|
getInviteCode: isAuthenticated, // and inviteRegistration
|
||||||
Room: isAuthenticated,
|
Room: isAuthenticated,
|
||||||
Message: isAuthenticated,
|
Message: isAuthenticated,
|
||||||
|
UnreadRooms: isAuthenticated,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
'*': deny,
|
'*': deny,
|
||||||
@ -463,6 +464,7 @@ export default shield(
|
|||||||
saveCategorySettings: isAuthenticated,
|
saveCategorySettings: isAuthenticated,
|
||||||
CreateRoom: isAuthenticated,
|
CreateRoom: isAuthenticated,
|
||||||
CreateMessage: isAuthenticated,
|
CreateMessage: isAuthenticated,
|
||||||
|
MarkMessagesAsSeen: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: or(isMyOwn, isAdmin),
|
email: or(isMyOwn, isAdmin),
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import { createRoomMutation } from '../../graphql/rooms'
|
import { createRoomMutation, roomQuery } from '../../graphql/rooms'
|
||||||
import { createMessageMutation, messageQuery } from '../../graphql/messages'
|
import { createMessageMutation, messageQuery, markMessagesAsSeen } from '../../graphql/messages'
|
||||||
import createServer from '../../server'
|
import createServer, { pubsub } from '../../server'
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
|
|
||||||
|
const pubsubSpy = jest.spyOn(pubsub, 'publish')
|
||||||
|
|
||||||
let query
|
let query
|
||||||
let mutate
|
let mutate
|
||||||
let authenticatedUser
|
let authenticatedUser
|
||||||
@ -22,6 +24,9 @@ beforeAll(async () => {
|
|||||||
driver,
|
driver,
|
||||||
neode,
|
neode,
|
||||||
user: authenticatedUser,
|
user: authenticatedUser,
|
||||||
|
cypherParams: {
|
||||||
|
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -55,6 +60,10 @@ describe('Message', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('create message', () => {
|
describe('create message', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
@ -77,7 +86,7 @@ describe('Message', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('room does not exist', () => {
|
describe('room does not exist', () => {
|
||||||
it('returns null', async () => {
|
it('returns null and does not publish subscription', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createMessageMutation(),
|
mutation: createMessageMutation(),
|
||||||
@ -92,6 +101,7 @@ describe('Message', () => {
|
|||||||
CreateMessage: null,
|
CreateMessage: null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
expect(pubsubSpy).not.toBeCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -107,7 +117,7 @@ describe('Message', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('user chats in room', () => {
|
describe('user chats in room', () => {
|
||||||
it('returns the message', async () => {
|
it('returns the message and publishes subscriptions', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createMessageMutation(),
|
mutation: createMessageMutation(),
|
||||||
@ -122,9 +132,92 @@ describe('Message', () => {
|
|||||||
CreateMessage: {
|
CreateMessage: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
content: 'Some nice message to other chatting user',
|
content: 'Some nice message to other chatting user',
|
||||||
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
|
saved: true,
|
||||||
|
distributed: false,
|
||||||
|
seen: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
expect(pubsubSpy).toBeCalledWith('ROOM_COUNT_UPDATED', {
|
||||||
|
roomCountUpdated: '1',
|
||||||
|
userId: 'other-chatting-user',
|
||||||
|
})
|
||||||
|
expect(pubsubSpy).toBeCalledWith('CHAT_MESSAGE_ADDED', {
|
||||||
|
chatMessageAdded: expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
content: 'Some nice message to other chatting user',
|
||||||
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
|
saved: true,
|
||||||
|
distributed: false,
|
||||||
|
seen: false,
|
||||||
|
}),
|
||||||
|
userId: 'other-chatting-user',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('room is updated as well', () => {
|
||||||
|
it('has last message set', async () => {
|
||||||
|
const result = await query({ query: roomQuery() })
|
||||||
|
await expect(result).toMatchObject({
|
||||||
|
errors: undefined,
|
||||||
|
data: {
|
||||||
|
Room: [
|
||||||
|
expect.objectContaining({
|
||||||
|
lastMessageAt: expect.any(String),
|
||||||
|
unreadCount: 0,
|
||||||
|
lastMessage: expect.objectContaining({
|
||||||
|
_id: result.data.Room[0].lastMessage.id,
|
||||||
|
id: expect.any(String),
|
||||||
|
content: 'Some nice message to other chatting user',
|
||||||
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
|
saved: true,
|
||||||
|
distributed: false,
|
||||||
|
seen: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unread count for other user', () => {
|
||||||
|
it('has unread count = 1', async () => {
|
||||||
|
authenticatedUser = await otherChattingUser.toJson()
|
||||||
|
await expect(query({ query: roomQuery() })).resolves.toMatchObject({
|
||||||
|
errors: undefined,
|
||||||
|
data: {
|
||||||
|
Room: [
|
||||||
|
expect.objectContaining({
|
||||||
|
lastMessageAt: expect.any(String),
|
||||||
|
unreadCount: 1,
|
||||||
|
lastMessage: expect.objectContaining({
|
||||||
|
_id: expect.any(String),
|
||||||
|
id: expect.any(String),
|
||||||
|
content: 'Some nice message to other chatting user',
|
||||||
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
|
saved: true,
|
||||||
|
distributed: false,
|
||||||
|
seen: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -212,11 +305,15 @@ describe('Message', () => {
|
|||||||
{
|
{
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
_id: result.data.Message[0].id,
|
_id: result.data.Message[0].id,
|
||||||
|
indexId: 0,
|
||||||
content: 'Some nice message to other chatting user',
|
content: 'Some nice message to other chatting user',
|
||||||
senderId: 'chatting-user',
|
senderId: 'chatting-user',
|
||||||
username: 'Chatting User',
|
username: 'Chatting User',
|
||||||
avatar: expect.any(String),
|
avatar: expect.any(String),
|
||||||
date: expect.any(String),
|
date: expect.any(String),
|
||||||
|
saved: true,
|
||||||
|
distributed: true,
|
||||||
|
seen: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -253,17 +350,65 @@ describe('Message', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
Message: expect.arrayContaining([
|
Message: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
indexId: 0,
|
||||||
content: 'Some nice message to other chatting user',
|
content: 'Some nice message to other chatting user',
|
||||||
senderId: 'chatting-user',
|
senderId: 'chatting-user',
|
||||||
username: 'Chatting User',
|
username: 'Chatting User',
|
||||||
avatar: expect.any(String),
|
avatar: expect.any(String),
|
||||||
date: expect.any(String),
|
date: expect.any(String),
|
||||||
|
saved: true,
|
||||||
|
distributed: true,
|
||||||
|
seen: false,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
indexId: 1,
|
||||||
|
content: 'A nice response message to chatting user',
|
||||||
|
senderId: 'other-chatting-user',
|
||||||
|
username: 'Other Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
|
saved: true,
|
||||||
|
distributed: true,
|
||||||
|
seen: false,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
indexId: 2,
|
||||||
|
content: 'And another nice message to other chatting user',
|
||||||
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
|
saved: true,
|
||||||
|
distributed: false,
|
||||||
|
seen: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the messages paginated', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: messageQuery(),
|
||||||
|
variables: {
|
||||||
|
roomId,
|
||||||
|
first: 2,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: undefined,
|
||||||
|
data: {
|
||||||
|
Message: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
indexId: 1,
|
||||||
content: 'A nice response message to chatting user',
|
content: 'A nice response message to chatting user',
|
||||||
senderId: 'other-chatting-user',
|
senderId: 'other-chatting-user',
|
||||||
username: 'Other Chatting User',
|
username: 'Other Chatting User',
|
||||||
@ -272,13 +417,40 @@ describe('Message', () => {
|
|||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
indexId: 2,
|
||||||
content: 'And another nice message to other chatting user',
|
content: 'And another nice message to other chatting user',
|
||||||
senderId: 'chatting-user',
|
senderId: 'chatting-user',
|
||||||
username: 'Chatting User',
|
username: 'Chatting User',
|
||||||
avatar: expect.any(String),
|
avatar: expect.any(String),
|
||||||
date: expect.any(String),
|
date: expect.any(String),
|
||||||
}),
|
}),
|
||||||
]),
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: messageQuery(),
|
||||||
|
variables: {
|
||||||
|
roomId,
|
||||||
|
first: 2,
|
||||||
|
offset: 2,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: undefined,
|
||||||
|
data: {
|
||||||
|
Message: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
indexId: 0,
|
||||||
|
content: 'Some nice message to other chatting user',
|
||||||
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
|
}),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -308,4 +480,74 @@ describe('Message', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('marks massges as seen', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
authenticatedUser = null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: markMessagesAsSeen(),
|
||||||
|
variables: {
|
||||||
|
messageIds: ['some-id'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [{ message: 'Not Authorized!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
const messageIds: string[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
authenticatedUser = await otherChattingUser.toJson()
|
||||||
|
const msgs = await query({
|
||||||
|
query: messageQuery(),
|
||||||
|
variables: {
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
msgs.data.Message.forEach((m) => messageIds.push(m.id))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: markMessagesAsSeen(),
|
||||||
|
variables: {
|
||||||
|
messageIds,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: undefined,
|
||||||
|
data: {
|
||||||
|
MarkMessagesAsSeen: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has seen prop set to true', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: messageQuery(),
|
||||||
|
variables: {
|
||||||
|
roomId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
Message: [
|
||||||
|
expect.objectContaining({ seen: true }),
|
||||||
|
expect.objectContaining({ seen: false }),
|
||||||
|
expect.objectContaining({ seen: true }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,36 @@
|
|||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
import Resolver from './helpers/Resolver'
|
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 {
|
export default {
|
||||||
|
Subscription: {
|
||||||
|
chatMessageAdded: {
|
||||||
|
subscribe: withFilter(
|
||||||
|
() => pubsub.asyncIterator(CHAT_MESSAGE_ADDED),
|
||||||
|
(payload, variables) => {
|
||||||
|
return payload.userId === variables.userId
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
Query: {
|
Query: {
|
||||||
Message: async (object, params, context, resolveInfo) => {
|
Message: async (object, params, context, resolveInfo) => {
|
||||||
const { roomId } = params
|
const { roomId } = params
|
||||||
@ -13,13 +42,24 @@ export default {
|
|||||||
id: context.user.id,
|
id: context.user.id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
|
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
|
||||||
|
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
resolved.forEach((message) => {
|
const undistributedMessagesIds = resolved
|
||||||
message._id = message.id
|
.filter((msg) => !msg.distributed && msg.senderId !== context.user.id)
|
||||||
})
|
.map((msg) => msg.id)
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
|
if (undistributedMessagesIds.length > 0) {
|
||||||
|
await setMessagesAsDistributed(undistributedMessagesIds, session)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
// send subscription to author to updated the messages
|
||||||
}
|
}
|
||||||
return resolved
|
return resolved.reverse()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@ -32,25 +72,59 @@ export default {
|
|||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
const createMessageCypher = `
|
const createMessageCypher = `
|
||||||
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
|
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
|
||||||
|
OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image)
|
||||||
|
OPTIONAL MATCH (m:Message)-[:INSIDE]->(room)
|
||||||
|
OPTIONAL MATCH (room)<-[:CHATS_IN]-(recipientUser:User)
|
||||||
|
WHERE NOT recipientUser.id = $currentUserId
|
||||||
|
WITH MAX(m.indexId) as maxIndex, room, currentUser, image, recipientUser
|
||||||
CREATE (currentUser)-[:CREATED]->(message:Message {
|
CREATE (currentUser)-[:CREATED]->(message:Message {
|
||||||
createdAt: toString(datetime()),
|
createdAt: toString(datetime()),
|
||||||
id: apoc.create.uuid(),
|
id: apoc.create.uuid(),
|
||||||
content: $content
|
indexId: CASE WHEN maxIndex IS NOT NULL THEN maxIndex + 1 ELSE 0 END,
|
||||||
|
content: $content,
|
||||||
|
saved: true,
|
||||||
|
distributed: false,
|
||||||
|
seen: false
|
||||||
})-[:INSIDE]->(room)
|
})-[: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, {
|
const createMessageTxResponse = await transaction.run(createMessageCypher, {
|
||||||
currentUserId,
|
currentUserId,
|
||||||
roomId,
|
roomId,
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [message] = await createMessageTxResponse.records.map((record) =>
|
const [message] = await createMessageTxResponse.records.map((record) =>
|
||||||
record.get('message'),
|
record.get('message'),
|
||||||
)
|
)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const message = await writeTxResultPromise
|
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
|
return message
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
@ -58,6 +132,32 @@ export default {
|
|||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
MarkMessagesAsSeen: async (_parent, params, context, _resolveInfo) => {
|
||||||
|
const { messageIds } = params
|
||||||
|
const currentUserId = context.user.id
|
||||||
|
const session = context.driver.session()
|
||||||
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
|
const setSeenCypher = `
|
||||||
|
MATCH (m:Message)<-[:CREATED]-(user:User)
|
||||||
|
WHERE m.id IN $messageIds AND NOT user.id = $currentUserId
|
||||||
|
SET m.seen = true
|
||||||
|
RETURN m { .* }
|
||||||
|
`
|
||||||
|
const setSeenTxResponse = await transaction.run(setSeenCypher, {
|
||||||
|
messageIds,
|
||||||
|
currentUserId,
|
||||||
|
})
|
||||||
|
const messages = await setSeenTxResponse.records.map((record) => record.get('m'))
|
||||||
|
return messages
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await writeTxResultPromise
|
||||||
|
// send subscription to author to updated the messages
|
||||||
|
return true
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Message: {
|
Message: {
|
||||||
...Resolver('Message', {
|
...Resolver('Message', {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
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'
|
import createServer from '../../server'
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
@ -21,6 +22,9 @@ beforeAll(async () => {
|
|||||||
driver,
|
driver,
|
||||||
neode,
|
neode,
|
||||||
user: authenticatedUser,
|
user: authenticatedUser,
|
||||||
|
cypherParams: {
|
||||||
|
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -34,6 +38,8 @@ afterAll(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Room', () => {
|
describe('Room', () => {
|
||||||
|
let roomId: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([
|
;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([
|
||||||
Factory.build('user', {
|
Factory.build('user', {
|
||||||
@ -48,6 +54,14 @@ describe('Room', () => {
|
|||||||
id: 'not-chatting-user',
|
id: 'not-chatting-user',
|
||||||
name: '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', () => {
|
describe('authenticated', () => {
|
||||||
let roomId: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
authenticatedUser = await chattingUser.toJson()
|
authenticatedUser = await chattingUser.toJson()
|
||||||
})
|
})
|
||||||
@ -122,6 +134,26 @@ describe('Room', () => {
|
|||||||
CreateRoom: {
|
CreateRoom: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
roomId: result.data.CreateRoom.id,
|
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),
|
id: expect.any(String),
|
||||||
roomId: result.data.Room[0].id,
|
roomId: result.data.Room[0].id,
|
||||||
roomName: 'Chatting User',
|
roomName: 'Chatting User',
|
||||||
|
unreadCount: 0,
|
||||||
users: expect.arrayContaining([
|
users: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
_id: 'chatting-user',
|
_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: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,29 +1,50 @@
|
|||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
import Resolver from './helpers/Resolver'
|
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 {
|
export default {
|
||||||
|
Subscription: {
|
||||||
|
roomCountUpdated: {
|
||||||
|
subscribe: withFilter(
|
||||||
|
() => pubsub.asyncIterator(ROOM_COUNT_UPDATED),
|
||||||
|
(payload, variables) => {
|
||||||
|
return payload.userId === variables.userId
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
Query: {
|
Query: {
|
||||||
Room: async (object, params, context, resolveInfo) => {
|
Room: async (object, params, context, resolveInfo) => {
|
||||||
if (!params.filter) params.filter = {}
|
if (!params.filter) params.filter = {}
|
||||||
params.filter.users_some = {
|
params.filter.users_some = {
|
||||||
id: context.user.id,
|
id: context.user.id,
|
||||||
}
|
}
|
||||||
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
|
return neo4jgraphql(object, params, context, resolveInfo)
|
||||||
if (resolved) {
|
},
|
||||||
resolved.forEach((room) => {
|
UnreadRooms: async (object, params, context, resolveInfo) => {
|
||||||
if (room.users) {
|
const {
|
||||||
// buggy, you must query the username for this to function correctly
|
user: { id: currentUserId },
|
||||||
room.roomName = room.users.filter((user) => user.id !== context.user.id)[0].name
|
} = context
|
||||||
room.avatar =
|
const session = context.driver.session()
|
||||||
room.users.filter((user) => user.id !== context.user.id)[0].avatar?.url ||
|
try {
|
||||||
'default-avatar'
|
const count = await getUnreadRoomsCount(currentUserId, session)
|
||||||
room.users.forEach((user) => {
|
return count
|
||||||
user._id = user.id
|
} finally {
|
||||||
})
|
session.close()
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return resolved
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@ -44,7 +65,17 @@ export default {
|
|||||||
ON CREATE SET
|
ON CREATE SET
|
||||||
room.createdAt = toString(datetime()),
|
room.createdAt = toString(datetime()),
|
||||||
room.id = apoc.create.uuid()
|
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, {
|
const createRommTxResponse = await transaction.run(createRoomCypher, {
|
||||||
userId,
|
userId,
|
||||||
@ -68,6 +99,7 @@ export default {
|
|||||||
},
|
},
|
||||||
Room: {
|
Room: {
|
||||||
...Resolver('Room', {
|
...Resolver('Room', {
|
||||||
|
undefinedToNull: ['lastMessageAt'],
|
||||||
hasMany: {
|
hasMany: {
|
||||||
users: '<-[:CHATS_IN]-(related:User)',
|
users: '<-[:CHATS_IN]-(related:User)',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -25,4 +25,3 @@ type LocationMapBox {
|
|||||||
type Query {
|
type Query {
|
||||||
queryLocations(place: String!, lang: String!): [LocationMapBox]!
|
queryLocations(place: String!, lang: String!): [LocationMapBox]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,13 @@
|
|||||||
# room: _RoomFilter
|
# room: _RoomFilter
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
enum _MessageOrdering {
|
||||||
|
indexId_desc
|
||||||
|
}
|
||||||
|
|
||||||
type Message {
|
type Message {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
indexId: Int!
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
|
|
||||||
@ -16,6 +21,10 @@ type Message {
|
|||||||
username: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.name")
|
username: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.name")
|
||||||
avatar: String @cypher(statement: "MATCH (this)<-[:CREATED]-(:User)-[:AVATAR_IMAGE]->(image:Image) RETURN image.url")
|
avatar: String @cypher(statement: "MATCH (this)<-[:CREATED]-(:User)-[:AVATAR_IMAGE]->(image:Image) RETURN image.url")
|
||||||
date: String! @cypher(statement: "RETURN this.createdAt")
|
date: String! @cypher(statement: "RETURN this.createdAt")
|
||||||
|
|
||||||
|
saved: Boolean
|
||||||
|
distributed: Boolean
|
||||||
|
seen: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
@ -23,8 +32,19 @@ type Mutation {
|
|||||||
roomId: ID!
|
roomId: ID!
|
||||||
content: String!
|
content: String!
|
||||||
): Message
|
): Message
|
||||||
|
|
||||||
|
MarkMessagesAsSeen(messageIds: [String!]): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
Message(roomId: ID!): [Message]
|
Message(
|
||||||
|
roomId: ID!,
|
||||||
|
first: Int
|
||||||
|
offset: Int
|
||||||
|
orderBy: [_MessageOrdering]
|
||||||
|
): [Message]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscription {
|
||||||
|
chatMessageAdded(userId: ID!): Message
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,8 +84,8 @@ input _PostFilter {
|
|||||||
group: _GroupFilter
|
group: _GroupFilter
|
||||||
postsInMyGroups: Boolean
|
postsInMyGroups: Boolean
|
||||||
postType_in: [PostType]
|
postType_in: [PostType]
|
||||||
eventStart_gte: String
|
eventStart_gte: String
|
||||||
eventEnd_gte: String
|
eventEnd_gte: String
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _PostOrdering {
|
enum _PostOrdering {
|
||||||
|
|||||||
@ -5,6 +5,11 @@
|
|||||||
# users_some: _UserFilter
|
# users_some: _UserFilter
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
# TODO change this to last message date
|
||||||
|
enum _RoomOrdering {
|
||||||
|
lastMessageAt_desc
|
||||||
|
}
|
||||||
|
|
||||||
type Room {
|
type Room {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: String
|
createdAt: String
|
||||||
@ -13,8 +18,28 @@ type Room {
|
|||||||
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
||||||
|
|
||||||
roomId: String! @cypher(statement: "RETURN this.id")
|
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")
|
roomName: String! @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user.name")
|
||||||
avatar: String! ## @cypher match not own user in users array
|
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 {
|
type Mutation {
|
||||||
@ -24,5 +49,13 @@ type Mutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
Room: [Room]
|
Room(
|
||||||
|
id: ID
|
||||||
|
orderBy: [_RoomOrdering]
|
||||||
|
): [Room]
|
||||||
|
UnreadRooms: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscription {
|
||||||
|
roomCountUpdated(userId: ID!): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import bodyParser from 'body-parser'
|
|||||||
import { graphqlUploadExpress } from 'graphql-upload'
|
import { graphqlUploadExpress } from 'graphql-upload'
|
||||||
|
|
||||||
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
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
|
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
|
||||||
let prodPubsub, devPubsub
|
let prodPubsub, devPubsub
|
||||||
const options = {
|
const options = {
|
||||||
|
|||||||
12
cypress/create-cucumber-html-report.js
Normal file
12
cypress/create-cucumber-html-report.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const report = require("multiple-cucumber-html-reporter");
|
||||||
|
|
||||||
|
const reportTitle = "Ocelot webapp end-to-end test report"
|
||||||
|
|
||||||
|
report.generate({
|
||||||
|
jsonDir: "reports/json_logs",
|
||||||
|
reportPath: "./reports/cucumber_html_report",
|
||||||
|
pageTitle: reportTitle,
|
||||||
|
reportName: reportTitle,
|
||||||
|
pageFooter: "<div></div>",
|
||||||
|
hideMetadata: true
|
||||||
|
});
|
||||||
@ -21,13 +21,7 @@ async function setupNodeEvents(on, config) {
|
|||||||
return testStore[name]
|
return testStore[name]
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
on("after:run", (results) => {
|
|
||||||
if (results) {
|
|
||||||
console.log(results.status);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,10 +36,7 @@ module.exports = defineConfig({
|
|||||||
baseUrl: "http://localhost:3000",
|
baseUrl: "http://localhost:3000",
|
||||||
specPattern: "cypress/e2e/**/*.feature",
|
specPattern: "cypress/e2e/**/*.feature",
|
||||||
supportFile: "cypress/support/e2e.js",
|
supportFile: "cypress/support/e2e.js",
|
||||||
retries: {
|
retries: 0,
|
||||||
runMode: 2,
|
|
||||||
openMode: 0,
|
|
||||||
},
|
|
||||||
video: false,
|
video: false,
|
||||||
setupNodeEvents,
|
setupNodeEvents,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
|
||||||
/* globals Cypress cy */
|
/* globals Cypress cy */
|
||||||
import "cypress-file-upload";
|
|
||||||
import { GraphQLClient, request } from 'graphql-request'
|
import { GraphQLClient, request } from 'graphql-request'
|
||||||
import CONFIG from '../../backend/build/src/config'
|
import CONFIG from '../../backend/build/src/config'
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,27 @@
|
|||||||
import { Then } from "@badeball/cypress-cucumber-preprocessor";
|
import { Then } from "@badeball/cypress-cucumber-preprocessor";
|
||||||
|
|
||||||
Then("I should be able to {string} a teaser image", condition => {
|
Then("I should be able to {string} a teaser image", condition => {
|
||||||
// cy.reload()
|
let postTeaserImage = ""
|
||||||
|
|
||||||
switch(condition){
|
switch(condition){
|
||||||
case 'change':
|
case "change":
|
||||||
cy.get('.delete-image-button')
|
postTeaserImage = "humanconnection.png"
|
||||||
|
cy.get(".delete-image-button")
|
||||||
.click()
|
.click()
|
||||||
cy.fixture('humanconnection.png').as('postTeaserImage').then(function() {
|
cy.get("#postdropzone").selectFile(
|
||||||
cy.get("#postdropzone").upload(
|
{ contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" },
|
||||||
{ fileContent: this.postTeaserImage, fileName: 'humanconnection.png', mimeType: "image/png" },
|
{ action: "drag-drop", force: true }
|
||||||
{ subjectType: "drag-n-drop", force: true }
|
).wait(750);
|
||||||
).wait(750);
|
|
||||||
})
|
|
||||||
break;
|
break;
|
||||||
case 'add':
|
case "add":
|
||||||
cy.fixture('onourjourney.png').as('postTeaserImage').then(function() {
|
postTeaserImage = "onourjourney.png"
|
||||||
cy.get("#postdropzone").upload(
|
cy.get("#postdropzone").selectFile(
|
||||||
{ fileContent: this.postTeaserImage, fileName: 'onourjourney.png', mimeType: "image/png" },
|
{ contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" },
|
||||||
{ subjectType: "drag-n-drop", force: true }
|
{ action: "drag-drop", force: true }
|
||||||
).wait(750);
|
).wait(750);
|
||||||
})
|
|
||||||
break;
|
break;
|
||||||
case 'remove':
|
case "remove":
|
||||||
cy.get('.delete-image-button')
|
cy.get(".delete-image-button")
|
||||||
.click()
|
.click()
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,11 @@ import { Then } from "@badeball/cypress-cucumber-preprocessor";
|
|||||||
|
|
||||||
Then("I should be able to change my profile picture", () => {
|
Then("I should be able to change my profile picture", () => {
|
||||||
const avatarUpload = "onourjourney.png";
|
const avatarUpload = "onourjourney.png";
|
||||||
|
|
||||||
cy.fixture(avatarUpload, "base64").then(fileContent => {
|
cy.get("#customdropzone").selectFile(
|
||||||
cy.get("#customdropzone").upload(
|
{ contents: `cypress/fixtures/${avatarUpload}`, fileName: avatarUpload, mimeType: "image/png" },
|
||||||
{ fileContent, fileName: avatarUpload, mimeType: "image/png" },
|
{ action: "drag-drop" }
|
||||||
{ subjectType: "drag-n-drop", force: true }
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
cy.get(".profile-page-avatar img")
|
cy.get(".profile-page-avatar img")
|
||||||
.should("have.attr", "src")
|
.should("have.attr", "src")
|
||||||
.and("contains", "onourjourney");
|
.and("contains", "onourjourney");
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
When you overtake this deploy and rebrand repo to your network you have to recognize the following changes and doings:
|
When you overtake this deploy and rebrand repo to your network you have to recognize the following changes and doings:
|
||||||
|
|
||||||
|
## Version >= 2.7.0 with 'ocelotDockerVersionTag' 2.7.0-470
|
||||||
|
|
||||||
|
- You have to rename all `.js` files to `.ts` in `branding/constants`
|
||||||
|
|
||||||
## Version >= 2.4.0 with 'ocelotDockerVersionTag' 2.4.0-298
|
## Version >= 2.4.0 with 'ocelotDockerVersionTag' 2.4.0-298
|
||||||
|
|
||||||
- You have to set `SHOW_CONTENT_FILTER_HEADER_MENU` and `SHOW_CONTENT_FILTER_MASONRY_GRID` in `branding/constants/filter.js` originally in main code file `webapp/constants/filter.js` to your preferred value.
|
- You have to set `SHOW_CONTENT_FILTER_HEADER_MENU` and `SHOW_CONTENT_FILTER_MASONRY_GRID` in `branding/constants/filter.js` originally in main code file `webapp/constants/filter.js` to your preferred value.
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 350237c62dcff1a5c34f1e8d718f89b05ce3d33f
|
Subproject commit fdc2e52fa444b300e1c4736600bc0e9ae3314222
|
||||||
20
package.json
20
package.json
@ -10,13 +10,25 @@
|
|||||||
"url": "https://github.com/Ocelot-Social-Community/Ocelot-Social.git"
|
"url": "https://github.com/Ocelot-Social-Community/Ocelot-Social.git"
|
||||||
},
|
},
|
||||||
"cypress-cucumber-preprocessor": {
|
"cypress-cucumber-preprocessor": {
|
||||||
"nonGlobalStepDefinitions": true
|
"stepDefinitions": "cypress/support/step_definitions/**/*.js",
|
||||||
|
"json": {
|
||||||
|
"enabled": true,
|
||||||
|
"output": "cypress/reports/json_logs/cucumber_log.json",
|
||||||
|
"formatter": "cucumber-json-formatter"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"enabled": true,
|
||||||
|
"output": "cypress/reports/json_logs/messages.ndjson"
|
||||||
|
},
|
||||||
|
"html": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"db:seed": "cd backend && yarn run db:seed",
|
"db:seed": "cd backend && yarn run db:seed",
|
||||||
"db:reset": "cd backend && yarn run db:reset",
|
"db:reset": "cd backend && yarn run db:reset",
|
||||||
"cypress:run": "cypress run --browser electron --config-file ./cypress/cypress.config.js",
|
"cypress:run": "cypress run --e2e --browser electron --config-file ./cypress/cypress.config.js",
|
||||||
"cypress:open": "cypress open --browser electron --config-file ./cypress/cypress.config.js",
|
"cypress:open": "cypress open --e2e --browser electron --config-file ./cypress/cypress.config.js",
|
||||||
"cucumber:setup": "cd backend && yarn run dev",
|
"cucumber:setup": "cd backend && yarn run dev",
|
||||||
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
||||||
"release": "yarn version --no-git-tag-version --no-commit-hooks --no-commit && auto-changelog --latest-version $(node -p -e \"require('./package.json').version\") && cd backend && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp/maintenance/source && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../../../package.json').version\")"
|
"release": "yarn version --no-git-tag-version --no-commit-hooks --no-commit && auto-changelog --latest-version $(node -p -e \"require('./package.json').version\") && cd backend && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp/maintenance/source && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../../../package.json').version\")"
|
||||||
@ -33,7 +45,6 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cucumber": "^6.0.5",
|
"cucumber": "^6.0.5",
|
||||||
"cypress": "^12.17.0",
|
"cypress": "^12.17.0",
|
||||||
"cypress-file-upload": "^3.5.3",
|
|
||||||
"cypress-network-idle": "^1.14.2",
|
"cypress-network-idle": "^1.14.2",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
@ -42,6 +53,7 @@
|
|||||||
"import": "^0.0.6",
|
"import": "^0.0.6",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mock-socket": "^9.0.3",
|
"mock-socket": "^9.0.3",
|
||||||
|
"multiple-cucumber-html-reporter": "^3.4.0",
|
||||||
"neo4j-driver": "^4.3.4",
|
"neo4j-driver": "^4.3.4",
|
||||||
"neode": "^0.4.8",
|
"neode": "^0.4.8",
|
||||||
"rosie": "^2.1.0",
|
"rosie": "^2.1.0",
|
||||||
|
|||||||
4
webapp/assets/_new/icons/svgs/chat-bubble.svg
Normal file
4
webapp/assets/_new/icons/svgs/chat-bubble.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||||
|
<title>chat-bubble</title>
|
||||||
|
<g data-name="Chat"><path d="M27.23,24.91A9,9,0,0,0,22.44,9.52,8.57,8.57,0,0,0,21,9.41a8.94,8.94,0,0,0-8.92,10.14c0,.1,0,.2,0,.29a9,9,0,0,0,.22,1l0,.11a8.93,8.93,0,0,0,.38,1l.13.28a9,9,0,0,0,.45.83l.07.14a9.13,9.13,0,0,1-2.94-.36,1,1,0,0,0-.68,0L7,24.13l.54-1.9a1,1,0,0,0-.32-1,9,9,0,0,1,11.27-14,1,1,0,0,0,1.23-1.58A10.89,10.89,0,0,0,13,3.25a11,11,0,0,0-7.5,19l-1,3.34A1,1,0,0,0,5.9,26.82l4.35-1.93a11,11,0,0,0,4.68.16A9,9,0,0,0,21,27.41a8.81,8.81,0,0,0,2.18-.27l3.41,1.52A1,1,0,0,0,28,27.48Zm-1.77-1.1a1,1,0,0,0-.32,1L25.45,26l-1.79-.8a1,1,0,0,0-.41-.09,1,1,0,0,0-.29,0,6.64,6.64,0,0,1-2,.29,7,7,0,0,1,0-14,6.65,6.65,0,0,1,1.11.09,7,7,0,0,1,3.35,12.31Z"></path><path d="M17.82 17.08H17a1 1 0 0 0 0 2h.82a1 1 0 0 0 0-2zM21.41 17.08h-.82a1 1 0 0 0 0 2h.82a1 1 0 0 0 0-2zM25 17.08h-.82a1 1 0 0 0 0 2H25a1 1 0 0 0 0-2z"></path></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 961 B |
34
webapp/assets/_new/styles/export.scss
Normal file
34
webapp/assets/_new/styles/export.scss
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
:export {
|
||||||
|
colorPrimary: $color-primary;
|
||||||
|
colorPrimaryActive: $color-primary-active;
|
||||||
|
colorPrimaryLight: $color-primary-light;
|
||||||
|
|
||||||
|
borderColorSoft: $border-color-soft;
|
||||||
|
|
||||||
|
borderRadiusBase: $border-radius-base;
|
||||||
|
|
||||||
|
textColorBase: $text-color-base;
|
||||||
|
textColorSoft: $text-color-soft;
|
||||||
|
textColorInverse: $text-color-inverse;
|
||||||
|
|
||||||
|
boxShadowBase: $box-shadow-base;
|
||||||
|
|
||||||
|
backgroundColorBase: $background-color-base;
|
||||||
|
backgroundColorSoft: $background-color-soft;
|
||||||
|
backgroundColorSoftest: $background-color-softest;
|
||||||
|
backgroundColorPrimary: $background-color-primary;
|
||||||
|
|
||||||
|
colorNeutral30: $color-neutral-30;
|
||||||
|
|
||||||
|
chatMessageColor: $chat-message-color;
|
||||||
|
|
||||||
|
chatMessageBgMe: $chat-message-bg-me;
|
||||||
|
chatMessageBgOthers: $chat-message-bg-others;
|
||||||
|
|
||||||
|
chatNewMessageColor: $chat-new-message-color;
|
||||||
|
|
||||||
|
chatMessageTimestamp: $chat-message-timestamp;
|
||||||
|
chatMessageCheckmarkSeen: $chat-message-checkmark-seen;
|
||||||
|
chatMessageCheckmark: $chat-message-checkmark;
|
||||||
|
}
|
||||||
@ -406,4 +406,17 @@ $color-toast-green: $color-success;
|
|||||||
$color-ribbon-event: $background-color-third;
|
$color-ribbon-event: $background-color-third;
|
||||||
$color-ribbon-event-active: $background-color-third-active;
|
$color-ribbon-event-active: $background-color-third-active;
|
||||||
$color-ribbon-article: $background-color-secondary;
|
$color-ribbon-article: $background-color-secondary;
|
||||||
$color-ribbon-article-active: $background-color-secondary-active;
|
$color-ribbon-article-active: $background-color-secondary-active;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tokens Chat Color
|
||||||
|
*/
|
||||||
|
|
||||||
|
$chat-message-bg-me: $color-primary-light;
|
||||||
|
$chat-message-color: $text-color-base;
|
||||||
|
$chat-message-bg-others: $color-neutral-80;
|
||||||
|
$chat-sidemenu-bg: $color-secondary-active;
|
||||||
|
$chat-new-message-color: $color-secondary-active;
|
||||||
|
$chat-message-timestamp: $text-color-soft;
|
||||||
|
$chat-message-checkmark-seen: $text-color-secondary;
|
||||||
|
$chat-message-checkmark: $text-color-soft;
|
||||||
|
|||||||
@ -4,40 +4,84 @@
|
|||||||
<vue-advanced-chat
|
<vue-advanced-chat
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
:current-user-id="currentUser.id"
|
:current-user-id="currentUser.id"
|
||||||
:room-id="null"
|
:room-id="!singleRoom ? roomId : null"
|
||||||
:template-actions="JSON.stringify(templatesText)"
|
:template-actions="JSON.stringify(templatesText)"
|
||||||
:menu-actions="JSON.stringify(menuActions)"
|
:menu-actions="JSON.stringify(menuActions)"
|
||||||
:text-messages="JSON.stringify(textMessages)"
|
:text-messages="JSON.stringify(textMessages)"
|
||||||
|
:message-actions="messageActions"
|
||||||
:messages="JSON.stringify(messages)"
|
:messages="JSON.stringify(messages)"
|
||||||
:messages-loaded="messagesLoaded"
|
:messages-loaded="messagesLoaded"
|
||||||
:rooms="JSON.stringify(rooms)"
|
:rooms="JSON.stringify(rooms)"
|
||||||
:room-actions="JSON.stringify(roomActions)"
|
:room-actions="JSON.stringify(roomActions)"
|
||||||
:rooms-loaded="true"
|
:rooms-loaded="roomsLoaded"
|
||||||
|
:loading-rooms="loadingRooms"
|
||||||
show-files="false"
|
show-files="false"
|
||||||
show-audio="false"
|
show-audio="false"
|
||||||
|
:styles="JSON.stringify(computedChatStyle)"
|
||||||
:show-footer="true"
|
:show-footer="true"
|
||||||
@send-message="sendMessage($event.detail[0])"
|
@send-message="sendMessage($event.detail[0])"
|
||||||
@fetch-messages="fetchMessages($event.detail[0])"
|
@fetch-messages="fetchMessages($event.detail[0])"
|
||||||
|
@fetch-more-rooms="fetchRooms"
|
||||||
:responsive-breakpoint="responsiveBreakpoint"
|
:responsive-breakpoint="responsiveBreakpoint"
|
||||||
:single-room="singleRoom"
|
:single-room="singleRoom"
|
||||||
|
show-reaction-emojis="false"
|
||||||
@show-demo-options="showDemoOptions = $event"
|
@show-demo-options="showDemoOptions = $event"
|
||||||
/>
|
>
|
||||||
|
<div slot="menu-icon" @click.prevent.stop="$emit('close-single-room', true)">
|
||||||
|
<div v-if="singleRoom">
|
||||||
|
<ds-icon name="close"></ds-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="room-header-avatar">
|
||||||
|
<div
|
||||||
|
v-if="selectedRoom && selectedRoom.avatar && selectedRoom.avatar !== 'default-avatar'"
|
||||||
|
class="vac-avatar"
|
||||||
|
:style="{ 'background-image': `url('${selectedRoom.avatar}')` }"
|
||||||
|
/>
|
||||||
|
<div v-else-if="selectedRoom" class="vac-avatar">
|
||||||
|
<span class="initials">{{ getInitialsName(selectedRoom.roomName) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="room in rooms" :slot="'room-list-avatar_' + room.id" :key="room.id">
|
||||||
|
<div
|
||||||
|
v-if="room.avatar && room.avatar !== 'default-avatar'"
|
||||||
|
class="vac-avatar"
|
||||||
|
:style="{ 'background-image': `url('${room.avatar}')` }"
|
||||||
|
/>
|
||||||
|
<div v-else class="vac-avatar">
|
||||||
|
<span class="initials">{{ getInitialsName(room.roomName) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</vue-advanced-chat>
|
||||||
</client-only>
|
</client-only>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { roomQuery, createRoom } from '~/graphql/Rooms'
|
import { roomQuery, createRoom, unreadRoomsQuery } from '~/graphql/Rooms'
|
||||||
import { messageQuery, createMessageMutation } from '~/graphql/Messages'
|
import {
|
||||||
import { mapGetters } from 'vuex'
|
messageQuery,
|
||||||
|
createMessageMutation,
|
||||||
|
chatMessageAdded,
|
||||||
|
markMessagesAsSeen,
|
||||||
|
} from '~/graphql/Messages'
|
||||||
|
import chatStyle from '~/constants/chat.js'
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Chat',
|
name: 'Chat',
|
||||||
props: {
|
props: {
|
||||||
theme: {
|
theme: {
|
||||||
type: String,
|
type: String,
|
||||||
|
default: 'light',
|
||||||
},
|
},
|
||||||
singleRoomId: {
|
singleRoom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
roomId: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
@ -45,28 +89,37 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
menuActions: [
|
menuActions: [
|
||||||
/* {
|
// NOTE: if menuActions is empty, the related slot is not shown
|
||||||
name: 'inviteUser',
|
|
||||||
title: 'Invite User',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'removeUser',
|
name: 'dummyItem',
|
||||||
title: 'Remove User',
|
title: 'Just a dummy item',
|
||||||
},
|
},
|
||||||
{
|
/*
|
||||||
name: 'deleteRoom',
|
{
|
||||||
title: 'Delete Room',
|
name: 'inviteUser',
|
||||||
}, */
|
title: 'Invite User',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'removeUser',
|
||||||
|
title: 'Remove User',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deleteRoom',
|
||||||
|
title: 'Delete Room',
|
||||||
|
},
|
||||||
|
*/
|
||||||
],
|
],
|
||||||
messageActions: [
|
messageActions: [
|
||||||
{
|
/*
|
||||||
name: 'addMessageToFavorite',
|
{
|
||||||
title: 'Add To Favorite',
|
name: 'addMessageToFavorite',
|
||||||
},
|
title: 'Add To Favorite',
|
||||||
{
|
},
|
||||||
name: 'shareMessage',
|
{
|
||||||
title: 'Share Message',
|
name: 'shareMessage',
|
||||||
},
|
title: 'Share Message',
|
||||||
|
},
|
||||||
|
*/
|
||||||
],
|
],
|
||||||
templatesText: [
|
templatesText: [
|
||||||
{
|
{
|
||||||
@ -78,37 +131,30 @@ export default {
|
|||||||
text: 'This is the action',
|
text: 'This is the action',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
textMessages: {
|
|
||||||
ROOMS_EMPTY: 'Aucune conversation',
|
|
||||||
ROOM_EMPTY: 'Aucune conversation sélectionnée',
|
|
||||||
NEW_MESSAGES: 'Nouveaux messages',
|
|
||||||
MESSAGE_DELETED: 'Ce message a été supprimé',
|
|
||||||
MESSAGES_EMPTY: 'Aucun message',
|
|
||||||
CONVERSATION_STARTED: 'La conversation a commencée le :',
|
|
||||||
TYPE_MESSAGE: 'Tapez votre message',
|
|
||||||
SEARCH: 'Rechercher',
|
|
||||||
IS_ONLINE: 'est en ligne',
|
|
||||||
LAST_SEEN: 'dernière connexion ',
|
|
||||||
IS_TYPING: 'est en train de taper...',
|
|
||||||
CANCEL_SELECT_MESSAGE: 'Annuler Sélection',
|
|
||||||
},
|
|
||||||
roomActions: [
|
roomActions: [
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
name: 'archiveRoom',
|
name: 'archiveRoom',
|
||||||
title: 'Archive Room',
|
title: 'Archive Room',
|
||||||
},
|
},
|
||||||
{ name: 'inviteUser', title: 'Invite User' },
|
{ name: 'inviteUser', title: 'Invite User' },
|
||||||
{ name: 'removeUser', title: 'Remove User' },
|
{ name: 'removeUser', title: 'Remove User' },
|
||||||
{ name: 'deleteRoom', title: 'Delete Room' },
|
{ name: 'deleteRoom', title: 'Delete Room' },
|
||||||
*/
|
*/
|
||||||
],
|
],
|
||||||
rooms: [],
|
|
||||||
messages: [],
|
|
||||||
messagesLoaded: true,
|
|
||||||
showDemoOptions: true,
|
showDemoOptions: true,
|
||||||
responsiveBreakpoint: 600,
|
responsiveBreakpoint: 600,
|
||||||
singleRoom: !!this.singleRoomId || false,
|
rooms: [],
|
||||||
|
roomsLoaded: false,
|
||||||
|
roomPage: 0,
|
||||||
|
roomPageSize: 10,
|
||||||
|
selectedRoom: this.roomId,
|
||||||
|
loadingRooms: true,
|
||||||
|
messagesLoaded: false,
|
||||||
|
messagePage: 0,
|
||||||
|
messagePageSize: 20,
|
||||||
|
messages: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -117,11 +163,11 @@ export default {
|
|||||||
.mutate({
|
.mutate({
|
||||||
mutation: createRoom(),
|
mutation: createRoom(),
|
||||||
variables: {
|
variables: {
|
||||||
userId: this.singleRoomId,
|
userId: this.roomId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(({ data: { CreateRoom } }) => {
|
||||||
this.$apollo.queries.Rooms.refetch()
|
this.fetchRooms({ room: CreateRoom })
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error(error)
|
this.$toast.error(error)
|
||||||
@ -129,41 +175,171 @@ export default {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
// this.loading = false
|
// this.loading = false
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
this.fetchRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscriptions
|
||||||
|
const observer = this.$apollo.subscribe({
|
||||||
|
query: chatMessageAdded(),
|
||||||
|
variables: {
|
||||||
|
userId: this.currentUser.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.subscribe({
|
||||||
|
next: this.chatMessageAdded,
|
||||||
|
error(error) {
|
||||||
|
this.$toast.error(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'auth/user',
|
currentUser: 'auth/user',
|
||||||
}),
|
}),
|
||||||
|
computedChatStyle() {
|
||||||
|
return chatStyle.STYLE.light
|
||||||
|
},
|
||||||
|
textMessages() {
|
||||||
|
return {
|
||||||
|
ROOMS_EMPTY: this.$t('chat.roomsEmpty'),
|
||||||
|
ROOM_EMPTY: this.$t('chat.roomEmpty'),
|
||||||
|
NEW_MESSAGES: this.$t('chat.newMessages'),
|
||||||
|
MESSAGE_DELETED: this.$t('chat.messageDeleted'),
|
||||||
|
MESSAGES_EMPTY: this.$t('chat.messagesEmpty'),
|
||||||
|
CONVERSATION_STARTED: this.$t('chat.conversationStarted'),
|
||||||
|
TYPE_MESSAGE: this.$t('chat.typeMessage'),
|
||||||
|
SEARCH: this.$t('chat.search'),
|
||||||
|
IS_ONLINE: this.$t('chat.isOnline'),
|
||||||
|
LAST_SEEN: this.$t('chat.lastSeen'),
|
||||||
|
IS_TYPING: this.$t('chat.isTyping'),
|
||||||
|
CANCEL_SELECT_MESSAGE: this.$t('chat.cancelSelectMessage'),
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchMessages({ room, options = {} }) {
|
...mapMutations({
|
||||||
this.messagesLoaded = false
|
commitUnreadRoomCount: 'chat/UPDATE_ROOM_COUNT',
|
||||||
setTimeout(async () => {
|
}),
|
||||||
try {
|
async fetchRooms({ room } = {}) {
|
||||||
const {
|
this.roomsLoaded = false
|
||||||
data: { Message },
|
const offset = this.roomPage * this.roomPageSize
|
||||||
} = await this.$apollo.query({
|
try {
|
||||||
query: messageQuery(),
|
const {
|
||||||
variables: {
|
data: { Room },
|
||||||
roomId: room.id,
|
} = await this.$apollo.query({
|
||||||
},
|
query: roomQuery(),
|
||||||
fetchPolicy: 'no-cache',
|
variables: {
|
||||||
})
|
id: room?.id,
|
||||||
this.messages = Message
|
first: this.roomPageSize,
|
||||||
} catch (error) {
|
offset,
|
||||||
this.messages = []
|
},
|
||||||
this.$toast.error(error.message)
|
fetchPolicy: 'no-cache',
|
||||||
|
})
|
||||||
|
|
||||||
|
const newRooms = Room.map((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
users: r.users.map((u) => {
|
||||||
|
return { ...u, username: u.name, avatar: u.avatar?.url }
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.rooms = [...this.rooms, ...newRooms]
|
||||||
|
|
||||||
|
if (Room.length < this.roomPageSize) {
|
||||||
|
this.roomsLoaded = true
|
||||||
}
|
}
|
||||||
this.messagesLoaded = true
|
this.roomPage += 1
|
||||||
})
|
} catch (error) {
|
||||||
|
this.rooms = []
|
||||||
|
this.$toast.error(error.message)
|
||||||
|
}
|
||||||
|
// must be set false after initial rooms are loaded and never changed again
|
||||||
|
this.loadingRooms = false
|
||||||
},
|
},
|
||||||
|
|
||||||
refetchMessage(roomId) {
|
async fetchMessages({ room, options = {} }) {
|
||||||
this.fetchMessages({ room: this.rooms.find((r) => r.roomId === roomId) })
|
if (this.selectedRoom?.id !== room.id) {
|
||||||
|
this.messages = []
|
||||||
|
this.messagePage = 0
|
||||||
|
this.selectedRoom = room
|
||||||
|
}
|
||||||
|
this.messagesLoaded = options.refetch ? this.messagesLoaded : false
|
||||||
|
const offset = (options.refetch ? 0 : this.messagePage) * this.messagePageSize
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { Message },
|
||||||
|
} = await this.$apollo.query({
|
||||||
|
query: messageQuery(),
|
||||||
|
variables: {
|
||||||
|
roomId: room.id,
|
||||||
|
first: this.messagePageSize,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
})
|
||||||
|
|
||||||
|
const newMsgIds = Message.filter((m) => m.seen === false).map((m) => m.id)
|
||||||
|
if (newMsgIds.length) {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: markMessagesAsSeen(),
|
||||||
|
variables: {
|
||||||
|
messageIds: newMsgIds,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: unreadRoomsQuery(),
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
})
|
||||||
|
.then(({ data: { UnreadRooms } }) => {
|
||||||
|
this.commitUnreadRoomCount(UnreadRooms)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgs = []
|
||||||
|
;[...this.messages, ...Message].forEach((m) => {
|
||||||
|
if (m.senderId !== this.currentUser.id) m.seen = true
|
||||||
|
m.date = new Date(m.date).toDateString()
|
||||||
|
msgs[m.indexId] = m
|
||||||
|
})
|
||||||
|
this.messages = msgs.filter(Boolean)
|
||||||
|
|
||||||
|
if (Message.length < this.messagePageSize) {
|
||||||
|
this.messagesLoaded = true
|
||||||
|
}
|
||||||
|
this.messagePage += 1
|
||||||
|
} catch (error) {
|
||||||
|
this.messages = []
|
||||||
|
this.$toast.error(error.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async chatMessageAdded({ data }) {
|
||||||
|
if (data.chatMessageAdded.room.id === this.selectedRoom?.id) {
|
||||||
|
this.fetchMessages({ room: this.selectedRoom, options: { refetch: true } })
|
||||||
|
} else {
|
||||||
|
// TODO this might be optimized selectively (first page vs rest)
|
||||||
|
this.rooms = []
|
||||||
|
this.roomPage = 0
|
||||||
|
this.roomsLoaded = false
|
||||||
|
this.fetchRooms()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendMessage(message) {
|
async sendMessage(message) {
|
||||||
|
// check for usersTag and change userid to username
|
||||||
|
message.usersTag.forEach((userTag) => {
|
||||||
|
const needle = `<usertag>${userTag.id}</usertag>`
|
||||||
|
const replacement = `<usertag>@${userTag.name.replaceAll(' ', '-').toLowerCase()}</usertag>`
|
||||||
|
message.content = message.content.replaceAll(needle, replacement)
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
await this.$apollo.mutate({
|
await this.$apollo.mutate({
|
||||||
mutation: createMessageMutation(),
|
mutation: createMessageMutation(),
|
||||||
@ -175,40 +351,15 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$toast.error(error.message)
|
this.$toast.error(error.message)
|
||||||
}
|
}
|
||||||
this.refetchMessage(message.roomId)
|
this.fetchMessages({
|
||||||
|
room: this.rooms.find((r) => r.roomId === message.roomId),
|
||||||
|
options: { refetch: true },
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
|
||||||
apollo: {
|
|
||||||
Rooms: {
|
|
||||||
query() {
|
|
||||||
return roomQuery()
|
|
||||||
},
|
|
||||||
update({ Room }) {
|
|
||||||
if (!Room) {
|
|
||||||
this.rooms = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backend result needs mapping of the following values
|
getInitialsName(fullname) {
|
||||||
// room[i].users[j].name -> room[i].users[j].username
|
if (!fullname) return
|
||||||
// room[i].users[j].avatar.url -> room[i].users[j].avatar
|
return fullname.match(/\b\w/g).join('').substring(0, 3).toUpperCase()
|
||||||
// also filter rooms for the single room
|
|
||||||
this.rooms = Room.map((r) => {
|
|
||||||
return {
|
|
||||||
...r,
|
|
||||||
users: r.users.map((u) => {
|
|
||||||
return { ...u, username: u.name, avatar: u.avatar?.url }
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}).filter((r) =>
|
|
||||||
this.singleRoom ? r.users.filter((u) => u.id === this.singleRoomId).length > 0 : true,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
error(error) {
|
|
||||||
this.rooms = []
|
|
||||||
this.$toast.error(error.message)
|
|
||||||
},
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -217,4 +368,25 @@ export default {
|
|||||||
body {
|
body {
|
||||||
font-family: 'Quicksand', sans-serif;
|
font-family: 'Quicksand', sans-serif;
|
||||||
}
|
}
|
||||||
|
.vac-avatar {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: $color-primary-dark;
|
||||||
|
color: $text-color-primary-inverse;
|
||||||
|
height: 42px;
|
||||||
|
width: 42px;
|
||||||
|
min-height: 42px;
|
||||||
|
min-width: 42px;
|
||||||
|
margin-right: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> .initials {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,140 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link v-if="!unreadChatNotificationsCount" class="chat-menu" :to="{ name: 'chat' }">
|
<nuxt-link class="chat-notification-menu" :to="{ name: 'chat' }">
|
||||||
<base-button
|
<base-button
|
||||||
ghost
|
ghost
|
||||||
circle
|
circle
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t('notifications.headerMenuButton.chat'),
|
content: $t('header.chat.tooltip'),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<img src="/img/empty/chat-bubble.svg" />
|
<counter-icon icon="chat-bubble" :count="unreadRoomCount" danger />
|
||||||
</base-button>
|
</base-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<dropdown v-else class="chat-notifications-menu" offset="8">
|
|
||||||
<template #default="{ toggleMenu }">
|
|
||||||
<base-button
|
|
||||||
ghost
|
|
||||||
circle
|
|
||||||
v-tooltip="{
|
|
||||||
content: $t('notifications.headerMenuButton.tooltip'),
|
|
||||||
placement: 'bottom-start',
|
|
||||||
}"
|
|
||||||
@click="toggleMenu"
|
|
||||||
>
|
|
||||||
<counter-icon icon="envelope" :count="unreadChatNotificationsCount" danger />
|
|
||||||
<img src="/img/empty/chat-bubble.svg" />
|
|
||||||
</base-button>
|
|
||||||
</template>
|
|
||||||
<template #popover="{}">
|
|
||||||
<div class="chat-notifications-menu-popover">
|
|
||||||
<div v-for="notification in notifications" v-bind:key="notification.roomid">
|
|
||||||
<ds-space>
|
|
||||||
<div
|
|
||||||
class="chat-notifications-menu-popover-item"
|
|
||||||
@click="
|
|
||||||
$store.commit('chat/SET_OPEN_CHAT', { showChat: true, roomID: notification.roomid })
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<p>{{ notification.name }}</p>
|
|
||||||
{{ notification.title }}
|
|
||||||
</div>
|
|
||||||
</ds-space>
|
|
||||||
</div>
|
|
||||||
<!-- <notification-list :notifications="notifications" /> -->
|
|
||||||
</div>
|
|
||||||
<ds-flex class="chat-notifications-link-container">
|
|
||||||
<ds-flex-item
|
|
||||||
class="chat-notifications-link-container-item"
|
|
||||||
:width="{ base: '100%' }"
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
<nuxt-link :to="{ name: 'chat' }">
|
|
||||||
<base-button ghost primary>All Chat Messages</base-button>
|
|
||||||
</nuxt-link>
|
|
||||||
</ds-flex-item>
|
|
||||||
</ds-flex>
|
|
||||||
</template>
|
|
||||||
</dropdown>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import { unreadRoomsQuery, roomCountUpdated } from '~/graphql/Rooms'
|
||||||
// import NotificationList from '../NotificationList/NotificationList'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatNotificationMenu',
|
name: 'ChatNotificationMenu',
|
||||||
components: {
|
components: {
|
||||||
CounterIcon,
|
CounterIcon,
|
||||||
Dropdown,
|
|
||||||
// NotificationList,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
notifications: [
|
|
||||||
{ roomid: 'u1', name: 'Jenny', title: 'last Message from Jenny' },
|
|
||||||
{ roomid: 'u2', name: 'Honey', title: 'last Message from Honey' },
|
|
||||||
{ roomid: 'u3', name: 'Bob der Baumeister', title: 'last Message from Bob der Baumeister' },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
unreadChatNotificationsCount() {
|
...mapGetters({
|
||||||
const result = this.notifications.reduce((count, notification) => {
|
user: 'auth/user',
|
||||||
return notification.read ? count : count + 1
|
unreadRoomCount: 'chat/unreadRoomCount',
|
||||||
}, 0)
|
}),
|
||||||
return result
|
},
|
||||||
},
|
methods: {
|
||||||
hasNotifications() {
|
...mapMutations({
|
||||||
return this.notifications.length
|
commitUnreadRoomCount: 'chat/UPDATE_ROOM_COUNT',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
UnreadRooms: {
|
||||||
|
query() {
|
||||||
|
return unreadRoomsQuery()
|
||||||
|
},
|
||||||
|
update({ UnreadRooms }) {
|
||||||
|
this.commitUnreadRoomCount(UnreadRooms)
|
||||||
|
},
|
||||||
|
subscribeToMore: {
|
||||||
|
document: roomCountUpdated(),
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
userId: this.user.id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateQuery: (previousResult, { subscriptionData }) => {
|
||||||
|
return { UnreadRooms: subscriptionData.data.roomCountUpdated }
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
|
||||||
.chat-notifications-menu {
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
.vue-popover-theme {
|
|
||||||
z-index: 1000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.counter-icon {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
> .count {
|
|
||||||
position: absolute;
|
|
||||||
top: -$space-xx-small;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: $size-icon-base;
|
|
||||||
min-width: $size-icon-base;
|
|
||||||
padding: 3px; // magic number to center count
|
|
||||||
border-radius: 50%;
|
|
||||||
transform: translateX(50%);
|
|
||||||
|
|
||||||
color: $color-neutral-100;
|
|
||||||
background-color: $color-primary;
|
|
||||||
font-size: 10px; // magic number to center count
|
|
||||||
line-height: 1;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&.--danger {
|
|
||||||
background-color: $color-danger;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.--inactive {
|
|
||||||
background-color: $color-neutral-60;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.--soft {
|
|
||||||
background-color: $color-neutral-90;
|
|
||||||
color: $text-color-soft;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -74,6 +74,10 @@
|
|||||||
<!-- locale switch -->
|
<!-- locale switch -->
|
||||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
|
<!-- chat menu -->
|
||||||
|
<client-only>
|
||||||
|
<chat-notification-menu placement="top" />
|
||||||
|
</client-only>
|
||||||
<!-- notification menu -->
|
<!-- notification menu -->
|
||||||
<client-only>
|
<client-only>
|
||||||
<notification-menu placement="top" />
|
<notification-menu placement="top" />
|
||||||
@ -92,10 +96,6 @@
|
|||||||
<client-only v-if="!isEmpty(this.$env.MAPBOX_TOKEN)">
|
<client-only v-if="!isEmpty(this.$env.MAPBOX_TOKEN)">
|
||||||
<map-button />
|
<map-button />
|
||||||
</client-only>
|
</client-only>
|
||||||
<!-- chat menü -->
|
|
||||||
<client-only>
|
|
||||||
<chat-notification-menu placement="top" />
|
|
||||||
</client-only>
|
|
||||||
<!-- avatar menu -->
|
<!-- avatar menu -->
|
||||||
<client-only>
|
<client-only>
|
||||||
<avatar-menu placement="top" />
|
<avatar-menu placement="top" />
|
||||||
@ -131,10 +131,16 @@
|
|||||||
<!-- mobile hamburger menu -->
|
<!-- mobile hamburger menu -->
|
||||||
<ds-flex-item class="mobile-hamburger-menu">
|
<ds-flex-item class="mobile-hamburger-menu">
|
||||||
<client-only>
|
<client-only>
|
||||||
|
<!-- chat menu -->
|
||||||
|
<div style="display: inline-flex">
|
||||||
|
<chat-notification-menu />
|
||||||
|
</div>
|
||||||
|
<!-- notification menu -->
|
||||||
<div style="display: inline-flex; padding-right: 20px">
|
<div style="display: inline-flex; padding-right: 20px">
|
||||||
<notification-menu />
|
<notification-menu />
|
||||||
</div>
|
</div>
|
||||||
</client-only>
|
</client-only>
|
||||||
|
<!-- hamburger menu -->
|
||||||
<base-button icon="bars" @click="toggleMobileMenuView" circle />
|
<base-button icon="bars" @click="toggleMobileMenuView" circle />
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
@ -257,10 +263,11 @@ import isEmpty from 'lodash/isEmpty'
|
|||||||
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
||||||
import { SHOW_CONTENT_FILTER_HEADER_MENU } from '~/constants/filter.js'
|
import { SHOW_CONTENT_FILTER_HEADER_MENU } from '~/constants/filter.js'
|
||||||
import LOGOS from '~/constants/logos.js'
|
import LOGOS from '~/constants/logos.js'
|
||||||
import headerMenu from '~/constants/headerMenu.js'
|
|
||||||
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
|
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
|
||||||
|
import ChatNotificationMenu from '~/components/ChatNotificationMenu/ChatNotificationMenu'
|
||||||
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
||||||
import GroupButton from '~/components/Group/GroupButton'
|
import GroupButton from '~/components/Group/GroupButton'
|
||||||
|
import headerMenu from '~/constants/headerMenu.js'
|
||||||
import InviteButton from '~/components/InviteButton/InviteButton'
|
import InviteButton from '~/components/InviteButton/InviteButton'
|
||||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||||
import Logo from '~/components/Logo/Logo'
|
import Logo from '~/components/Logo/Logo'
|
||||||
@ -269,7 +276,6 @@ import SearchField from '~/components/features/SearchField/SearchField.vue'
|
|||||||
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
|
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
|
||||||
import links from '~/constants/links.js'
|
import links from '~/constants/links.js'
|
||||||
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
|
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
|
||||||
import ChatNotificationMenu from '~/components/ChatNotificationMenu/ChatNotificationMenu'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@ -146,6 +146,7 @@ export default {
|
|||||||
const {
|
const {
|
||||||
data: { notificationAdded: newNotification },
|
data: { notificationAdded: newNotification },
|
||||||
} = subscriptionData
|
} = subscriptionData
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notifications: unionBy(
|
notifications: unionBy(
|
||||||
[newNotification],
|
[newNotification],
|
||||||
|
|||||||
299
webapp/constants/chat.js
Normal file
299
webapp/constants/chat.js
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import tokens from './../assets/_new/styles/tokens.scss'
|
||||||
|
// import branding from './../assets/styles/imports/_branding.scss'
|
||||||
|
|
||||||
|
const styleData = tokens
|
||||||
|
|
||||||
|
const STYLE = {
|
||||||
|
light: {
|
||||||
|
general: {
|
||||||
|
color: styleData.textColorBase,
|
||||||
|
colorButtonClear: '#1976d2',
|
||||||
|
colorButton: '#fff',
|
||||||
|
backgroundColorButton: '#1976d2',
|
||||||
|
backgroundInput: '#fff',
|
||||||
|
colorPlaceholder: styleData.textColorSoft,
|
||||||
|
colorCaret: '#1976d2',
|
||||||
|
colorSpinner: styleData.colorPrimary,
|
||||||
|
borderStyle: '1px solid #e1e4e8',
|
||||||
|
backgroundScrollIcon: '#fff',
|
||||||
|
},
|
||||||
|
|
||||||
|
container: {
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: styleData.borderRadiusBase,
|
||||||
|
boxShadow: styleData.boxShadowBase,
|
||||||
|
},
|
||||||
|
|
||||||
|
header: {
|
||||||
|
background: styleData.backgroundColorSoft,
|
||||||
|
colorRoomName: styleData.textColorBase,
|
||||||
|
colorRoomInfo: styleData.textColorSoft,
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
background: styleData.backgroundColorSoft,
|
||||||
|
borderStyleInput: '1px solid #e1e4e8',
|
||||||
|
borderInputSelected: '#1976d2',
|
||||||
|
backgroundReply: styleData.backgroundColorSoft,
|
||||||
|
backgroundTagActive: styleData.backgroundColorSoft,
|
||||||
|
backgroundTag: styleData.backgroundColorBase,
|
||||||
|
},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
background: styleData.backgroundColorBase,
|
||||||
|
},
|
||||||
|
|
||||||
|
sidemenu: {
|
||||||
|
background: '#fff',
|
||||||
|
backgroundHover: '#f6f6f6',
|
||||||
|
backgroundActive: styleData.colorPrimaryLight,
|
||||||
|
colorActive: '#1976d2',
|
||||||
|
borderColorSearch: '#e1e5e8',
|
||||||
|
},
|
||||||
|
|
||||||
|
dropdown: {
|
||||||
|
background: '#fff',
|
||||||
|
backgroundHover: '#f6f6f6',
|
||||||
|
},
|
||||||
|
|
||||||
|
message: {
|
||||||
|
background: styleData.chatMessageBgOthers,
|
||||||
|
backgroundMe: styleData.chatMessageBgMe,
|
||||||
|
color: styleData.chatMessageColor,
|
||||||
|
colorStarted: '#9ca6af',
|
||||||
|
backgroundDeleted: '#dadfe2',
|
||||||
|
backgroundSelected: '#c2dcf2',
|
||||||
|
colorDeleted: '#757e85',
|
||||||
|
colorUsername: '#9ca6af',
|
||||||
|
colorTimestamp: styleData.chatMessageTimestamp,
|
||||||
|
backgroundDate: '#e5effa',
|
||||||
|
colorDate: '#505a62',
|
||||||
|
backgroundSystem: '#e5effa',
|
||||||
|
colorSystem: '#505a62',
|
||||||
|
backgroundMedia: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
backgroundReply: 'rgba(0, 0, 0, 0.08)',
|
||||||
|
colorReplyUsername: '#0a0a0a',
|
||||||
|
colorReply: '#6e6e6e',
|
||||||
|
colorTag: '#0d579c',
|
||||||
|
backgroundImage: '#ddd',
|
||||||
|
colorNewMessages: styleData.chatNewMessageColor,
|
||||||
|
backgroundScrollCounter: '#0696c7',
|
||||||
|
colorScrollCounter: '#fff',
|
||||||
|
backgroundReaction: '#eee',
|
||||||
|
borderStyleReaction: '1px solid #eee',
|
||||||
|
backgroundReactionHover: '#fff',
|
||||||
|
borderStyleReactionHover: '1px solid #ddd',
|
||||||
|
colorReactionCounter: '#0a0a0a',
|
||||||
|
backgroundReactionMe: '#cfecf5',
|
||||||
|
borderStyleReactionMe: '1px solid #3b98b8',
|
||||||
|
backgroundReactionHoverMe: '#cfecf5',
|
||||||
|
borderStyleReactionHoverMe: '1px solid #3b98b8',
|
||||||
|
colorReactionCounterMe: '#0b59b3',
|
||||||
|
backgroundAudioRecord: '#eb4034',
|
||||||
|
backgroundAudioLine: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
backgroundAudioProgress: '#455247',
|
||||||
|
backgroundAudioProgressSelector: '#455247',
|
||||||
|
colorFileExtension: '#757e85',
|
||||||
|
},
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
background: 'rgba(239, 239, 239, 0.7)',
|
||||||
|
border: 'rgba(212, 212, 212, 0.9)',
|
||||||
|
color: '#e01e5a',
|
||||||
|
colorMulti: '#0a0a0a',
|
||||||
|
},
|
||||||
|
|
||||||
|
room: {
|
||||||
|
colorUsername: '#0a0a0a',
|
||||||
|
colorMessage: '#67717a',
|
||||||
|
colorTimestamp: '#a2aeb8',
|
||||||
|
colorStateOnline: '#4caf50',
|
||||||
|
colorStateOffline: '#9ca6af',
|
||||||
|
backgroundCounterBadge: '#0696c7',
|
||||||
|
colorCounterBadge: '#fff',
|
||||||
|
},
|
||||||
|
|
||||||
|
emoji: {
|
||||||
|
background: '#fff',
|
||||||
|
},
|
||||||
|
|
||||||
|
icons: {
|
||||||
|
search: '#9ca6af',
|
||||||
|
add: styleData.colorPrimary,
|
||||||
|
toggle: styleData.colorNeutral30,
|
||||||
|
menu: styleData.colorNeutral30,
|
||||||
|
close: '#9ca6af',
|
||||||
|
closeImage: '#fff',
|
||||||
|
file: styleData.colorPrimary,
|
||||||
|
paperclip: styleData.colorPrimary,
|
||||||
|
closeOutline: '#000',
|
||||||
|
closePreview: '#fff',
|
||||||
|
send: styleData.colorPrimary,
|
||||||
|
sendDisabled: '#9ca6af',
|
||||||
|
emoji: styleData.colorPrimary,
|
||||||
|
emojiReaction: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
document: styleData.colorPrimary,
|
||||||
|
pencil: '#9e9e9e',
|
||||||
|
checkmark: styleData.chatMessageCheckmark,
|
||||||
|
checkmarkSeen: styleData.chatMessageCheckmarkSeen,
|
||||||
|
eye: '#fff',
|
||||||
|
dropdownMessage: '#fff',
|
||||||
|
dropdownMessageBackground: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
dropdownRoom: '#9e9e9e',
|
||||||
|
dropdownScroll: '#0a0a0a',
|
||||||
|
microphone: styleData.colorPrimary,
|
||||||
|
audioPlay: '#455247',
|
||||||
|
audioPause: '#455247',
|
||||||
|
audioCancel: '#eb4034',
|
||||||
|
audioConfirm: '#1ba65b',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
general: {
|
||||||
|
color: '#fff',
|
||||||
|
colorButtonClear: '#fff',
|
||||||
|
colorButton: '#fff',
|
||||||
|
backgroundColorButton: '#1976d2',
|
||||||
|
backgroundInput: '#202223',
|
||||||
|
colorPlaceholder: '#596269',
|
||||||
|
colorCaret: '#fff',
|
||||||
|
colorSpinner: '#fff',
|
||||||
|
borderStyle: 'none',
|
||||||
|
backgroundScrollIcon: '#fff',
|
||||||
|
},
|
||||||
|
|
||||||
|
container: {
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12)',
|
||||||
|
},
|
||||||
|
|
||||||
|
header: {
|
||||||
|
background: '#181a1b',
|
||||||
|
colorRoomName: '#fff',
|
||||||
|
colorRoomInfo: '#9ca6af',
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
background: '#131415',
|
||||||
|
borderStyleInput: 'none',
|
||||||
|
borderInputSelected: '#1976d2',
|
||||||
|
backgroundReply: '#1b1c1c',
|
||||||
|
backgroundTagActive: '#1b1c1c',
|
||||||
|
backgroundTag: '#131415',
|
||||||
|
},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
background: '#131415',
|
||||||
|
},
|
||||||
|
|
||||||
|
sidemenu: {
|
||||||
|
background: '#181a1b',
|
||||||
|
backgroundHover: '#202224',
|
||||||
|
backgroundActive: '#151617',
|
||||||
|
colorActive: '#fff',
|
||||||
|
borderColorSearch: '#181a1b',
|
||||||
|
},
|
||||||
|
|
||||||
|
dropdown: {
|
||||||
|
background: '#2a2c33',
|
||||||
|
backgroundHover: '#26282e',
|
||||||
|
},
|
||||||
|
|
||||||
|
message: {
|
||||||
|
background: '#22242a',
|
||||||
|
backgroundMe: '#1f7e80',
|
||||||
|
color: '#fff',
|
||||||
|
colorStarted: '#9ca6af',
|
||||||
|
backgroundDeleted: '#1b1c21',
|
||||||
|
backgroundSelected: '#c2dcf2',
|
||||||
|
colorDeleted: '#a2a5a8',
|
||||||
|
colorUsername: '#b3bac9',
|
||||||
|
colorTimestamp: '#ebedf2',
|
||||||
|
backgroundDate: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
colorDate: '#bec5cc',
|
||||||
|
backgroundSystem: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
colorSystem: '#bec5cc',
|
||||||
|
backgroundMedia: 'rgba(0, 0, 0, 0.18)',
|
||||||
|
backgroundReply: 'rgba(0, 0, 0, 0.18)',
|
||||||
|
colorReplyUsername: '#fff',
|
||||||
|
colorReply: '#d6d6d6',
|
||||||
|
colorTag: '#f0c60a',
|
||||||
|
backgroundImage: '#ddd',
|
||||||
|
colorNewMessages: '#fff',
|
||||||
|
backgroundScrollCounter: '#1976d2',
|
||||||
|
colorScrollCounter: '#fff',
|
||||||
|
backgroundReaction: 'none',
|
||||||
|
borderStyleReaction: 'none',
|
||||||
|
backgroundReactionHover: '#202223',
|
||||||
|
borderStyleReactionHover: 'none',
|
||||||
|
colorReactionCounter: '#fff',
|
||||||
|
backgroundReactionMe: '#4e9ad1',
|
||||||
|
borderStyleReactionMe: 'none',
|
||||||
|
backgroundReactionHoverMe: '#4e9ad1',
|
||||||
|
borderStyleReactionHoverMe: 'none',
|
||||||
|
colorReactionCounterMe: '#fff',
|
||||||
|
backgroundAudioRecord: '#eb4034',
|
||||||
|
backgroundAudioLine: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
backgroundAudioProgress: '#b7d4d3',
|
||||||
|
backgroundAudioProgressSelector: '#b7d4d3',
|
||||||
|
colorFileExtension: '#a2a5a8',
|
||||||
|
},
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
background: 'rgba(239, 239, 239, 0.7)',
|
||||||
|
border: 'rgba(212, 212, 212, 0.9)',
|
||||||
|
color: '#e01e5a',
|
||||||
|
colorMulti: '#0a0a0a',
|
||||||
|
},
|
||||||
|
|
||||||
|
room: {
|
||||||
|
colorUsername: '#fff',
|
||||||
|
colorMessage: '#6c7278',
|
||||||
|
colorTimestamp: '#6c7278',
|
||||||
|
colorStateOnline: '#4caf50',
|
||||||
|
colorStateOffline: '#596269',
|
||||||
|
backgroundCounterBadge: '#1976d2',
|
||||||
|
colorCounterBadge: '#fff',
|
||||||
|
},
|
||||||
|
|
||||||
|
emoji: {
|
||||||
|
background: '#343740',
|
||||||
|
},
|
||||||
|
|
||||||
|
icons: {
|
||||||
|
search: '#596269',
|
||||||
|
add: '#fff',
|
||||||
|
toggle: '#fff',
|
||||||
|
menu: '#fff',
|
||||||
|
close: '#9ca6af',
|
||||||
|
closeImage: '#fff',
|
||||||
|
file: '#1976d2',
|
||||||
|
paperclip: '#fff',
|
||||||
|
closeOutline: '#fff',
|
||||||
|
closePreview: '#fff',
|
||||||
|
send: '#fff',
|
||||||
|
sendDisabled: '#646a70',
|
||||||
|
emoji: '#fff',
|
||||||
|
emojiReaction: '#fff',
|
||||||
|
document: '#1976d2',
|
||||||
|
pencil: '#ebedf2',
|
||||||
|
checkmark: '#ebedf2',
|
||||||
|
checkmarkSeen: '#f0d90a',
|
||||||
|
eye: '#fff',
|
||||||
|
dropdownMessage: '#fff',
|
||||||
|
dropdownMessageBackground: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
dropdownRoom: '#fff',
|
||||||
|
dropdownScroll: '#0a0a0a',
|
||||||
|
microphone: '#fff',
|
||||||
|
audioPlay: '#b7d4d3',
|
||||||
|
audioPause: '#b7d4d3',
|
||||||
|
audioCancel: '#eb4034',
|
||||||
|
audioConfirm: '#1ba65b',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
STYLE,
|
||||||
|
}
|
||||||
@ -1,21 +1,5 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export const messageQuery = () => {
|
|
||||||
return gql`
|
|
||||||
query ($roomId: ID!) {
|
|
||||||
Message(roomId: $roomId) {
|
|
||||||
_id
|
|
||||||
id
|
|
||||||
senderId
|
|
||||||
content
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createMessageMutation = () => {
|
export const createMessageMutation = () => {
|
||||||
return gql`
|
return gql`
|
||||||
mutation ($roomId: ID!, $content: String!) {
|
mutation ($roomId: ID!, $content: String!) {
|
||||||
@ -26,3 +10,60 @@ export const createMessageMutation = () => {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const messageQuery = () => {
|
||||||
|
return gql`
|
||||||
|
query ($roomId: ID!, $first: Int, $offset: Int) {
|
||||||
|
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) {
|
||||||
|
_id
|
||||||
|
id
|
||||||
|
indexId
|
||||||
|
content
|
||||||
|
senderId
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
username
|
||||||
|
avatar
|
||||||
|
date
|
||||||
|
saved
|
||||||
|
distributed
|
||||||
|
seen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatMessageAdded = () => {
|
||||||
|
return gql`
|
||||||
|
subscription chatMessageAdded($userId: ID!) {
|
||||||
|
chatMessageAdded(userId: $userId) {
|
||||||
|
_id
|
||||||
|
id
|
||||||
|
indexId
|
||||||
|
content
|
||||||
|
senderId
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
username
|
||||||
|
avatar
|
||||||
|
date
|
||||||
|
room {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
saved
|
||||||
|
distributed
|
||||||
|
seen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const markMessagesAsSeen = () => {
|
||||||
|
return gql`
|
||||||
|
mutation ($messageIds: [String!]) {
|
||||||
|
MarkMessagesAsSeen(messageIds: $messageIds)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
@ -77,6 +77,10 @@ export const filterPosts = (i18n) => {
|
|||||||
eventEnd
|
eventEnd
|
||||||
eventVenue
|
eventVenue
|
||||||
eventLocationName
|
eventLocationName
|
||||||
|
eventLocation {
|
||||||
|
lng
|
||||||
|
lat
|
||||||
|
}
|
||||||
eventIsOnline
|
eventIsOnline
|
||||||
...post
|
...post
|
||||||
...postCounts
|
...postCounts
|
||||||
|
|||||||
@ -1,12 +1,35 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const createRoom = () => gql`
|
||||||
|
mutation ($userId: ID!) {
|
||||||
|
CreateRoom(userId: $userId) {
|
||||||
|
id
|
||||||
|
roomId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const roomQuery = () => gql`
|
export const roomQuery = () => gql`
|
||||||
query {
|
query Room($first: Int, $offset: Int, $id: ID) {
|
||||||
Room {
|
Room(first: $first, offset: $offset, id: $id, orderBy: lastMessageAt_desc) {
|
||||||
id
|
id
|
||||||
roomId
|
roomId
|
||||||
roomName
|
roomName
|
||||||
avatar
|
avatar
|
||||||
|
lastMessageAt
|
||||||
|
unreadCount
|
||||||
|
lastMessage {
|
||||||
|
_id
|
||||||
|
id
|
||||||
|
content
|
||||||
|
senderId
|
||||||
|
username
|
||||||
|
avatar
|
||||||
|
date
|
||||||
|
saved
|
||||||
|
distributed
|
||||||
|
seen
|
||||||
|
}
|
||||||
users {
|
users {
|
||||||
_id
|
_id
|
||||||
id
|
id
|
||||||
@ -19,11 +42,18 @@ export const roomQuery = () => gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const createRoom = () => gql`
|
export const unreadRoomsQuery = () => {
|
||||||
mutation ($userId: ID!) {
|
return gql`
|
||||||
CreateRoom(userId: $userId) {
|
query {
|
||||||
id
|
UnreadRooms
|
||||||
roomId
|
|
||||||
}
|
}
|
||||||
}
|
`
|
||||||
`
|
}
|
||||||
|
|
||||||
|
export const roomCountUpdated = () => {
|
||||||
|
return gql`
|
||||||
|
subscription roomCountUpdated($userId: ID!) {
|
||||||
|
roomCountUpdated(userId: $userId)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
@ -13,34 +13,42 @@
|
|||||||
<client-only>
|
<client-only>
|
||||||
<modal />
|
<modal />
|
||||||
</client-only>
|
</client-only>
|
||||||
<div v-if="$store.getters['chat/showChat'].showChat" class="chat-modul">
|
<div v-if="getShowChat.showChat" class="chat-modul">
|
||||||
<ds-text align="right" class="close">
|
<chat singleRoom :roomId="getShowChat.roomID" @close-single-room="closeSingleRoom" />
|
||||||
RoomID: {{ $store.getters['chat/showChat'].roomID }}
|
|
||||||
<ds-button @click="$store.commit('chat/SET_OPEN_CHAT', { showChat: false, roomID: null })">
|
|
||||||
x
|
|
||||||
</ds-button>
|
|
||||||
</ds-text>
|
|
||||||
<chat-module :singleRoomId="$store.getters['chat/showChat'].roomID" />
|
|
||||||
</div>
|
</div>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import seo from '~/mixins/seo'
|
import seo from '~/mixins/seo'
|
||||||
import mobile from '~/mixins/mobile'
|
import mobile from '~/mixins/mobile'
|
||||||
import HeaderMenu from '~/components/HeaderMenu/HeaderMenu'
|
import HeaderMenu from '~/components/HeaderMenu/HeaderMenu'
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
import PageFooter from '~/components/PageFooter/PageFooter'
|
import PageFooter from '~/components/PageFooter/PageFooter'
|
||||||
import ChatModule from '~/components/Chat/Chat.vue'
|
import Chat from '~/components/Chat/Chat.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HeaderMenu,
|
HeaderMenu,
|
||||||
Modal,
|
Modal,
|
||||||
PageFooter,
|
PageFooter,
|
||||||
ChatModule,
|
Chat,
|
||||||
},
|
},
|
||||||
mixins: [seo, mobile()],
|
mixins: [seo, mobile()],
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
getShowChat: 'chat/showChat',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
showChat: 'chat/SET_OPEN_CHAT',
|
||||||
|
}),
|
||||||
|
closeSingleRoom() {
|
||||||
|
this.showChat({ showChat: false, roomID: null })
|
||||||
|
},
|
||||||
|
},
|
||||||
beforeCreate() {
|
beforeCreate() {
|
||||||
this.$store.commit('chat/SET_OPEN_CHAT', { showChat: false, roomID: null })
|
this.$store.commit('chat/SET_OPEN_CHAT', { showChat: false, roomID: null })
|
||||||
},
|
},
|
||||||
@ -58,7 +66,6 @@ export default {
|
|||||||
|
|
||||||
.chat-modul {
|
.chat-modul {
|
||||||
background-color: rgb(233, 228, 228);
|
background-color: rgb(233, 228, 228);
|
||||||
height: 667px;
|
|
||||||
width: 355px;
|
width: 355px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 45px;
|
bottom: 45px;
|
||||||
|
|||||||
@ -77,6 +77,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"chat": {
|
||||||
|
"cancelSelectMessage": "Abbrechen",
|
||||||
|
"conversationStarted": "Unterhaltung startete am:",
|
||||||
|
"isOnline": "online",
|
||||||
|
"isTyping": "tippt...",
|
||||||
|
"lastSeen": "zuletzt gesehen ",
|
||||||
|
"messageDeleted": "Diese Nachricht wuerde gelöscht",
|
||||||
|
"messagesEmpty": "Keine Nachrichten",
|
||||||
|
"newMessages": "Neue Nachrichten",
|
||||||
|
"page": {
|
||||||
|
"headline": "Chat"
|
||||||
|
},
|
||||||
|
"roomEmpty": "Keinen Raum selektiert",
|
||||||
|
"roomsEmpty": "Keine Räume",
|
||||||
|
"search": "Chat-Räume filtern",
|
||||||
|
"typeMessage": "Nachricht schreiben",
|
||||||
|
"userProfileButton": {
|
||||||
|
"label": "Chat",
|
||||||
|
"tooltip": "Chatte mit „{name}“"
|
||||||
|
}
|
||||||
|
},
|
||||||
"client-only": {
|
"client-only": {
|
||||||
"loading": "Lade …"
|
"loading": "Lade …"
|
||||||
},
|
},
|
||||||
@ -553,6 +574,9 @@
|
|||||||
},
|
},
|
||||||
"groups": "Gruppen",
|
"groups": "Gruppen",
|
||||||
"myProfile": "Mein Profil"
|
"myProfile": "Mein Profil"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"tooltip": "Meine Chats"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"index": {
|
"index": {
|
||||||
@ -581,7 +605,7 @@
|
|||||||
"moreInfo": "Was ist {APPLICATION_NAME}?",
|
"moreInfo": "Was ist {APPLICATION_NAME}?",
|
||||||
"moreInfoHint": "zur Präsentationsseite",
|
"moreInfoHint": "zur Präsentationsseite",
|
||||||
"no-account": "Du hast noch kein Nutzerkonto?",
|
"no-account": "Du hast noch kein Nutzerkonto?",
|
||||||
"no-cookie": "Es kann kein Cookie angelegt werden. Du must Cookies akzeptieren.",
|
"no-cookie": "Es kann kein Cookie angelegt werden. Du musst Cookies akzeptieren.",
|
||||||
"password": "Dein Passwort",
|
"password": "Dein Passwort",
|
||||||
"register": "Nutzerkonto erstellen",
|
"register": "Nutzerkonto erstellen",
|
||||||
"success": "Du bist eingeloggt!"
|
"success": "Du bist eingeloggt!"
|
||||||
@ -596,9 +620,16 @@
|
|||||||
"button": {
|
"button": {
|
||||||
"tooltip": "Landkarte anzeigen"
|
"tooltip": "Landkarte anzeigen"
|
||||||
},
|
},
|
||||||
"markerTypes": {
|
"legend": {
|
||||||
|
"event": "Veranstaltung",
|
||||||
"group": "Gruppe",
|
"group": "Gruppe",
|
||||||
"theUser": "deine Position",
|
"theUser": "Meine Position",
|
||||||
|
"user": "Nutzer"
|
||||||
|
},
|
||||||
|
"markerTypes": {
|
||||||
|
"event": "Veranstaltung",
|
||||||
|
"group": "Gruppe",
|
||||||
|
"theUser": "meine Position",
|
||||||
"user": "Nutzer"
|
"user": "Nutzer"
|
||||||
},
|
},
|
||||||
"pageTitle": "Landkarte",
|
"pageTitle": "Landkarte",
|
||||||
@ -692,10 +723,6 @@
|
|||||||
"unread": "Ungelesen"
|
"unread": "Ungelesen"
|
||||||
},
|
},
|
||||||
"group": "Beschreibung",
|
"group": "Beschreibung",
|
||||||
"headerMenuButton": {
|
|
||||||
"chat": "Meine Chat",
|
|
||||||
"tooltip": "Meine Benachrichtigungen"
|
|
||||||
},
|
|
||||||
"markAllAsRead": "Markiere alle als gelesen",
|
"markAllAsRead": "Markiere alle als gelesen",
|
||||||
"pageLink": "Alle Benachrichtigungen",
|
"pageLink": "Alle Benachrichtigungen",
|
||||||
"post": "Beitrag oder Gruppe",
|
"post": "Beitrag oder Gruppe",
|
||||||
@ -711,11 +738,6 @@
|
|||||||
"title": "Benachrichtigungen",
|
"title": "Benachrichtigungen",
|
||||||
"user": "Nutzer"
|
"user": "Nutzer"
|
||||||
},
|
},
|
||||||
"position": {
|
|
||||||
"group": "Gruppe",
|
|
||||||
"my": "Meine Position",
|
|
||||||
"user": "Nutzer"
|
|
||||||
},
|
|
||||||
"post": {
|
"post": {
|
||||||
"comment": {
|
"comment": {
|
||||||
"reply": "Antworten",
|
"reply": "Antworten",
|
||||||
@ -760,9 +782,9 @@
|
|||||||
"viewEvent": {
|
"viewEvent": {
|
||||||
"eventEnd": "Ende",
|
"eventEnd": "Ende",
|
||||||
"eventIsOnline": "Online",
|
"eventIsOnline": "Online",
|
||||||
"eventLocationName": "Stadt",
|
"eventLocationName": "Stadt - z.B. Musterstraße 1, 12345 Musterstadt",
|
||||||
"eventStart": "Beginn",
|
"eventStart": "Beginn",
|
||||||
"eventVenue": "Veranstaltungsort",
|
"eventVenue": "Veranstaltungsort - z.B. Hinterhof, 1. OG, ...",
|
||||||
"title": "Veranstaltung"
|
"title": "Veranstaltung"
|
||||||
},
|
},
|
||||||
"viewPost": {
|
"viewPost": {
|
||||||
|
|||||||
@ -77,6 +77,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"chat": {
|
||||||
|
"cancelSelectMessage": "Cancel",
|
||||||
|
"conversationStarted": "Conversation started on:",
|
||||||
|
"isOnline": "is online",
|
||||||
|
"isTyping": "is writing...",
|
||||||
|
"lastSeen": "last seen ",
|
||||||
|
"messageDeleted": "This message was deleted",
|
||||||
|
"messagesEmpty": "No messages",
|
||||||
|
"newMessages": "New Messages",
|
||||||
|
"page": {
|
||||||
|
"headline": "Chat"
|
||||||
|
},
|
||||||
|
"roomEmpty": "No room selected",
|
||||||
|
"roomsEmpty": "No rooms",
|
||||||
|
"search": "Filter chat rooms",
|
||||||
|
"typeMessage": "Type message",
|
||||||
|
"userProfileButton": {
|
||||||
|
"label": "Chat",
|
||||||
|
"tooltip": "Chat with “{name}”"
|
||||||
|
}
|
||||||
|
},
|
||||||
"client-only": {
|
"client-only": {
|
||||||
"loading": "Loading …"
|
"loading": "Loading …"
|
||||||
},
|
},
|
||||||
@ -553,6 +574,9 @@
|
|||||||
},
|
},
|
||||||
"groups": "Groups",
|
"groups": "Groups",
|
||||||
"myProfile": "My profile"
|
"myProfile": "My profile"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"tooltip": "My chats"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"index": {
|
"index": {
|
||||||
@ -596,9 +620,16 @@
|
|||||||
"button": {
|
"button": {
|
||||||
"tooltip": "Show map"
|
"tooltip": "Show map"
|
||||||
},
|
},
|
||||||
|
"legend": {
|
||||||
|
"event": "Event",
|
||||||
|
"group": "Group",
|
||||||
|
"theUser": "My position",
|
||||||
|
"user": "User"
|
||||||
|
},
|
||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
|
"event": "event",
|
||||||
"group": "group",
|
"group": "group",
|
||||||
"theUser": "your position",
|
"theUser": "my position",
|
||||||
"user": "user"
|
"user": "user"
|
||||||
},
|
},
|
||||||
"pageTitle": "Map",
|
"pageTitle": "Map",
|
||||||
@ -692,10 +723,6 @@
|
|||||||
"unread": "Unread"
|
"unread": "Unread"
|
||||||
},
|
},
|
||||||
"group": "Description",
|
"group": "Description",
|
||||||
"headerMenuButton": {
|
|
||||||
"chat": "My Chat",
|
|
||||||
"tooltip": "My notifications"
|
|
||||||
},
|
|
||||||
"markAllAsRead": "Mark all as read",
|
"markAllAsRead": "Mark all as read",
|
||||||
"pageLink": "All notifications",
|
"pageLink": "All notifications",
|
||||||
"post": "Post or Group",
|
"post": "Post or Group",
|
||||||
@ -711,11 +738,6 @@
|
|||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"user": "User"
|
"user": "User"
|
||||||
},
|
},
|
||||||
"position": {
|
|
||||||
"group": "Group",
|
|
||||||
"my": "My position",
|
|
||||||
"user": "User"
|
|
||||||
},
|
|
||||||
"post": {
|
"post": {
|
||||||
"comment": {
|
"comment": {
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
@ -760,9 +782,9 @@
|
|||||||
"viewEvent": {
|
"viewEvent": {
|
||||||
"eventEnd": "End",
|
"eventEnd": "End",
|
||||||
"eventIsOnline": "Online",
|
"eventIsOnline": "Online",
|
||||||
"eventLocationName": "City",
|
"eventLocationName": "City - e.g. Example street 1, 12345 City",
|
||||||
"eventStart": "Start",
|
"eventStart": "Start",
|
||||||
"eventVenue": "Venue",
|
"eventVenue": "Venue - e.g. Backyard, 1st Floor, ...",
|
||||||
"title": "Event"
|
"title": "Event"
|
||||||
},
|
},
|
||||||
"viewPost": {
|
"viewPost": {
|
||||||
|
|||||||
@ -75,6 +75,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"chat": {
|
||||||
|
"search": "Filtrar salas de chat"
|
||||||
|
},
|
||||||
"code-of-conduct": {
|
"code-of-conduct": {
|
||||||
"subheader": "para la red social de {ORGANIZATION_NAME}"
|
"subheader": "para la red social de {ORGANIZATION_NAME}"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -75,6 +75,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"chat": {
|
||||||
|
"search": "Filtrer les salons de chat"
|
||||||
|
},
|
||||||
"code-of-conduct": {
|
"code-of-conduct": {
|
||||||
"subheader": "pour le réseau social de {ORGANIZATION_NAME}"
|
"subheader": "pour le réseau social de {ORGANIZATION_NAME}"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -105,6 +105,7 @@ export default {
|
|||||||
styleguideStyles,
|
styleguideStyles,
|
||||||
'~assets/_new/styles/tokens.scss',
|
'~assets/_new/styles/tokens.scss',
|
||||||
'~assets/styles/imports/_branding.scss',
|
'~assets/styles/imports/_branding.scss',
|
||||||
|
'~assets/_new/styles/export.scss',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<chat />
|
<div>
|
||||||
|
<ds-heading tag="h1">{{ $t('chat.page.headline') }}</ds-heading>
|
||||||
|
<chat :roomId="getShowChat.showChat ? getShowChat.roomID : null" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import Chat from '../components/Chat/Chat.vue'
|
import Chat from '../components/Chat/Chat.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Chat },
|
components: { Chat },
|
||||||
|
mounted() {
|
||||||
|
this.showChat({ showChat: false, roomID: null })
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
getShowChat: 'chat/showChat',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
showChat: 'chat/SET_OPEN_CHAT',
|
||||||
|
}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -5,16 +5,15 @@
|
|||||||
<ds-heading tag="h1">{{ $t('map.pageTitle') }}</ds-heading>
|
<ds-heading tag="h1">{{ $t('map.pageTitle') }}</ds-heading>
|
||||||
<small>
|
<small>
|
||||||
<div>
|
<div>
|
||||||
<img
|
<span v-for="type in markers.types" :key="type.id">
|
||||||
alt="my position"
|
<img
|
||||||
src="/img/mapbox/marker-icons/mapbox-marker-icon-orange.svg"
|
:alt="$t('map.legend.' + type.id)"
|
||||||
width="15"
|
:src="'/img/mapbox/marker-icons/' + type.icon.legendName"
|
||||||
/>
|
width="15"
|
||||||
{{ $t('position.my') }}
|
/>
|
||||||
<img alt="user" src="/img/mapbox/marker-icons/mapbox-marker-icon-green.svg" width="15" />
|
{{ $t('map.legend.' + type.id) }}
|
||||||
{{ $t('position.user') }}
|
|
||||||
<img alt="group" src="/img/mapbox/marker-icons/mapbox-marker-icon-blue.svg" width="15" />
|
</span>
|
||||||
{{ $t('position.group') }}
|
|
||||||
</div>
|
</div>
|
||||||
</small>
|
</small>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
@ -66,6 +65,7 @@ import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { profileUserQuery, mapUserQuery } from '~/graphql/User'
|
import { profileUserQuery, mapUserQuery } from '~/graphql/User'
|
||||||
import { groupQuery } from '~/graphql/groups'
|
import { groupQuery } from '~/graphql/groups'
|
||||||
|
import { filterPosts } from '~/graphql/PostQuery.js'
|
||||||
import mobile from '~/mixins/mobile'
|
import mobile from '~/mixins/mobile'
|
||||||
import Empty from '~/components/Empty/Empty'
|
import Empty from '~/components/Empty/Empty'
|
||||||
import MapStylesButtons from '~/components/Map/MapStylesButtons'
|
import MapStylesButtons from '~/components/Map/MapStylesButtons'
|
||||||
@ -95,19 +95,40 @@ export default {
|
|||||||
currentUserCoordinates: null,
|
currentUserCoordinates: null,
|
||||||
users: null,
|
users: null,
|
||||||
groups: null,
|
groups: null,
|
||||||
|
posts: null,
|
||||||
markers: {
|
markers: {
|
||||||
icons: [
|
types: [
|
||||||
{
|
{
|
||||||
id: 'marker-blue',
|
id: 'theUser',
|
||||||
name: 'mapbox-marker-icon-20px-blue.png',
|
icon: {
|
||||||
|
id: 'marker-orange',
|
||||||
|
legendName: 'mapbox-marker-icon-orange.svg',
|
||||||
|
mapName: 'mapbox-marker-icon-20px-orange.png',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'marker-orange',
|
id: 'user',
|
||||||
name: 'mapbox-marker-icon-20px-orange.png',
|
icon: {
|
||||||
|
id: 'marker-green',
|
||||||
|
legendName: 'mapbox-marker-icon-green.svg',
|
||||||
|
mapName: 'mapbox-marker-icon-20px-green.png',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'marker-green',
|
id: 'group',
|
||||||
name: 'mapbox-marker-icon-20px-green.png',
|
icon: {
|
||||||
|
id: 'marker-red',
|
||||||
|
legendName: 'mapbox-marker-icon-red.svg',
|
||||||
|
mapName: 'mapbox-marker-icon-20px-red.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'event',
|
||||||
|
icon: {
|
||||||
|
id: 'marker-purple',
|
||||||
|
legendName: 'mapbox-marker-icon-purple.svg',
|
||||||
|
mapName: 'mapbox-marker-icon-20px-purple.png',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isImagesLoaded: false,
|
isImagesLoaded: false,
|
||||||
@ -137,7 +158,8 @@ export default {
|
|||||||
this.markers.isImagesLoaded &&
|
this.markers.isImagesLoaded &&
|
||||||
this.currentUser &&
|
this.currentUser &&
|
||||||
this.users &&
|
this.users &&
|
||||||
this.groups
|
this.groups &&
|
||||||
|
this.posts
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
styles() {
|
styles() {
|
||||||
@ -236,17 +258,27 @@ export default {
|
|||||||
|
|
||||||
// Copy coordinates array.
|
// Copy coordinates array.
|
||||||
const coordinates = e.features[0].geometry.coordinates.slice()
|
const coordinates = e.features[0].geometry.coordinates.slice()
|
||||||
const markerTypeLabel =
|
const markerTypeLabel = this.$t(`map.markerTypes.${e.features[0].properties.type}`)
|
||||||
e.features[0].properties.type === 'group'
|
const markerProfile = {
|
||||||
? this.$t('map.markerTypes.group')
|
theUser: {
|
||||||
: e.features[0].properties.type === 'user'
|
linkTitle: '@' + e.features[0].properties.slug,
|
||||||
? this.$t('map.markerTypes.user')
|
link: `/profile/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
|
||||||
: this.$t('map.markerTypes.theUser')
|
},
|
||||||
const markerProfileLinkTitle =
|
user: {
|
||||||
(e.features[0].properties.type === 'group' ? '&' : '@') + e.features[0].properties.slug
|
linkTitle: '@' + e.features[0].properties.slug,
|
||||||
const markerProfileLink =
|
link: `/profile/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
|
||||||
(e.features[0].properties.type === 'group' ? '/group' : '/profile') +
|
},
|
||||||
`/${e.features[0].properties.id}/${e.features[0].properties.slug}`
|
group: {
|
||||||
|
linkTitle: '&' + e.features[0].properties.slug,
|
||||||
|
link: `/group/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
linkTitle: e.features[0].properties.slug,
|
||||||
|
link: `/post/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const markerProfileLinkTitle = markerProfile[e.features[0].properties.type].linkTitle
|
||||||
|
const markerProfileLink = markerProfile[e.features[0].properties.type].link
|
||||||
let description = `
|
let description = `
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
@ -258,11 +290,11 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
description +=
|
description +=
|
||||||
e.features[0].properties.about && e.features[0].properties.about.length > 0
|
e.features[0].properties.description && e.features[0].properties.description.length > 0
|
||||||
? `
|
? `
|
||||||
<hr>
|
<hr>
|
||||||
<div>
|
<div>
|
||||||
${e.features[0].properties.about}
|
${e.features[0].properties.description}
|
||||||
</div>`
|
</div>`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
@ -305,15 +337,18 @@ export default {
|
|||||||
},
|
},
|
||||||
loadMarkersIconsAndAddMarkers() {
|
loadMarkersIconsAndAddMarkers() {
|
||||||
Promise.all(
|
Promise.all(
|
||||||
this.markers.icons.map(
|
this.markers.types.map(
|
||||||
(marker) =>
|
(marker) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
// our images have to be in the 'static/img/*' folder otherwise they are not reachable via URL
|
// our images have to be in the 'static/img/*' folder otherwise they are not reachable via URL
|
||||||
this.map.loadImage('img/mapbox/marker-icons/' + marker.name, (error, image) => {
|
this.map.loadImage(
|
||||||
if (error) throw error
|
'img/mapbox/marker-icons/' + marker.icon.mapName,
|
||||||
this.map.addImage(marker.id, image)
|
(error, image) => {
|
||||||
resolve()
|
if (error) throw error
|
||||||
})
|
this.map.addImage(marker.icon.id, image)
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).then(() => {
|
).then(() => {
|
||||||
@ -337,7 +372,7 @@ export default {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
slug: user.slug,
|
slug: user.slug,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
about: user.about ? user.about : undefined,
|
description: user.about ? user.about : undefined,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
@ -346,27 +381,6 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// add markers for "groups"
|
|
||||||
this.groups.forEach((group) => {
|
|
||||||
if (group.location) {
|
|
||||||
this.markers.geoJSON.push({
|
|
||||||
type: 'Feature',
|
|
||||||
properties: {
|
|
||||||
type: 'group',
|
|
||||||
iconName: 'marker-blue',
|
|
||||||
iconRotate: 0.0,
|
|
||||||
id: group.id,
|
|
||||||
slug: group.slug,
|
|
||||||
name: group.name,
|
|
||||||
about: group.about ? group.about : undefined,
|
|
||||||
},
|
|
||||||
geometry: {
|
|
||||||
type: 'Point',
|
|
||||||
coordinates: this.getCoordinates(group.location),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// add marker for "currentUser"
|
// add marker for "currentUser"
|
||||||
if (this.currentUserCoordinates) {
|
if (this.currentUserCoordinates) {
|
||||||
this.markers.geoJSON.push({
|
this.markers.geoJSON.push({
|
||||||
@ -378,7 +392,7 @@ export default {
|
|||||||
id: this.currentUser.id,
|
id: this.currentUser.id,
|
||||||
slug: this.currentUser.slug,
|
slug: this.currentUser.slug,
|
||||||
name: this.currentUser.name,
|
name: this.currentUser.name,
|
||||||
about: this.currentUser.about ? this.currentUser.about : undefined,
|
description: this.currentUser.about ? this.currentUser.about : undefined,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
@ -386,6 +400,48 @@ export default {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// add markers for "groups"
|
||||||
|
this.groups.forEach((group) => {
|
||||||
|
if (group.location) {
|
||||||
|
this.markers.geoJSON.push({
|
||||||
|
type: 'Feature',
|
||||||
|
properties: {
|
||||||
|
type: 'group',
|
||||||
|
iconName: 'marker-red',
|
||||||
|
iconRotate: 0.0,
|
||||||
|
id: group.id,
|
||||||
|
slug: group.slug,
|
||||||
|
name: group.name,
|
||||||
|
description: group.about ? group.about : undefined,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: this.getCoordinates(group.location),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// add markers for "posts", post type "Event" with location coordinates
|
||||||
|
this.posts.forEach((post) => {
|
||||||
|
if (post.postType.includes('Event') && post.eventLocation) {
|
||||||
|
this.markers.geoJSON.push({
|
||||||
|
type: 'Feature',
|
||||||
|
properties: {
|
||||||
|
type: 'event',
|
||||||
|
iconName: 'marker-purple',
|
||||||
|
iconRotate: 0.0,
|
||||||
|
id: post.id,
|
||||||
|
slug: post.slug,
|
||||||
|
name: post.title,
|
||||||
|
description: post.contentExcerpt,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: this.getCoordinates(post.eventLocation),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.markers.isGeoJSON = true
|
this.markers.isGeoJSON = true
|
||||||
}
|
}
|
||||||
@ -483,6 +539,24 @@ export default {
|
|||||||
},
|
},
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
},
|
},
|
||||||
|
Post: {
|
||||||
|
query() {
|
||||||
|
return filterPosts(this.$i18n)
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
filter: {
|
||||||
|
postType_in: ['Event'],
|
||||||
|
eventStart_gte: new Date(),
|
||||||
|
// would be good to just query for events with defined "eventLocation". couldn't get it working
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update({ Post }) {
|
||||||
|
this.posts = Post
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -80,13 +80,14 @@
|
|||||||
@update="updateFollow"
|
@update="updateFollow"
|
||||||
/>
|
/>
|
||||||
<base-button
|
<base-button
|
||||||
@click="$store.commit('chat/SET_OPEN_CHAT', { showChat: true, roomID: user.id })"
|
icon="chat-bubble"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t('notifications.headerMenuButton.chat'),
|
content: $t('chat.userProfileButton.tooltip', { name: userName }),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
|
@click="showOrChangeChat(user.id)"
|
||||||
>
|
>
|
||||||
<img src="/img/empty/chat-bubble.svg" height="20" />
|
{{ $t('chat.userProfileButton.label') }}
|
||||||
</base-button>
|
</base-button>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="user.about">
|
<template v-if="user.about">
|
||||||
@ -181,6 +182,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import uniqBy from 'lodash/uniqBy'
|
import uniqBy from 'lodash/uniqBy'
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import postListActions from '~/mixins/postListActions'
|
import postListActions from '~/mixins/postListActions'
|
||||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||||
import HcFollowButton from '~/components/Button/FollowButton'
|
import HcFollowButton from '~/components/Button/FollowButton'
|
||||||
@ -252,6 +254,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
getShowChat: 'chat/showChat',
|
||||||
|
}),
|
||||||
myProfile() {
|
myProfile() {
|
||||||
return this.$route.params.id === this.$store.getters['auth/user'].id
|
return this.$route.params.id === this.$store.getters['auth/user'].id
|
||||||
},
|
},
|
||||||
@ -290,6 +295,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
commitModalData: 'modal/SET_OPEN',
|
||||||
|
showChat: 'chat/SET_OPEN_CHAT',
|
||||||
|
}),
|
||||||
handleTab(tab) {
|
handleTab(tab) {
|
||||||
if (this.tabActive !== tab) {
|
if (this.tabActive !== tab) {
|
||||||
this.tabActive = tab
|
this.tabActive = tab
|
||||||
@ -365,7 +374,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteUser(userdata) {
|
async deleteUser(userdata) {
|
||||||
this.$store.commit('modal/SET_OPEN', {
|
this.commitModalData({
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
data: {
|
data: {
|
||||||
userdata: userdata,
|
userdata: userdata,
|
||||||
@ -397,6 +406,12 @@ export default {
|
|||||||
if (type === 'following') this.followingCount = count
|
if (type === 'following') this.followingCount = count
|
||||||
if (type === 'followedBy') this.followedByCount = count
|
if (type === 'followedBy') this.followedByCount = count
|
||||||
},
|
},
|
||||||
|
async showOrChangeChat(roomID) {
|
||||||
|
if (this.getShowChat.showChat) {
|
||||||
|
await this.showChat({ showChat: false, roomID: null })
|
||||||
|
}
|
||||||
|
await this.showChat({ showChat: true, roomID })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
profilePagePosts: {
|
profilePagePosts: {
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" id="chat-bubble"><g data-name="Chat"><path fill="#9B9B9B" d="M27.23,24.91A9,9,0,0,0,22.44,9.52,8.57,8.57,0,0,0,21,9.41a8.94,8.94,0,0,0-8.92,10.14c0,.1,0,.2,0,.29a9,9,0,0,0,.22,1l0,.11a8.93,8.93,0,0,0,.38,1l.13.28a9,9,0,0,0,.45.83l.07.14a9.13,9.13,0,0,1-2.94-.36,1,1,0,0,0-.68,0L7,24.13l.54-1.9a1,1,0,0,0-.32-1,9,9,0,0,1,11.27-14,1,1,0,0,0,1.23-1.58A10.89,10.89,0,0,0,13,3.25a11,11,0,0,0-7.5,19l-1,3.34A1,1,0,0,0,5.9,26.82l4.35-1.93a11,11,0,0,0,4.68.16A9,9,0,0,0,21,27.41a8.81,8.81,0,0,0,2.18-.27l3.41,1.52A1,1,0,0,0,28,27.48Zm-1.77-1.1a1,1,0,0,0-.32,1L25.45,26l-1.79-.8a1,1,0,0,0-.41-.09,1,1,0,0,0-.29,0,6.64,6.64,0,0,1-2,.29,7,7,0,0,1,0-14,6.65,6.65,0,0,1,1.11.09,7,7,0,0,1,3.35,12.31Z"></path><path fill="#9B9B9B" d="M17.82 17.08H17a1 1 0 0 0 0 2h.82a1 1 0 0 0 0-2zM21.41 17.08h-.82a1 1 0 0 0 0 2h.82a1 1 0 0 0 0-2zM25 17.08h-.82a1 1 0 0 0 0 2H25a1 1 0 0 0 0-2z"></path></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 942 B |
@ -2,6 +2,7 @@ export const state = () => {
|
|||||||
return {
|
return {
|
||||||
showChat: false,
|
showChat: false,
|
||||||
roomID: null,
|
roomID: null,
|
||||||
|
unreadRoomCount: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,6 +11,9 @@ export const mutations = {
|
|||||||
state.showChat = ctx.showChat || false
|
state.showChat = ctx.showChat || false
|
||||||
state.roomID = ctx.roomID || null
|
state.roomID = ctx.roomID || null
|
||||||
},
|
},
|
||||||
|
UPDATE_ROOM_COUNT(state, count) {
|
||||||
|
state.unreadRoomCount = count
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@ -19,4 +23,7 @@ export const getters = {
|
|||||||
roomID(state) {
|
roomID(state) {
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
|
unreadRoomCount(state) {
|
||||||
|
return state.unreadRoomCount
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user