mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
feat(webapp): track online status (#8312)
* client * backend * tests * also save awaySince timestamp * remove console.log
This commit is contained in:
parent
4f02060fd7
commit
01027e42cc
@ -462,6 +462,7 @@ export default shield(
|
||||
switchUserRole: isAdmin,
|
||||
markTeaserAsViewed: allow,
|
||||
saveCategorySettings: isAuthenticated,
|
||||
updateOnlineStatus: isAuthenticated,
|
||||
CreateRoom: isAuthenticated,
|
||||
CreateMessage: isAuthenticated,
|
||||
MarkMessagesAsSeen: isAuthenticated,
|
||||
|
||||
@ -54,6 +54,8 @@ export default {
|
||||
},
|
||||
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
|
||||
lastActiveAt: { type: 'string', isoDate: true },
|
||||
lastOnlineStatus: { type: 'string' },
|
||||
awaySince: { type: 'string', isoDate: true },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
|
||||
@ -63,6 +63,12 @@ const saveCategorySettings = gql`
|
||||
}
|
||||
`
|
||||
|
||||
const updateOnlineStatus = gql`
|
||||
mutation ($status: OnlineStatus!) {
|
||||
updateOnlineStatus(status: $status)
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
@ -722,3 +728,114 @@ describe('save category settings', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateOnlineStatus', () => {
|
||||
beforeEach(async () => {
|
||||
user = await Factory.build('user', {
|
||||
id: 'user',
|
||||
role: 'user',
|
||||
})
|
||||
variables = {
|
||||
status: 'online',
|
||||
}
|
||||
})
|
||||
|
||||
describe('not authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = undefined
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe('set online', () => {
|
||||
it('returns true and saves the user in the database as online', async () => {
|
||||
await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { updateOnlineStatus: true },
|
||||
}),
|
||||
)
|
||||
|
||||
const cypher = 'MATCH (u:User {id: $id}) RETURN u'
|
||||
const result = await neode.cypher(cypher, { id: authenticatedUser.id })
|
||||
const dbUser = neode.hydrateFirst(result, 'u', neode.model('User'))
|
||||
await expect(dbUser.toJson()).resolves.toMatchObject({
|
||||
lastOnlineStatus: 'online',
|
||||
})
|
||||
await expect(dbUser.toJson()).resolves.not.toMatchObject({
|
||||
awaySince: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('set away', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
status: 'away',
|
||||
}
|
||||
})
|
||||
|
||||
it('returns true and saves the user in the database as away', async () => {
|
||||
await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { updateOnlineStatus: true },
|
||||
}),
|
||||
)
|
||||
|
||||
const cypher = 'MATCH (u:User {id: $id}) RETURN u'
|
||||
const result = await neode.cypher(cypher, { id: authenticatedUser.id })
|
||||
const dbUser = neode.hydrateFirst(result, 'u', neode.model('User'))
|
||||
await expect(dbUser.toJson()).resolves.toMatchObject({
|
||||
lastOnlineStatus: 'away',
|
||||
awaySince: expect.any(String),
|
||||
})
|
||||
})
|
||||
|
||||
it('stores the timestamp of the first away call', async () => {
|
||||
await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { updateOnlineStatus: true },
|
||||
}),
|
||||
)
|
||||
|
||||
const cypher = 'MATCH (u:User {id: $id}) RETURN u'
|
||||
const result = await neode.cypher(cypher, { id: authenticatedUser.id })
|
||||
const dbUser = neode.hydrateFirst(result, 'u', neode.model('User'))
|
||||
await expect(dbUser.toJson()).resolves.toMatchObject({
|
||||
lastOnlineStatus: 'away',
|
||||
awaySince: expect.any(String),
|
||||
})
|
||||
|
||||
const awaySince = (await dbUser.toJson()).awaySince
|
||||
|
||||
await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { updateOnlineStatus: true },
|
||||
}),
|
||||
)
|
||||
|
||||
const result2 = await neode.cypher(cypher, { id: authenticatedUser.id })
|
||||
const dbUser2 = neode.hydrateFirst(result2, 'u', neode.model('User'))
|
||||
await expect(dbUser2.toJson()).resolves.toMatchObject({
|
||||
lastOnlineStatus: 'away',
|
||||
awaySince,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -314,6 +314,37 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
updateOnlineStatus: async (object, args, context, resolveInfo) => {
|
||||
const { status } = args
|
||||
const {
|
||||
user: { id },
|
||||
} = context
|
||||
|
||||
const CYPHER_AWAY = `
|
||||
MATCH (user:User {id: $id})
|
||||
WITH user,
|
||||
CASE user.lastOnlineStatus
|
||||
WHEN 'away' THEN user.awaySince
|
||||
ELSE toString(datetime())
|
||||
END AS awaySince
|
||||
SET user.awaySince = awaySince
|
||||
SET user.lastOnlineStatus = $status
|
||||
`
|
||||
const CYPHER_ONLINE = `
|
||||
MATCH (user:User {id: $id})
|
||||
SET user.awaySince = null
|
||||
SET user.lastOnlineStatus = $status
|
||||
`
|
||||
|
||||
// Last Online Time is saved as `lastActiveAt`
|
||||
const session = context.driver.session()
|
||||
await session.writeTransaction((transaction) => {
|
||||
// return transaction.run(status === 'away' ? CYPHER_AWAY : CYPHER_ONLINE, { id, status })
|
||||
return transaction.run(status === 'away' ? CYPHER_AWAY : CYPHER_ONLINE, { id, status })
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
User: {
|
||||
email: async (parent, params, context, resolveInfo) => {
|
||||
|
||||
4
backend/src/schema/types/enum/OnlineStatus.gql
Normal file
4
backend/src/schema/types/enum/OnlineStatus.gql
Normal file
@ -0,0 +1,4 @@
|
||||
enum OnlineStatus {
|
||||
online
|
||||
away
|
||||
}
|
||||
@ -224,6 +224,8 @@ type Mutation {
|
||||
switchUserRole(role: UserRole!, id: ID!): User
|
||||
|
||||
saveCategorySettings(activeCategories: [String]): Boolean
|
||||
|
||||
updateOnlineStatus(status: OnlineStatus!): Boolean!
|
||||
|
||||
requestPasswordReset(email: String!): Boolean!
|
||||
resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean!
|
||||
|
||||
7
webapp/graphql/updateOnlineStatus.js
Normal file
7
webapp/graphql/updateOnlineStatus.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const updateOnlineStatus = gql`
|
||||
mutation ($status: OnlineStatus!) {
|
||||
updateOnlineStatus(status: $status)
|
||||
}
|
||||
`
|
||||
@ -130,6 +130,7 @@ export default {
|
||||
{ src: '~/plugins/vue-observe-visibility.js', ssr: false },
|
||||
{ src: '~/plugins/v-mapbox.js', mode: 'client' },
|
||||
{ src: '~/plugins/vue-advanced-chat.js', mode: 'client' },
|
||||
{ src: '~/plugins/onlineStatus.js', mode: 'client' },
|
||||
],
|
||||
|
||||
router: {
|
||||
|
||||
27
webapp/plugins/onlineStatus.js
Normal file
27
webapp/plugins/onlineStatus.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { updateOnlineStatus as updateOnlineStatusMutation } from '~/graphql/updateOnlineStatus.js'
|
||||
|
||||
let _app = null
|
||||
|
||||
const updateOnlineStatus = async () => {
|
||||
if (!_app.store.getters['auth/isAuthenticated']) {
|
||||
return
|
||||
}
|
||||
|
||||
const status = document.hidden ? 'away' : 'online'
|
||||
|
||||
const client = _app.apolloProvider.defaultClient
|
||||
|
||||
await client.mutate({
|
||||
mutation: updateOnlineStatusMutation,
|
||||
variables: { status },
|
||||
})
|
||||
}
|
||||
|
||||
export default ({ app }) => {
|
||||
_app = app
|
||||
if (process.client) {
|
||||
window.addEventListener('visibilitychange', updateOnlineStatus)
|
||||
setInterval(updateOnlineStatus, 30000)
|
||||
updateOnlineStatus()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user