Merge branch 'master' into message-pagination

This commit is contained in:
Ulf Gebhardt 2023-07-14 09:13:40 +02:00
commit a9ad8c34c8
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
28 changed files with 1766 additions and 1689 deletions

View File

@ -24,22 +24,37 @@ jobs:
- name: boot up test system | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
- name: cypress | Fullstack tests
id: e2e-tests
- name: Full stack tests | prepare
run: |
wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
chmod +x /opt/cucumber-json-formatter
sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter
cd backend
yarn install
yarn build
cd ..
yarn install
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
##########################################################################
# UPLOAD SCREENSHOTS - IF TESTS FAIL #####################################
##########################################################################
- name: Full stack tests | if any test failed, upload screenshots
- name: Full stack tests | run tests
id: e2e-tests
run: yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
- name: Full stack tests | if tests failed, compile html report
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
run: |
cd cypress/
node create-cucumber-html-report.js
- name: End-to-end tests | if tests failed, get pr number
id: pr
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: 8BitJonny/gh-get-current-pr@2.2.0
- name: End-to-end tests | if tests failed, upload report
id: e2e-report
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: cypress/screenshots/
name: ocelot-e2e-test-report-pr${{ steps.pr.outputs.number }}
path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report

View File

@ -11,7 +11,7 @@ module.exports = {
],
coverageThreshold: {
global: {
lines: 70,
lines: 67,
},
},
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,9 @@ export const createMessageMutation = () => {
CreateMessage(roomId: $roomId, content: $content) {
id
content
saved
distributed
seen
}
}
`
@ -23,7 +26,18 @@ export const messageQuery = () => {
username
avatar
date
saved
distributed
seen
}
}
`
}
export const markMessagesAsSeen = () => {
return gql`
mutation ($messageIds: [String!]) {
MarkMessagesAsSeen(messageIds: $messageIds)
}
`
}

View File

@ -463,6 +463,7 @@ export default shield(
saveCategorySettings: isAuthenticated,
CreateRoom: isAuthenticated,
CreateMessage: isAuthenticated,
MarkMessagesAsSeen: isAuthenticated,
},
User: {
email: or(isMyOwn, isAdmin),

View File

@ -2,7 +2,7 @@ import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories'
import { getNeode, getDriver } from '../../db/neo4j'
import { createRoomMutation } from '../../graphql/rooms'
import { createMessageMutation, messageQuery } from '../../graphql/messages'
import { createMessageMutation, messageQuery, markMessagesAsSeen } from '../../graphql/messages'
import createServer from '../../server'
const driver = getDriver()
@ -122,6 +122,9 @@ describe('Message', () => {
CreateMessage: {
id: expect.any(String),
content: 'Some nice message to other chatting user',
saved: true,
distributed: false,
seen: false,
},
},
})
@ -218,6 +221,9 @@ describe('Message', () => {
username: 'Chatting User',
avatar: expect.any(String),
date: expect.any(String),
saved: true,
distributed: true,
seen: false,
},
],
},
@ -263,6 +269,9 @@ describe('Message', () => {
username: 'Chatting User',
avatar: expect.any(String),
date: expect.any(String),
saved: true,
distributed: true,
seen: false,
}),
expect.objectContaining({
id: expect.any(String),
@ -272,6 +281,9 @@ describe('Message', () => {
username: 'Other Chatting User',
avatar: expect.any(String),
date: expect.any(String),
saved: true,
distributed: true,
seen: false,
}),
expect.objectContaining({
id: expect.any(String),
@ -281,6 +293,9 @@ describe('Message', () => {
username: 'Chatting User',
avatar: expect.any(String),
date: expect.any(String),
saved: true,
distributed: false,
seen: false,
}),
],
},
@ -375,4 +390,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 }),
],
},
})
})
})
})
})

View File

