Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into 6593-@all-mention-for-network-admins

This commit is contained in:
Wolfgang Huß 2023-07-20 16:53:36 +02:00
commit cf8c224062
54 changed files with 1545 additions and 1123 deletions

View File

@ -1,4 +1,5 @@
backend: &backend
- '.github/workflows/test-backend.yml'
- 'backend/**/*'
- 'neo4j/**/*'
@ -6,4 +7,5 @@ docker: &docker
- 'docker-compose.*'
webapp: &webapp
- '.github/workflows/test-webapp.yml'
- 'webapp/**/*'

View 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 }}

View File

@ -1,7 +1,7 @@
name: ocelot.social backend test CI
on: [push]
on: push
jobs:
files-changed:
@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v3.3.0
- name: Check for frontend file changes
- name: Check for backend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
@ -34,12 +34,13 @@ jobs:
run: |
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
- name: Cache docker images
id: cache-neo4j
uses: actions/cache/save@v3.3.1
with:
name: docker-neo4j-image
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-backend-neo4j-cache
build_test_backend:
name: Docker Build Test - Backend
@ -54,12 +55,13 @@ jobs:
run: |
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
- name: Cache docker images
id: cache-backend
uses: actions/cache/save@v3.3.1
with:
name: docker-backend-test
path: /tmp/backend.tar
key: ${{ github.run_id }}-backend-cache
lint_backend:
name: Lint Backend
@ -84,28 +86,29 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image (Neo4J)
uses: actions/download-artifact@v3
- name: Restore Neo4J cache
uses: actions/cache/restore@v3.3.1
with:
name: docker-neo4j-image
path: /tmp
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-backend-neo4j-cache
fail-on-cache-miss: true
- name: Load Docker Image
run: docker load < /tmp/neo4j.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
- name: Restore Backend cache
uses: actions/cache/restore@v3.3.1
with:
name: docker-backend-test
path: /tmp
path: /tmp/backend.tar
key: ${{ github.run_id }}-backend-cache
fail-on-cache-miss: true
- name: Load Docker Image
run: docker load < /tmp/backend.tar
- name: Load Docker Images
run: |
docker load < /tmp/neo4j.tar
docker load < /tmp/backend.tar
- name: backend | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
run: cp backend/.env.template backend/.env
- name: backend | copy env files
run: |
cp webapp/.env.template webapp/.env
cp backend/.env.template backend/.env
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend
@ -118,3 +121,20 @@ jobs:
- name: backend | Unit test incl. coverage check
run: docker-compose exec -T backend yarn test
cleanup:
name: Cleanup
if: ${{ needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true' }}
needs: [files-changed, unit_test_backend]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Delete cache
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
KEY="${{ github.run_id }}-backend-neo4j-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
KEY="${{ github.run_id }}-backend-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm

View File

@ -1,12 +1,11 @@
name: ocelot.social end-to-end test CI
on: push
jobs:
docker_preparation:
name: Fullstack test preparation
runs-on: ubuntu-latest
outputs:
pr-number: ${{ steps.pr.outputs.number }}
steps:
- name: Checkout code
uses: actions/checkout@v3
@ -34,10 +33,6 @@ jobs:
yarn build
cd ..
yarn install
- name: Get pr number
id: pr
uses: 8BitJonny/gh-get-current-pr@2.2.0
- name: Cache docker images
id: cache
@ -48,7 +43,7 @@ jobs:
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: e2e-preparation-cache-pr${{ steps.pr.outputs.number }}
key: ${{ github.run_id }}-e2e-preparation-cache
fullstack_tests:
name: Fullstack tests
@ -71,7 +66,7 @@ jobs:
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: e2e-preparation-cache-pr${{ needs.docker_preparation.outputs.pr-number }}
key: ${{ github.run_id }}-e2e-preparation-cache
fail-on-cache-miss: true
- name: Boot up test system | docker-compose
@ -104,14 +99,14 @@ jobs:
cleanup:
name: Cleanup
if: always()
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="e2e-preparation-cache-pr${{ needs.docker_preparation.outputs.pr-number }}"
KEY="${{ github.run_id }}-e2e-preparation-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm

View File

@ -1,7 +1,7 @@
name: ocelot.social webapp test CI
on: [push]
on: push
jobs:
files-changed:
@ -34,7 +34,7 @@ jobs:
run: |
scripts/translations/sort.sh
scripts/translations/missing-keys.sh
build_test_webapp:
name: Docker Build Test - Webapp
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
@ -44,16 +44,16 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: webapp | Build 'test' image
- name: Webapp | Build 'test' image
run: |
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
- name: Cache docker image
uses: actions/cache/save@v3.3.1
with:
name: docker-webapp-test
path: /tmp/webapp.tar
key: ${{ github.run_id }}-webapp-cache
lint_webapp:
name: Lint Webapp
@ -78,20 +78,19 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image (Webapp)
uses: actions/download-artifact@v3
- name: Restore webapp cache
uses: actions/cache/restore@v3.3.1
with:
name: docker-webapp-test
path: /tmp
path: /tmp/webapp.tar
key: ${{ github.run_id }}-webapp-cache
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
- name: backend | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
run: cp backend/.env.template backend/.env
- name: Copy env files
run: |
cp webapp/.env.template webapp/.env
cp backend/.env.template backend/.env
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
@ -99,3 +98,18 @@ jobs:
- name: webapp | Unit tests incl. coverage check
run: docker-compose exec -T webapp yarn test
cleanup:
name: Cleanup
if: ${{ needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true' }}
needs: [files-changed, unit_test_webapp]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Delete cache
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
KEY="${{ github.run_id }}-webapp-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,9 @@ export const messageQuery = () => {
indexId
content
senderId
author {
id
}
username
avatar
date

View File

@ -9,6 +9,7 @@ export const createRoomMutation = () => {
roomName
lastMessageAt
unreadCount
#avatar
users {
_id
id
@ -25,10 +26,11 @@ export const createRoomMutation = () => {
export const roomQuery = () => {
return gql`
query Room($first: Int, $offset: Int, $id: ID) {
Room(first: $first, offset: $offset, id: $id, orderBy: createdAt_desc) {
Room(first: $first, offset: $offset, id: $id, orderBy: lastMessageAt_desc) {
id
roomId
roomName
avatar
lastMessageAt
unreadCount
lastMessage {

View File

@ -9,10 +9,9 @@ function walkRecursive(data, fields, fieldName, callback, _key?) {
if (!Array.isArray(fields)) {
throw new Error('please provide an fields array for the walkRecursive helper')
}
if (data && typeof data === 'string' && fields.includes(_key)) {
// well we found what we searched for, lets replace the value with our callback result
const key = _key.split('!')
if (key.length === 1 || key[1] !== fieldName) data = callback(data, key[0])
const fieldDef = fields.find((f) => f.field === _key)
if (data && typeof data === 'string' && fieldDef) {
if (!fieldDef.excludes?.includes(fieldName)) data = callback(data, _key)
} else if (data && Array.isArray(data)) {
// go into the rabbit hole and dig through that array
data.forEach((res, index) => {

View File

@ -54,4 +54,7 @@ export default {
Mutation: {
CreateRoom: roomProperties,
},
Subscription: {
chatMessageAdded: messageProperties,
},
}

View File

@ -30,6 +30,7 @@ const standardSanitizeHtmlOptions = {
'strike',
'span',
'blockquote',
'usertag',
],
allowedAttributes: {
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],

View File

@ -3,11 +3,11 @@ import { cleanHtml } from '../middleware/helpers/cleanHtml'
// exclamation mark separetes field names, that should not be sanitized
const fields = [
'content',
'contentExcerpt',
'reasonDescription',
'description!embed',
'descriptionExcerpt',
{ field: 'content', excludes: ['CreateMessage', 'Message'] },
{ field: 'contentExcerpt' },
{ field: 'reasonDescription' },
{ field: 'description', excludes: ['embed'] },
{ field: 'descriptionExcerpt' },
]
export default {

View File

@ -117,7 +117,7 @@ describe('Message', () => {
})
describe('user chats in room', () => {
it('returns the message and publishes subscription', async () => {
it('returns the message and publishes subscriptions', async () => {
await expect(
mutate({
mutation: createMessageMutation(),
@ -146,6 +146,20 @@ describe('Message', () => {
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', () => {

View File

@ -1,7 +1,9 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import Resolver from './helpers/Resolver'
import { getUnreadRoomsCount } from './rooms'
import { pubsub, ROOM_COUNT_UPDATED } from '../../server'
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) => {
@ -19,6 +21,16 @@ const setMessagesAsDistributed = async (undistributedMessagesIds, session) => {
}
export default {
Subscription: {
chatMessageAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(CHAT_MESSAGE_ADDED),
(payload, variables) => {
return payload.userId === variables.userId
},
),
},
},
Query: {
Message: async (object, params, context, resolveInfo) => {
const { roomId } = params
@ -69,7 +81,7 @@ export default {
createdAt: toString(datetime()),
id: apoc.create.uuid(),
indexId: CASE WHEN maxIndex IS NOT NULL THEN maxIndex + 1 ELSE 0 END,
content: $content,
content: LEFT($content,2000),
saved: true,
distributed: false,
seen: false
@ -77,6 +89,7 @@ export default {
SET room.lastMessageAt = toString(datetime())
RETURN message {
.*,
indexId: toString(message.indexId),
recipientId: recipientUser.id,
senderId: currentUser.id,
username: currentUser.name,
@ -102,10 +115,14 @@ export default {
const roomCountUpdated = await getUnreadRoomsCount(message.recipientId, session)
// send subscriptions
await pubsub.publish(ROOM_COUNT_UPDATED, {
void pubsub.publish(ROOM_COUNT_UPDATED, {
roomCountUpdated,
userId: message.recipientId,
})
void pubsub.publish(CHAT_MESSAGE_ADDED, {
chatMessageAdded: message,
userId: message.recipientId,
})
}
return message

View File

@ -423,125 +423,147 @@ describe('Room', () => {
})
it('returns the rooms paginated', async () => {
expect(await query({ query: roomQuery(), variables: { first: 3, offset: 0 } })).toMatchObject(
{
errors: undefined,
data: {
Room: [
{
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),
roomId: expect.any(String),
roomName: 'Third Chatting User',
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
},
{
_id: 'third-chatting-user',
id: 'third-chatting-user',
name: 'Third Chatting User',
avatar: {
url: 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,
},
{
id: expect.any(String),
roomId: expect.any(String),
roomName: 'Second Chatting User',
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
users: expect.arrayContaining([
expect.objectContaining({
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
{
_id: 'second-chatting-user',
id: 'second-chatting-user',
name: 'Second 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),
},
]),
},
{
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),
},
},
]),
},
],
},
}),
]),
}),
]),
},
)
expect(await query({ query: roomQuery(), variables: { first: 3, offset: 3 } })).toMatchObject(
{
errors: undefined,
data: {
Room: [
{
id: expect.any(String),
roomId: expect.any(String),
roomName: 'Other Chatting User',
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: '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: 'other-chatting-user',
id: 'other-chatting-user',
name: 'Other 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(
@ -556,34 +578,19 @@ describe('Room', () => {
{
id: expect.any(String),
roomId: expect.any(String),
roomName: 'Third Chatting User',
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
},
{
_id: 'third-chatting-user',
id: 'third-chatting-user',
name: 'Third Chatting User',
avatar: {
url: 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(

View File

@ -44,3 +44,7 @@ type Query {
orderBy: [_MessageOrdering]
): [Message]
}
type Subscription {
chatMessageAdded(userId: ID!): Message
}

View File

@ -7,6 +7,7 @@
# TODO change this to last message date
enum _RoomOrdering {
lastMessageAt_desc
createdAt_desc
}

View File

@ -14,7 +14,7 @@ import bodyParser from 'body-parser'
import { graphqlUploadExpress } from 'graphql-upload'
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
// export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED'
export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED'
export const ROOM_COUNT_UPDATED = 'ROOM_COUNT_UPDATED'
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
let prodPubsub, devPubsub

View File

@ -27,4 +27,8 @@
chatMessageBgOthers: $chat-message-bg-others;
chatNewMessageColor: $chat-new-message-color;
chatMessageTimestamp: $chat-message-timestamp;
chatMessageCheckmarkSeen: $chat-message-checkmark-seen;
chatMessageCheckmark: $chat-message-checkmark;
}

View File

@ -417,3 +417,6 @@ $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;

View File

@ -0,0 +1,67 @@
<template>
<div class="add-chat-room-by-user-search">
<ds-flex class="headline">
<h2 class="title">{{ $t('chat.addRoomHeadline') }}</h2>
<base-button class="close-button" icon="close" circle @click="closeUserSearch" />
</ds-flex>
<ds-space margin-bottom="small" />
<ds-space>
<select-user-search :id="id" ref="selectUserSearch" @select-user="selectUser" />
</ds-space>
</div>
</template>
<script>
import SelectUserSearch from '~/components/generic/SelectUserSearch/SelectUserSearch'
export default {
name: 'AddChatRoomByUserSearch',
components: {
SelectUserSearch,
},
props: {
// chatRooms: {
// type: Array,
// default: [],
// },
},
data() {
return {
id: 'search-user-to-add-to-group',
user: {},
}
},
methods: {
selectUser(user) {
this.user = user
// if (this.groupMembers.find((member) => member.id === this.user.id)) {
// this.$toast.error(this.$t('group.errors.userAlreadyMember', { name: this.user.name }))
// this.$refs.selectUserSearch.clear()
// return
// }
this.$refs.selectUserSearch.clear()
this.$emit('close-user-search')
this.addChatRoom(this.user?.id)
},
async addChatRoom(userId) {
this.$emit('add-chat-room', userId)
},
closeUserSearch() {
this.$emit('close-user-search')
},
},
}
</script>
<style lang="scss">
.add-chat-room-by-user-search {
background-color: white;
padding: $space-base;
}
.ds-flex.headline {
justify-content: space-between;
}
.ds-flex.headline .close-button {
margin-top: -6px;
}
</style>

View File

@ -4,7 +4,7 @@
<vue-advanced-chat
:theme="theme"
:current-user-id="currentUser.id"
:room-id="null"
:room-id="computedRoomId"
:template-actions="JSON.stringify(templatesText)"
:menu-actions="JSON.stringify(menuActions)"
:text-messages="JSON.stringify(textMessages)"
@ -19,23 +19,42 @@
show-audio="false"
:styles="JSON.stringify(computedChatStyle)"
:show-footer="true"
@send-message="sendMessage($event.detail[0])"
@fetch-messages="fetchMessages($event.detail[0])"
@fetch-more-rooms="fetchRooms"
:responsive-breakpoint="responsiveBreakpoint"
:single-room="singleRoom"
show-reaction-emojis="false"
@send-message="sendMessage($event.detail[0])"
@fetch-messages="fetchMessages($event.detail[0])"
@fetch-more-rooms="fetchRooms"
@add-room="toggleUserSearch"
@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
v-if="selectedRoom && selectedRoom.roomId"
slot="room-options"
class="chat-room-options"
>
<ds-flex v-if="singleRoom">
<ds-flex-item centered class="single-chat-bubble">
<nuxt-link :to="{ name: 'chat' }">
<base-icon name="chat-bubble" />
</nuxt-link>
</ds-flex-item>
<ds-flex-item centered>
<div
class="vac-svg-button vac-room-options"
@click="$emit('close-single-room', true)"
>
<slot name="menu-icon">
<ds-icon name="close" />
</slot>
</div>
</ds-flex-item>
</ds-flex>
</div>
<div slot="room-header-avatar">
<div
v-if="selectedRoom && selectedRoom.avatar && selectedRoom.avatar !== 'default-avatar'"
v-if="selectedRoom && selectedRoom.avatar"
class="vac-avatar"
:style="{ 'background-image': `url('${selectedRoom.avatar}')` }"
/>
@ -46,7 +65,7 @@
<div v-for="room in rooms" :slot="'room-list-avatar_' + room.id" :key="room.id">
<div
v-if="room.avatar && room.avatar !== 'default-avatar'"
v-if="room.avatar"
class="vac-avatar"
:style="{ 'background-image': `url('${room.avatar}')` }"
/>
@ -61,7 +80,12 @@
<script>
import { roomQuery, createRoom, unreadRoomsQuery } from '~/graphql/Rooms'
import { messageQuery, createMessageMutation, markMessagesAsSeen } from '~/graphql/Messages'
import {
messageQuery,
createMessageMutation,
chatMessageAdded,
markMessagesAsSeen,
} from '~/graphql/Messages'
import chatStyle from '~/constants/chat.js'
import { mapGetters, mapMutations } from 'vuex'
@ -70,8 +94,13 @@ export default {
props: {
theme: {
type: String,
default: 'light',
},
singleRoomId: {
singleRoom: {
type: Boolean,
default: false,
},
roomId: {
type: String,
default: null,
},
@ -79,11 +108,6 @@ export default {
data() {
return {
menuActions: [
// NOTE: if menuActions is empty, the related slot is not shown
{
name: 'dummyItem',
title: 'Just a dummy item',
},
/*
{
name: 'inviteUser',
@ -139,8 +163,7 @@ export default {
roomsLoaded: false,
roomPage: 0,
roomPageSize: 10,
singleRoom: !!this.singleRoomId || false,
selectedRoom: null,
selectedRoom: this.roomId,
loadingRooms: true,
messagesLoaded: false,
messagePage: 0,
@ -150,33 +173,47 @@ export default {
},
mounted() {
if (this.singleRoom) {
this.$apollo
.mutate({
mutation: createRoom(),
variables: {
userId: this.singleRoomId,
},
})
.then(({ data: { CreateRoom } }) => {
this.fetchRooms({ room: CreateRoom })
})
.catch((error) => {
this.$toast.error(error)
})
.finally(() => {
// this.loading = false
})
this.newRoom(this.roomId)
} 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: {
...mapGetters({
currentUser: 'auth/user',
getStoreRoomId: 'chat/roomID',
}),
computedChatStyle() {
return chatStyle.STYLE.light
},
computedRoomId() {
let roomId = null
if (!this.singleRoom) {
roomId = this.roomId
if (this.getStoreRoomId.roomId) {
roomId = this.getStoreRoomId.roomId
}
}
return roomId
},
textMessages() {
return {
ROOMS_EMPTY: this.$t('chat.roomsEmpty'),
@ -197,7 +234,9 @@ export default {
methods: {
...mapMutations({
commitUnreadRoomCount: 'chat/UPDATE_ROOM_COUNT',
commitRoomIdFromSingleRoom: 'chat/UPDATE_ROOM_ID',
}),
async fetchRooms({ room } = {}) {
this.roomsLoaded = false
const offset = this.roomPage * this.roomPageSize
@ -214,21 +253,27 @@ export default {
fetchPolicy: 'no-cache',
})
const newRooms = Room.map((r) => {
return {
...r,
users: r.users.map((u) => {
return { ...u, username: u.name, avatar: u.avatar?.url }
}),
const rms = []
const rmsIds = []
;[...Room, ...this.rooms].forEach((r) => {
if (!rmsIds.find((v) => v === r.id)) {
rms.push(this.fixRoomObject(r))
rmsIds.push(r.id)
}
})
this.rooms = [...this.rooms, ...newRooms]
this.rooms = rms
if (Room.length < this.roomPageSize) {
this.roomsLoaded = true
}
this.roomPage += 1
if (this.singleRoom && this.rooms.length > 0) {
this.commitRoomIdFromSingleRoom(this.rooms[0].roomId)
} else if (this.getStoreRoomId.roomId) {
// reset store room id
this.commitRoomIdFromSingleRoom(null)
}
} catch (error) {
this.rooms = []
this.$toast.error(error.message)
@ -258,8 +303,14 @@ export default {
fetchPolicy: 'no-cache',
})
const newMsgIds = Message.filter((m) => m.seen === false).map((m) => m.id)
const newMsgIds = Message.filter(
(m) => m.seen === false && m.senderId !== this.currentUser.id,
).map((m) => m.id)
if (newMsgIds.length) {
const roomIndex = this.rooms.findIndex((r) => r.id === room.id)
const changedRoom = { ...this.rooms[roomIndex] }
changedRoom.unreadCount = changedRoom.unreadCount - newMsgIds.length
this.rooms[roomIndex] = changedRoom
this.$apollo
.mutate({
mutation: markMessagesAsSeen(),
@ -297,21 +348,41 @@ export default {
}
},
async chatMessageAdded({ data }) {
const roomIndex = this.rooms.findIndex((r) => r.id === data.chatMessageAdded.room.id)
const changedRoom = { ...this.rooms[roomIndex] }
changedRoom.lastMessage = data.chatMessageAdded
changedRoom.lastMessage.content = changedRoom.lastMessage.content.trim().substring(0, 30)
changedRoom.lastMessageAt = data.chatMessageAdded.date
changedRoom.unreadCount++
this.rooms[roomIndex] = changedRoom
if (data.chatMessageAdded.room.id === this.selectedRoom?.id) {
this.fetchMessages({ room: this.selectedRoom, options: { refetch: true } })
} else {
this.fetchRooms({ options: { refetch: true } })
}
},
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 {
await this.$apollo.mutate({
const {
data: { CreateMessage: createdMessage },
} = await this.$apollo.mutate({
mutation: createMessageMutation(),
variables: {
roomId: message.roomId,
content: message.content,
},
})
const roomIndex = this.rooms.findIndex((r) => r.id === message.roomId)
const changedRoom = { ...this.rooms[roomIndex] }
changedRoom.lastMessage = createdMessage
changedRoom.lastMessage.content = changedRoom.lastMessage.content.trim().substring(0, 30)
// move current room to top (not 100% working)
// const rooms = [...this.rooms]
// rooms.splice(roomIndex,1)
// this.rooms = [changedRoom, ...rooms]
this.rooms[roomIndex] = changedRoom
} catch (error) {
this.$toast.error(error.message)
}
@ -325,6 +396,58 @@ export default {
if (!fullname) return
return fullname.match(/\b\w/g).join('').substring(0, 3).toUpperCase()
},
toggleUserSearch() {
this.$emit('toggle-user-search')
},
fixRoomObject(room) {
// This fixes the room object which arrives from the backend
const fixedRoom = {
...room,
index: room.lastMessage ? room.lastMessage.date : room.createdAt,
lastMessage: room.lastMessage
? {
...room.lastMessage,
content: room.lastMessage?.content?.trim().substring(0, 30),
}
: null,
users: room.users.map((u) => {
return { ...u, username: u.name, avatar: u.avatar?.url }
}),
}
if (!fixedRoom.avatar) {
// as long as we cannot query avatar on CreateRoom
fixedRoom.avatar = fixedRoom.users.find((u) => u.id !== this.currentUser.id).avatar
}
return fixedRoom
},
newRoom(userId) {
this.$apollo
.mutate({
mutation: createRoom(),
variables: {
userId,
},
})
.then(({ data: { CreateRoom } }) => {
const roomIndex = this.rooms.findIndex((r) => r.id === CreateRoom.roomId)
const room = this.fixRoomObject(CreateRoom)
if (roomIndex === -1) {
this.rooms = [room, ...this.rooms]
}
this.fetchMessages({ room, options: { refetch: true } })
this.$emit('show-chat', CreateRoom.id)
})
.catch((error) => {
this.$toast.error(error.message)
})
.finally(() => {
// this.loading = false
})
},
},
}
</script>
@ -353,4 +476,8 @@ body {
transform: translate(-50%, -50%);
}
}
.ds-flex-item.single-chat-bubble {
margin-right: 1em;
}
</style>

View File

@ -8,7 +8,6 @@
:disabled="disabled && !update"
@click="handleCancel"
data-test="cancel-button"
danger
>
{{ $t('actions.cancel') }}
</base-button>

View File

@ -58,14 +58,14 @@ export default {
routes.push({
label: this.$t('group.contentMenu.visitGroupPage'),
icon: 'home',
name: 'group-id-slug',
path: `/groups/${this.group.id}`,
params: { id: this.group.id, slug: this.group.slug },
})
}
if (this.group.myRole === 'owner') {
routes.push({
label: this.$t('admin.settings.name'),
path: `/group/edit/${this.group.id}`,
path: `/groups/edit/${this.group.id}`,
icon: 'edit',
})
}

View File

@ -169,12 +169,7 @@
</ds-flex-item>
<ds-flex-item width="0.15" />
<ds-flex-item class="action-buttons-group" width="2">
<base-button
data-test="cancel-button"
:disabled="loading"
@click="$router.back()"
danger
>
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()">
{{ $t('actions.cancel') }}
</base-button>
<base-button type="submit" icon="check" :loading="loading" :disabled="errors" filled>

View File

@ -3,33 +3,7 @@
<h2 class="title">{{ $t('group.addUser') }}</h2>
<ds-space margin-bottom="small" />
<ds-space>
<ds-select
type="search"
icon="search"
label-prop="id"
v-model="query"
:id="id"
:icon-right="null"
:options="users"
:loading="$apollo.queries.searchUsers.loading"
:filter="(item) => item"
:no-options-available="$t('group.addUserNoOptions')"
:auto-reset-search="true"
:placeholder="$t('group.addUserPlaceholder')"
@focus.capture.native="onFocus"
@input.native="handleInput"
@keyup.enter.native="onEnter"
@keyup.delete.native="onDelete"
@keyup.esc.native="clear"
@blur.capture.native="onBlur"
@input.exact="onSelect"
>
<template #option="{ option }">
<p>
<user-teaser :user="option" :showPopover="false" :linkToProfile="false" />
</p>
</template>
</ds-select>
<select-user-search :id="id" ref="selectUserSearch" @select-user="selectUser" />
<ds-modal
v-if="isOpen"
force
@ -49,16 +23,15 @@
</ds-space>
</div>
</template>
<script>
import { changeGroupMemberRoleMutation } from '~/graphql/groups.js'
import { searchUsers } from '~/graphql/Search.js'
import UserTeaser from '~/components/UserTeaser/UserTeaser.vue'
import { isEmpty } from 'lodash'
import SelectUserSearch from '~/components/generic/SelectUserSearch/SelectUserSearch'
export default {
name: 'AddGroupMember',
components: {
UserTeaser,
SelectUserSearch,
},
props: {
groupId: {
@ -72,62 +45,34 @@ export default {
},
data() {
return {
users: [],
id: 'search-user-to-add-to-group',
query: '',
user: {},
isOpen: false,
}
},
computed: {
startSearch() {
return this.query && this.query.length > 3
},
},
methods: {
cancelModal() {
this.clear()
this.$refs.selectUserSearch.clear()
this.isOpen = false
},
closeModal() {
this.clear()
this.$refs.selectUserSearch.clear()
this.isOpen = false
},
confirmModal() {
this.addMemberToGroup()
this.isOpen = false
this.clear()
this.$refs.selectUserSearch.clear()
},
onFocus() {},
onBlur() {
this.query = ''
},
handleInput(event) {
this.query = event.target ? event.target.value.trim() : ''
},
onDelete(event) {
const value = event.target ? event.target.value.trim() : ''
if (isEmpty(value)) {
this.clear()
} else {
this.handleInput(event)
}
},
clear() {
this.query = ''
this.user = {}
this.users = []
},
onSelect(item) {
this.user = item
selectUser(user) {
this.user = user
if (this.groupMembers.find((member) => member.id === this.user.id)) {
this.$toast.error(this.$t('group.errors.userAlreadyMember', { name: this.user.name }))
this.clear()
this.$refs.selectUserSearch.clear()
return
}
this.isOpen = true
},
onEnter() {},
async addMemberToGroup() {
const newRole = 'usual'
const username = this.user.name
@ -148,29 +93,9 @@ export default {
}
},
},
apollo: {
searchUsers: {
query() {
return searchUsers
},
variables() {
return {
query: this.query,
firstUsers: 5,
usersOffset: 0,
}
},
skip() {
return !this.startSearch
},
update({ searchUsers }) {
this.users = searchUsers.users
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>
<style lang="scss">
.add-group-member {
background-color: white;

View File

@ -1,7 +1,7 @@
<template>
<nuxt-link
class="group-teaser"
:to="{ name: 'group-id-slug', params: { id: group.id, slug: group.slug } }"
:to="{ name: 'groups-id-slug', params: { id: group.id, slug: group.slug } }"
>
<base-card
:class="{

View File

@ -9,7 +9,7 @@
<p class="description">{{ $t(`notifications.reason.${notification.reason}`) }}</p>
<nuxt-link
class="link"
:to="{ name: isGroup ? 'group-id-slug' : 'post-id-slug', params, ...hashParam }"
:to="{ name: isGroup ? 'groups-id-slug' : 'post-id-slug', params, ...hashParam }"
@click.native="$emit('read')"
>
<base-card wideContent>

View File

@ -65,7 +65,7 @@
class="notification-mention-post"
:class="{ 'notification-status': notification.read }"
:to="{
name: isGroup(notification.from) ? 'group-id-slug' : 'post-id-slug',
name: isGroup(notification.from) ? 'groups-id-slug' : 'post-id-slug',
params: params(notification.from),
hash: hashParam(notification.from),
}"

View File

@ -96,7 +96,7 @@ export default {
groupLink() {
const { id, slug } = this.group
if (!(id && slug)) return ''
return { name: 'group-id-slug', params: { slug, id } }
return { name: 'groups-id-slug', params: { slug, id } }
},
groupSlug() {
const { slug } = this.group || {}

View File

@ -56,7 +56,7 @@
>
{{ isEditing ? $t('actions.save') : texts.addButton }}
</base-button>
<base-button v-if="isEditing" id="cancel" danger @click="handleCancel()">
<base-button v-if="isEditing" id="cancel" @click="handleCancel()">
{{ $t('actions.cancel') }}
</base-button>
</ds-space>

View File

@ -148,7 +148,7 @@ export default {
case 'User':
return 'profile-id-slug'
case 'Group':
return 'group-id-slug'
return 'groups-id-slug'
default:
return null
}

View File

@ -0,0 +1,109 @@
<template>
<ds-select
class="select-user-search"
type="search"
icon="search"
label-prop="id"
v-model="query"
:id="id"
:icon-right="null"
:options="users"
:loading="$apollo.queries.searchUsers.loading"
:filter="(item) => item"
:no-options-available="$t('group.addUserNoOptions')"
:auto-reset-search="true"
:placeholder="$t('group.addUserPlaceholder')"
@focus.capture.native="onFocus"
@input.native="handleInput"
@keyup.enter.native="onEnter"
@keyup.delete.native="onDelete"
@keyup.esc.native="clear"
@blur.capture.native="onBlur"
@input.exact="onSelect"
>
<template #option="{ option }">
<p>
<user-teaser :user="option" :showPopover="false" :linkToProfile="false" />
</p>
</template>
</ds-select>
</template>
<script>
import { isEmpty } from 'lodash'
import { searchUsers } from '~/graphql/Search.js'
import UserTeaser from '~/components/UserTeaser/UserTeaser.vue'
export default {
name: 'SelectUserSearch',
components: {
UserTeaser,
},
props: {
id: {
type: String,
required: true,
},
},
data() {
return {
users: [],
query: '',
user: {},
}
},
computed: {
startSearch() {
return this.query && this.query.length > 3
},
},
methods: {
onFocus() {},
onBlur() {
this.query = ''
},
handleInput(event) {
this.query = event.target ? event.target.value.trim() : ''
},
onDelete(event) {
const value = event.target ? event.target.value.trim() : ''
if (isEmpty(value)) {
this.clear()
} else {
this.handleInput(event)
}
},
clear() {
this.query = ''
this.user = {}
this.users = []
},
onSelect(item) {
this.user = item
this.$emit('select-user', this.user)
},
onEnter() {},
},
apollo: {
searchUsers: {
query() {
return searchUsers
},
variables() {
return {
query: this.query,
firstUsers: 5,
usersOffset: 0,
}
},
skip() {
return !this.startSearch
},
update({ searchUsers }) {
this.users = searchUsers.users
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>

View File

@ -65,7 +65,7 @@ const STYLE = {
backgroundSelected: '#c2dcf2',
colorDeleted: '#757e85',
colorUsername: '#9ca6af',
colorTimestamp: '#828c94',
colorTimestamp: styleData.chatMessageTimestamp,
backgroundDate: '#e5effa',
colorDate: '#505a62',
backgroundSystem: '#e5effa',
@ -134,8 +134,8 @@ const STYLE = {
emojiReaction: 'rgba(0, 0, 0, 0.3)',
document: styleData.colorPrimary,
pencil: '#9e9e9e',
checkmark: '#9e9e9e',
checkmarkSeen: '#0696c7',
checkmark: styleData.chatMessageCheckmark,
checkmarkSeen: styleData.chatMessageCheckmarkSeen,
eye: '#fff',
dropdownMessage: '#fff',
dropdownMessageBackground: 'rgba(0, 0, 0, 0.25)',

View File

@ -1,5 +1,31 @@
import gql from 'graphql-tag'
export const createMessageMutation = () => {
return gql`
mutation ($roomId: ID!, $content: String!) {
CreateMessage(roomId: $roomId, content: $content) {
#_id
id
indexId
content
senderId
author {
id
}
username
avatar
date
room {
id
}
saved
distributed
seen
}
}
`
}
export const messageQuery = () => {
return gql`
query ($roomId: ID!, $first: Int, $offset: Int) {
@ -15,6 +41,9 @@ export const messageQuery = () => {
username
avatar
date
room {
id
}
saved
distributed
seen
@ -23,12 +52,27 @@ export const messageQuery = () => {
`
}
export const createMessageMutation = () => {
export const chatMessageAdded = () => {
return gql`
mutation ($roomId: ID!, $content: String!) {
CreateMessage(roomId: $roomId, content: $content) {
subscription chatMessageAdded($userId: ID!) {
chatMessageAdded(userId: $userId) {
_id
id
indexId
content
senderId
author {
id
}
username
avatar
date
room {
id
}
saved
distributed
seen
}
}
`

View File

@ -1,12 +1,15 @@
import gql from 'graphql-tag'
export const roomQuery = () => gql`
query Room($first: Int, $offset: Int, $id: ID) {
Room(first: $first, offset: $offset, id: $id, orderBy: createdAt_desc) {
export const createRoom = () => gql`
mutation ($userId: ID!) {
CreateRoom(userId: $userId) {
id
roomId
roomName
avatar
lastMessageAt
createdAt
unreadCount
#avatar
users {
_id
id
@ -19,11 +22,36 @@ export const roomQuery = () => gql`
}
`
export const createRoom = () => gql`
mutation ($userId: ID!) {
CreateRoom(userId: $userId) {
export const roomQuery = () => gql`
query Room($first: Int, $offset: Int, $id: ID) {
Room(first: $first, offset: $offset, id: $id, orderBy: [createdAt_desc, lastMessageAt_desc]) {
id
roomId
roomName
avatar
lastMessageAt
createdAt
unreadCount
lastMessage {
_id
id
content
senderId
username
avatar
date
saved
distributed
seen
}
users {
_id
id
name
avatar {
url
}
}
}
}
`

View File

@ -13,33 +13,40 @@
<client-only>
<modal />
</client-only>
<div v-if="$store.getters['chat/showChat'].showChat" class="chat-modul">
<chat-module
v-on:close-single-room="closeSingleRoom"
:singleRoomId="$store.getters['chat/showChat'].roomID"
/>
<div v-if="getShowChat.showChat" class="chat-modul">
<chat singleRoom :roomId="getShowChat.roomID" @close-single-room="closeSingleRoom" />
</div>
</div>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import seo from '~/mixins/seo'
import mobile from '~/mixins/mobile'
import HeaderMenu from '~/components/HeaderMenu/HeaderMenu'
import Modal from '~/components/Modal'
import PageFooter from '~/components/PageFooter/PageFooter'
import ChatModule from '~/components/Chat/Chat.vue'
import Chat from '~/components/Chat/Chat.vue'
export default {
components: {
HeaderMenu,
Modal,
PageFooter,
ChatModule,
Chat,
},
mixins: [seo, mobile()],
computed: {
...mapGetters({
getShowChat: 'chat/showChat',
}),
},
methods: {
...mapMutations({
showChat: 'chat/SET_OPEN_CHAT',
}),
closeSingleRoom() {
this.$store.commit('chat/SET_OPEN_CHAT', { showChat: false, roomID: null })
this.showChat({ showChat: false, roomID: null })
},
},
beforeCreate() {

View File

@ -78,6 +78,7 @@
}
},
"chat": {
"addRoomHeadline": "Suche Nutzer für neuen Chat",
"cancelSelectMessage": "Abbrechen",
"conversationStarted": "Unterhaltung startete am:",
"isOnline": "online",
@ -86,9 +87,12 @@
"messageDeleted": "Diese Nachricht wuerde gelöscht",
"messagesEmpty": "Keine Nachrichten",
"newMessages": "Neue Nachrichten",
"page": {
"headline": "Chat"
},
"roomEmpty": "Keinen Raum selektiert",
"roomsEmpty": "Keine Räume",
"search": "Suche",
"search": "Chat-Räume filtern",
"typeMessage": "Nachricht schreiben",
"userProfileButton": {
"label": "Chat",

View File

@ -78,6 +78,7 @@
}
},
"chat": {
"addRoomHeadline": "Search User for new Chat",
"cancelSelectMessage": "Cancel",
"conversationStarted": "Conversation started on:",
"isOnline": "is online",
@ -86,9 +87,12 @@
"messageDeleted": "This message was deleted",
"messagesEmpty": "No messages",
"newMessages": "New Messages",
"page": {
"headline": "Chat"
},
"roomEmpty": "No room selected",
"roomsEmpty": "No rooms",
"search": "Search",
"search": "Filter chat rooms",
"typeMessage": "Type message",
"userProfileButton": {
"label": "Chat",
@ -472,7 +476,7 @@
"addMemberToGroupSuccess": "“{name}” was added to the group with the role “{role}”!",
"addUser": "Add User",
"addUserNoOptions": "No users found!",
"addUserPlaceholder": " Username",
"addUserPlaceholder": "User name",
"allGroups": "All Groups",
"button": {
"tooltip": "Show groups"

View File

@ -75,6 +75,9 @@
}
}
},
"chat": {
"search": "Filtrar salas de chat"
},
"code-of-conduct": {
"subheader": "para la red social de {ORGANIZATION_NAME}"
},

View File

@ -75,6 +75,9 @@
}
}
},
"chat": {
"search": "Filtrer les salons de chat"
},
"code-of-conduct": {
"subheader": "pour le réseau social de {ORGANIZATION_NAME}"
},

View File

@ -1,11 +1,54 @@
<template>
<chat />
<div>
<ds-heading tag="h1">{{ $t('chat.page.headline') }}</ds-heading>
<add-chat-room-by-user-search
v-if="showUserSearch"
@add-chat-room="addChatRoom"
@close-user-search="showUserSearch = false"
/>
<ds-space margin-bottom="small" />
<chat
:roomId="getShowChat.showChat ? getShowChat.roomID : null"
ref="chat"
@toggle-user-search="showUserSearch = !showUserSearch"
:show-room="showRoom"
/>
</div>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import AddChatRoomByUserSearch from '~/components/Chat/AddChatRoomByUserSearch'
import Chat from '../components/Chat/Chat.vue'
export default {
components: { Chat },
components: {
AddChatRoomByUserSearch,
Chat,
},
data() {
return {
showUserSearch: false,
}
},
mounted() {
this.showChat({ showChat: false, roomID: null })
},
computed: {
...mapGetters({
getShowChat: 'chat/showChat',
}),
},
methods: {
...mapMutations({
showChat: 'chat/SET_OPEN_CHAT',
}),
addChatRoom(userID) {
this.$refs.chat.newRoom(userID)
},
showRoom(roomId) {
this.showChat({ showChat: true, roomID: roomId })
},
},
}
</script>

View File

@ -24,7 +24,7 @@ const options = {
}
`,
message: 'error-pages.group-not-found',
path: 'group',
path: 'groups',
}
const persistentLinks = PersistentLinks(options)

View File

@ -59,7 +59,7 @@ export default {
})
this.$toast.success(this.$t('group.groupCreated'))
this.$router.history.push({
name: 'group-id-slug',
name: 'groups-id-slug',
params: { id: responseId, slug: responseSlug },
})
} catch (error) {

View File

@ -33,11 +33,11 @@ export default {
return [
{
name: this.$t('group.general'),
path: `/group/edit/${this.group.id}`,
path: `/groups/edit/${this.group.id}`,
},
{
name: this.$t('group.members'),
path: `/group/edit/${this.group.id}/members`,
path: `/groups/edit/${this.group.id}/members`,
},
]
},

View File

@ -60,7 +60,7 @@ export default {
})
this.$toast.success(this.$t('group.updatedGroup'))
this.$router.history.push({
name: 'group-id-slug',
name: 'groups-id-slug',
params: { id: responseId, slug: responseSlug },
})
} catch (error) {

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import groups from './groups.vue'
import groups from './index.vue'
const localVue = global.localVue

View File

@ -7,7 +7,7 @@
<ds-space>
<!-- create group -->
<ds-space centered>
<nuxt-link :to="{ name: 'group-create' }">
<nuxt-link :to="{ name: 'groups-create' }">
<base-button
class="group-add-button"
icon="plus"

View File

@ -85,7 +85,7 @@
content: $t('chat.userProfileButton.tooltip', { name: userName }),
placement: 'bottom-start',
}"
@click="showChat({ showChat: true, roomID: user.id })"
@click="showOrChangeChat(user.id)"
>
{{ $t('chat.userProfileButton.label') }}
</base-button>
@ -182,7 +182,7 @@
<script>
import uniqBy from 'lodash/uniqBy'
import { mapMutations } from 'vuex'
import { mapGetters, mapMutations } from 'vuex'
import postListActions from '~/mixins/postListActions'
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
import HcFollowButton from '~/components/Button/FollowButton'
@ -254,6 +254,9 @@ export default {
}
},
computed: {
...mapGetters({
getShowChat: 'chat/showChat',
}),
myProfile() {
return this.$route.params.id === this.$store.getters['auth/user'].id
},
@ -403,6 +406,12 @@ export default {
if (type === 'following') this.followingCount = 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: {
profilePagePosts: {

View File

@ -14,6 +14,9 @@ export const mutations = {
UPDATE_ROOM_COUNT(state, count) {
state.unreadRoomCount = count
},
UPDATE_ROOM_ID(state, roomid) {
state.roomId = roomid || null
},
}
export const getters = {