@ -17,8 +17,35 @@ export default {
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
if (resolved) {
const undistributedMessagesIds = resolved
.filter((msg) => !msg.distributed && msg.senderId !== context.user.id)
.map((msg) => msg.id)
if (undistributedMessagesIds.length > 0) {
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const setDistributedCypher = `
MATCH (m:Message) WHERE m.id IN $undistributedMessagesIds
SET m.distributed = true
RETURN m { .* }
`
const setDistributedTxResponse = await transaction.run(setDistributedCypher, {
undistributedMessagesIds,
})
const messages = await setDistributedTxResponse.records.map((record) => record.get('m'))
return messages
})
try {
await writeTxResultPromise
} finally {
session.close()
}
// send subscription to author to updated the messages
}
resolved.forEach((message) => {
message._id = message.id
if (message.senderId !== context.user.id) {
message.distributed = true
}
})
}
return resolved.reverse()
@ -40,7 +67,10 @@ 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: $content,
saved: true,
distributed: false,
seen: false
})-[:INSIDE]->(room)
RETURN message { .* }
`
@ -63,6 +93,32 @@ export default {
session.close()
}
},
MarkMessagesAsSeen: async (_parent, params, context, _resolveInfo) => {
const { messageIds } = params
const currentUserId = context.user.id
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const setSeenCypher = `
MATCH (m:Message)<-[:CREATED]-(user:User)
WHERE m.id IN $messageIds AND NOT user.id = $currentUserId
SET m.seen = true
RETURN m { .* }
`
const setSeenTxResponse = await transaction.run(setSeenCypher, {
messageIds,
currentUserId,
})
const messages = await setSeenTxResponse.records.map((record) => record.get('m'))
return messages
})
try {
await writeTxResultPromise
// send subscription to author to updated the messages
return true
} finally {
session.close()
}
},
},
Message: {
...Resolver('Message', {

View File

@ -25,4 +25,3 @@ type LocationMapBox {
type Query {
queryLocations(place: String!, lang: String!): [LocationMapBox]!
}

View File

@ -22,6 +22,10 @@ type Message {
username: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.name")
avatar: String @cypher(statement: "MATCH (this)<-[:CREATED]-(:User)-[:AVATAR_IMAGE]->(image:Image) RETURN image.url")
date: String! @cypher(statement: "RETURN this.createdAt")
saved: Boolean
distributed: Boolean
seen: Boolean
}
type Mutation {
@ -29,6 +33,8 @@ type Mutation {
roomId: ID!
content: String!
): Message
MarkMessagesAsSeen(messageIds: [String!]): Boolean
}
type Query {

View File

@ -84,8 +84,8 @@ input _PostFilter {
group: _GroupFilter
postsInMyGroups: Boolean
postType_in: [PostType]
eventStart_gte: String
eventEnd_gte: String
eventStart_gte: String
eventEnd_gte: String
}
enum _PostOrdering {

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

View File

@ -21,13 +21,7 @@ async function setupNodeEvents(on, config) {
return testStore[name]
},
});
on("after:run", (results) => {
if (results) {
console.log(results.status);
}
});
return config;
}

View File

@ -13,7 +13,6 @@
// Cypress.Commands.add('login', (email, password) => { ... })
/* globals Cypress cy */
import "cypress-file-upload";
import { GraphQLClient, request } from 'graphql-request'
import CONFIG from '../../backend/build/src/config'

View File

@ -1,28 +1,27 @@
import { Then } from "@badeball/cypress-cucumber-preprocessor";
Then("I should be able to {string} a teaser image", condition => {
// cy.reload()
let postTeaserImage = ""
switch(condition){
case 'change':
cy.get('.delete-image-button')
case "change":
postTeaserImage = "humanconnection.png"
cy.get(".delete-image-button")
.click()
cy.fixture('humanconnection.png').as('postTeaserImage').then(function() {
cy.get("#postdropzone").upload(
{ fileContent: this.postTeaserImage, fileName: 'humanconnection.png', mimeType: "image/png" },
{ subjectType: "drag-n-drop", force: true }
).wait(750);
})
cy.get("#postdropzone").selectFile(
{ contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" },
{ action: "drag-drop", force: true }
).wait(750);
break;
case 'add':
cy.fixture('onourjourney.png').as('postTeaserImage').then(function() {
cy.get("#postdropzone").upload(
{ fileContent: this.postTeaserImage, fileName: 'onourjourney.png', mimeType: "image/png" },
{ subjectType: "drag-n-drop", force: true }
).wait(750);
})
case "add":
postTeaserImage = "onourjourney.png"
cy.get("#postdropzone").selectFile(
{ contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" },
{ action: "drag-drop", force: true }
).wait(750);
break;
case 'remove':
cy.get('.delete-image-button')
case "remove":
cy.get(".delete-image-button")
.click()
break;
}

View File

@ -2,13 +2,11 @@ import { Then } from "@badeball/cypress-cucumber-preprocessor";
Then("I should be able to change my profile picture", () => {
const avatarUpload = "onourjourney.png";
cy.fixture(avatarUpload, "base64").then(fileContent => {
cy.get("#customdropzone").upload(
{ fileContent, fileName: avatarUpload, mimeType: "image/png" },
{ subjectType: "drag-n-drop", force: true }
);
});
cy.get("#customdropzone").selectFile(
{ contents: `cypress/fixtures/${avatarUpload}`, fileName: avatarUpload, mimeType: "image/png" },
{ action: "drag-drop" }
);
cy.get(".profile-page-avatar img")
.should("have.attr", "src")
.and("contains", "onourjourney");

@ -1 +1 @@
Subproject commit 350237c62dcff1a5c34f1e8d718f89b05ce3d33f
Subproject commit fdc2e52fa444b300e1c4736600bc0e9ae3314222

View File

@ -10,13 +10,25 @@
"url": "https://github.com/Ocelot-Social-Community/Ocelot-Social.git"
},
"cypress-cucumber-preprocessor": {
"nonGlobalStepDefinitions": true
"stepDefinitions": "cypress/support/step_definitions/**/*.js",
"json": {
"enabled": true,
"output": "cypress/reports/json_logs/cucumber_log.json",
"formatter": "cucumber-json-formatter"
},
"messages": {
"enabled": true,
"output": "cypress/reports/json_logs/messages.ndjson"
},
"html": {
"enabled": false
}
},
"scripts": {
"db:seed": "cd backend && yarn run db:seed",
"db:reset": "cd backend && yarn run db:reset",
"cypress:run": "cypress run --browser electron --config-file ./cypress/cypress.config.js",
"cypress:open": "cypress open --browser electron --config-file ./cypress/cypress.config.js",
"cypress:run": "cypress run --e2e --browser electron --config-file ./cypress/cypress.config.js",
"cypress:open": "cypress open --e2e --browser electron --config-file ./cypress/cypress.config.js",
"cucumber:setup": "cd backend && yarn run dev",
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
"release": "yarn version --no-git-tag-version --no-commit-hooks --no-commit && auto-changelog --latest-version $(node -p -e \"require('./package.json').version\") && cd backend && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp/maintenance/source && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../../../package.json').version\")"
@ -33,7 +45,6 @@
"cross-env": "^7.0.3",
"cucumber": "^6.0.5",
"cypress": "^12.17.0",
"cypress-file-upload": "^3.5.3",
"cypress-network-idle": "^1.14.2",
"date-fns": "^2.25.0",
"dotenv": "^8.2.0",
@ -42,6 +53,7 @@
"import": "^0.0.6",
"jsonwebtoken": "^8.5.1",
"mock-socket": "^9.0.3",
"multiple-cucumber-html-reporter": "^3.4.0",
"neo4j-driver": "^4.3.4",
"neode": "^0.4.8",
"rosie": "^2.1.0",

View File

@ -0,0 +1,30 @@
: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;
}

View File

@ -406,4 +406,14 @@ $color-toast-green: $color-success;
$color-ribbon-event: $background-color-third;
$color-ribbon-event-active: $background-color-third-active;
$color-ribbon-article: $background-color-secondary;
$color-ribbon-article-active: $background-color-secondary-active;
$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;

View File

@ -8,6 +8,7 @@
:template-actions="JSON.stringify(templatesText)"
:menu-actions="JSON.stringify(menuActions)"
:text-messages="JSON.stringify(textMessages)"
:message-actions="messageActions"
:messages="JSON.stringify(messages)"
:messages-loaded="messagesLoaded"
:rooms="JSON.stringify(rooms)"
@ -15,13 +16,43 @@
:rooms-loaded="true"
show-files="false"
show-audio="false"
:styles="JSON.stringify(computedChatStyle)"
:show-footer="true"
@send-message="sendMessage($event.detail[0])"
@fetch-messages="fetchMessages($event.detail[0])"
:responsive-breakpoint="responsiveBreakpoint"
:single-room="singleRoom"
show-reaction-emojis="false"
@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>
</div>
</template>
@ -29,6 +60,7 @@
<script>
import { roomQuery, createRoom } from '~/graphql/Rooms'
import { messageQuery, createMessageMutation } from '~/graphql/Messages'
import chatStyle from '~/constants/chat.js'
import { mapGetters } from 'vuex'
export default {
@ -45,8 +77,13 @@ 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',
title: 'Invite User',
},
@ -83,18 +120,18 @@ export default {
},
],
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',
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'),
},
roomActions: [
/*
@ -117,7 +154,7 @@ export default {
messagePageSize: 20,
roomPage: 0,
roomPageSize: 999, // TODO pagination is a problem with single rooms - cant use
lastRoom: null,
selectedRoom: null,
}
},
mounted() {
@ -144,13 +181,18 @@ export default {
...mapGetters({
currentUser: 'auth/user',
}),
computedChatStyle() {
// TODO light/dark theme still needed?
// return this.theme === 'light' ? chatStyle.STYLE.light : chatStyle.STYLE.dark
return chatStyle.STYLE.light
},
},
methods: {
async fetchMessages({ room, options = {} }) {
if (this.lastRoom !== room.id) {
if (this.selectedRoom !== room.id) {
this.messages = []
this.messagePage = 0
this.lastRoom = room.id
this.selectedRoom = room.id
}
this.messagesLoaded = options.refetch ? this.messagesLoaded : false
const offset = (options.refetch ? 0 : this.messagePage) * this.messagePageSize
@ -204,6 +246,11 @@ export default {
}
this.refetchMessage(message.roomId)
},
getInitialsName(fullname) {
if (!fullname) return
return fullname.match(/\b\w/g).join('').substring(0, 3).toUpperCase()
},
},
apollo: {
Rooms: {
@ -250,4 +297,25 @@ export default {
body {
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>

299
webapp/constants/chat.js Normal file
View 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: '#828c94',
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: '#9e9e9e',
checkmarkSeen: '#0696c7',
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,
}

View File

@ -77,6 +77,10 @@ export const filterPosts = (i18n) => {
eventEnd
eventVenue
eventLocationName
eventLocation {
lng
lat
}
eventIsOnline
...post
...postCounts

View File

@ -14,15 +14,11 @@
<modal />
</client-only>
<div v-if="$store.getters['chat/showChat'].showChat" class="chat-modul">
<ds-text align="right" class="close">
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" />
<chat-module
v-on:close-single-room="closeSingleRoom"
:singleRoomId="$store.getters['chat/showChat'].roomID"
/>
</div>
>
</div>
</template>
<script>
@ -41,6 +37,11 @@ export default {
ChatModule,
},
mixins: [seo, mobile()],
methods: {
closeSingleRoom() {
this.$store.commit('chat/SET_OPEN_CHAT', { showChat: false, roomID: null })
},
},
beforeCreate() {
this.$store.commit('chat/SET_OPEN_CHAT', { showChat: false, roomID: null })
},
@ -58,7 +59,6 @@ export default {
.chat-modul {
background-color: rgb(233, 228, 228);
height: 667px;
width: 355px;
position: fixed;
bottom: 45px;

View File

@ -78,6 +78,18 @@
}
},
"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",
"roomEmpty": "Keinen Raum selektiert",
"roomsEmpty": "Keine Räume",
"search": "Suche",
"typeMessage": "Nachricht schreiben",
"userProfileButton": {
"label": "Chat",
"tooltip": "Chatte mit „{name}“"
@ -604,9 +616,16 @@
"button": {
"tooltip": "Landkarte anzeigen"
},
"markerTypes": {
"legend": {
"event": "Veranstaltung",
"group": "Gruppe",
"theUser": "deine Position",
"theUser": "Meine Position",
"user": "Nutzer"
},
"markerTypes": {
"event": "Veranstaltung",
"group": "Gruppe",
"theUser": "meine Position",
"user": "Nutzer"
},
"pageTitle": "Landkarte",
@ -715,11 +734,6 @@
"title": "Benachrichtigungen",
"user": "Nutzer"
},
"position": {
"group": "Gruppe",
"my": "Meine Position",
"user": "Nutzer"
},
"post": {
"comment": {
"reply": "Antworten",

View File

@ -78,6 +78,18 @@
}
},
"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",
"roomEmpty": "No room selected",
"roomsEmpty": "No rooms",
"search": "Search",
"typeMessage": "Type message",
"userProfileButton": {
"label": "Chat",
"tooltip": "Chat with “{name}”"
@ -604,9 +616,16 @@
"button": {
"tooltip": "Show map"
},
"legend": {
"event": "Event",
"group": "Group",
"theUser": "My position",
"user": "User"
},
"markerTypes": {
"event": "event",
"group": "group",
"theUser": "your position",
"theUser": "my position",
"user": "user"
},
"pageTitle": "Map",
@ -715,11 +734,6 @@
"title": "Notifications",
"user": "User"
},
"position": {
"group": "Group",
"my": "My position",
"user": "User"
},
"post": {
"comment": {
"reply": "Reply",

View File

@ -105,6 +105,7 @@ export default {
styleguideStyles,
'~assets/_new/styles/tokens.scss',
'~assets/styles/imports/_branding.scss',
'~assets/_new/styles/export.scss',
],
},

View File

@ -5,16 +5,15 @@
<ds-heading tag="h1">{{ $t('map.pageTitle') }}</ds-heading>
<small>
<div>
<img
alt="my position"
src="/img/mapbox/marker-icons/mapbox-marker-icon-orange.svg"
width="15"
/>
{{ $t('position.my') }}
<img alt="user" src="/img/mapbox/marker-icons/mapbox-marker-icon-green.svg" width="15" />
{{ $t('position.user') }}
<img alt="group" src="/img/mapbox/marker-icons/mapbox-marker-icon-blue.svg" width="15" />
{{ $t('position.group') }}
<span v-for="type in markers.types" :key="type.id">
<img
:alt="$t('map.legend.' + type.id)"
:src="'/img/mapbox/marker-icons/' + type.icon.legendName"
width="15"
/>
{{ $t('map.legend.' + type.id) }}
&nbsp;&nbsp;
</span>
</div>
</small>
</ds-space>
@ -66,6 +65,7 @@ import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
import { mapGetters } from 'vuex'
import { profileUserQuery, mapUserQuery } from '~/graphql/User'
import { groupQuery } from '~/graphql/groups'
import { filterPosts } from '~/graphql/PostQuery.js'
import mobile from '~/mixins/mobile'
import Empty from '~/components/Empty/Empty'
import MapStylesButtons from '~/components/Map/MapStylesButtons'
@ -95,19 +95,40 @@ export default {
currentUserCoordinates: null,
users: null,
groups: null,
posts: null,
markers: {
icons: [
types: [
{
id: 'marker-blue',
name: 'mapbox-marker-icon-20px-blue.png',
id: 'theUser',
icon: {
id: 'marker-orange',
legendName: 'mapbox-marker-icon-orange.svg',
mapName: 'mapbox-marker-icon-20px-orange.png',
},
},
{
id: 'marker-orange',
name: 'mapbox-marker-icon-20px-orange.png',
id: 'user',
icon: {
id: 'marker-green',
legendName: 'mapbox-marker-icon-green.svg',
mapName: 'mapbox-marker-icon-20px-green.png',
},
},
{
id: 'marker-green',
name: 'mapbox-marker-icon-20px-green.png',
id: 'group',
icon: {
id: 'marker-red',
legendName: 'mapbox-marker-icon-red.svg',
mapName: 'mapbox-marker-icon-20px-red.png',
},
},
{
id: 'event',
icon: {
id: 'marker-purple',
legendName: 'mapbox-marker-icon-purple.svg',
mapName: 'mapbox-marker-icon-20px-purple.png',
},
},
],
isImagesLoaded: false,
@ -137,7 +158,8 @@ export default {
this.markers.isImagesLoaded &&
this.currentUser &&
this.users &&
this.groups
this.groups &&
this.posts
)
},
styles() {
@ -236,17 +258,27 @@ export default {
// Copy coordinates array.
const coordinates = e.features[0].geometry.coordinates.slice()
const markerTypeLabel =
e.features[0].properties.type === 'group'
? this.$t('map.markerTypes.group')
: e.features[0].properties.type === 'user'
? this.$t('map.markerTypes.user')
: this.$t('map.markerTypes.theUser')
const markerProfileLinkTitle =
(e.features[0].properties.type === 'group' ? '&' : '@') + e.features[0].properties.slug
const markerProfileLink =
(e.features[0].properties.type === 'group' ? '/group' : '/profile') +
`/${e.features[0].properties.id}/${e.features[0].properties.slug}`
const markerTypeLabel = this.$t(`map.markerTypes.${e.features[0].properties.type}`)
const markerProfile = {
theUser: {
linkTitle: '@' + e.features[0].properties.slug,
link: `/profile/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
},
user: {
linkTitle: '@' + e.features[0].properties.slug,
link: `/profile/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
},
group: {
linkTitle: '&' + e.features[0].properties.slug,
link: `/group/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
},
event: {
linkTitle: e.features[0].properties.slug,
link: `/post/${e.features[0].properties.id}/${e.features[0].properties.slug}`,
},
}
const markerProfileLinkTitle = markerProfile[e.features[0].properties.type].linkTitle
const markerProfileLink = markerProfile[e.features[0].properties.type].link
let description = `
<div>
<div>
@ -258,11 +290,11 @@ export default {
</div>
`
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>
<div>
${e.features[0].properties.about}
${e.features[0].properties.description}
</div>`
: ''
@ -305,15 +337,18 @@ export default {
},
loadMarkersIconsAndAddMarkers() {
Promise.all(
this.markers.icons.map(
this.markers.types.map(
(marker) =>
new Promise((resolve, reject) => {
// our images have to be in the 'static/img/*' folder otherwise they are not reachable via URL
this.map.loadImage('img/mapbox/marker-icons/' + marker.name, (error, image) => {
if (error) throw error
this.map.addImage(marker.id, image)
resolve()
})
this.map.loadImage(
'img/mapbox/marker-icons/' + marker.icon.mapName,
(error, image) => {
if (error) throw error
this.map.addImage(marker.icon.id, image)
resolve()
},
)
}),
),
).then(() => {
@ -337,7 +372,7 @@ export default {
id: user.id,
slug: user.slug,
name: user.name,
about: user.about ? user.about : undefined,
description: user.about ? user.about : undefined,
},
geometry: {
type: 'Point',
@ -346,27 +381,6 @@ export default {
})
}
})
// add markers for "groups"
this.groups.forEach((group) => {
if (group.location) {
this.markers.geoJSON.push({
type: 'Feature',
properties: {
type: 'group',
iconName: 'marker-blue',
iconRotate: 0.0,
id: group.id,
slug: group.slug,
name: group.name,
about: group.about ? group.about : undefined,
},
geometry: {
type: 'Point',
coordinates: this.getCoordinates(group.location),
},
})
}
})
// add marker for "currentUser"
if (this.currentUserCoordinates) {
this.markers.geoJSON.push({
@ -378,7 +392,7 @@ export default {
id: this.currentUser.id,
slug: this.currentUser.slug,
name: this.currentUser.name,
about: this.currentUser.about ? this.currentUser.about : undefined,
description: this.currentUser.about ? this.currentUser.about : undefined,
},
geometry: {
type: 'Point',
@ -386,6 +400,48 @@ export default {
},
})
}
// add markers for "groups"
this.groups.forEach((group) => {
if (group.location) {
this.markers.geoJSON.push({
type: 'Feature',
properties: {
type: 'group',
iconName: 'marker-red',
iconRotate: 0.0,
id: group.id,
slug: group.slug,
name: group.name,
description: group.about ? group.about : undefined,
},
geometry: {
type: 'Point',
coordinates: this.getCoordinates(group.location),
},
})
}
})
// add markers for "posts", post type "Event" with location coordinates
this.posts.forEach((post) => {
if (post.postType.includes('Event') && post.eventLocation) {
this.markers.geoJSON.push({
type: 'Feature',
properties: {
type: 'event',
iconName: 'marker-purple',
iconRotate: 0.0,
id: post.id,
slug: post.slug,
name: post.title,
description: post.contentExcerpt,
},
geometry: {
type: 'Point',
coordinates: this.getCoordinates(post.eventLocation),
},
})
}
})
this.markers.isGeoJSON = true
}
@ -483,6 +539,24 @@ export default {
},
fetchPolicy: 'cache-and-network',
},
Post: {
query() {
return filterPosts(this.$i18n)
},
variables() {
return {
filter: {
postType_in: ['Event'],
eventStart_gte: new Date(),
// would be good to just query for events with defined "eventLocation". couldn't get it working
},
}
},
update({ Post }) {
this.posts = Post
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>

1381
yarn.lock

File diff suppressed because it is too large Load Diff