Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apps/user_status/src/UserStatus.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { emit } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'
import { cleanup, render } from '@testing-library/vue'
import { createPinia } from 'pinia'
import { afterEach, describe, expect, it, vi } from 'vitest'
import UserStatus from './UserStatus.vue'
import { useUserStatusStore } from './store/userStatus.ts'

vi.mock('@nextcloud/auth')
vi.mock('@nextcloud/initial-state', () => ({ loadState: vi.fn() }))

describe('UserStatus', () => {
afterEach(cleanup)

it('shows the status from the initial state', async () => {
vi.mocked(loadState).mockReturnValue({
status: 'away',
statusIsUserDefined: true,
message: 'In a meeting',
icon: '📅',
clearAt: null,
messageIsPredefined: false,
messageId: null,
})

const component = render(UserStatus, {
props: { inline: true },
global: { plugins: [createPinia()] },
})

expect(await component.findByRole('button', { name: '📅 In a meeting' })).toBeTruthy()
})

it('updates the shared store from status events of the current user', async () => {
vi.mocked(loadState).mockReturnValue({ status: 'online', statusIsUserDefined: true, message: null, icon: null })

const pinia = createPinia()
const component = render(UserStatus, {
props: { inline: true },
global: { plugins: [pinia] },
})
expect(await component.findByRole('button', { name: 'Online' })).toBeTruthy()

emit('user_status:status.updated', { userId: 'other', status: 'dnd', message: 'Other user', icon: null })
emit('user_status:status.updated', { userId: 'test', status: 'dnd', message: null, icon: null })

expect(await component.findByRole('button', { name: 'Do not disturb' })).toBeTruthy()
expect(useUserStatusStore(pinia).status).toBe('dnd')
})
})
10 changes: 5 additions & 5 deletions apps/user_status/src/UserStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ export default {

/**
* Loads the current user's status from initial state
* and stores it in Vuex
* and stores it in the user status store
*/
mounted() {
this.$store.dispatch('loadStatusFromInitialState')
this.userStatusStore.loadStatusFromInitialState()

if (OC.config.session_keepalive) {
this.stopHeartbeat = startHeartbeat((isAway) => this._backgroundHeartbeat(isAway))
Expand Down Expand Up @@ -127,9 +127,9 @@ export default {
try {
const status = await sendHeartbeat(isAway)
if (status?.userId) {
this.$store.dispatch('setStatusFromHeartbeat', status)
this.userStatusStore.loadStatusFromServer(status)
} else {
await this.$store.dispatch('reFetchStatusFromServer')
await this.userStatusStore.reFetchStatusFromServer()
}
} catch (error) {
logger.debug('Failed sending heartbeat, got: ' + error.response?.status)
Expand All @@ -138,7 +138,7 @@ export default {

handleUserStatusUpdated(state) {
if (getCurrentUser()?.uid === state.userId) {
this.$store.dispatch('setStatusFromObject', {
this.userStatusStore.loadStatusFromServer({
status: state.status,
icon: state.icon,
message: state.message,
Expand Down
39 changes: 39 additions & 0 deletions apps/user_status/src/components/PredefinedStatusesList.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { cleanup, render } from '@testing-library/vue'
import { createPinia } from 'pinia'
import { afterEach, describe, expect, it, vi } from 'vitest'
import PredefinedStatusesList from './PredefinedStatusesList.vue'
import { fetchAllPredefinedStatuses } from '../services/predefinedStatusService.js'
import { useUserStatusStore } from '../store/userStatus.ts'

vi.mock('../services/predefinedStatusService.js', () => ({
fetchAllPredefinedStatuses: vi.fn(),
}))

describe('PredefinedStatusesList', () => {
afterEach(cleanup)

it('loads the predefined statuses and marks the current one as selected', async () => {
vi.mocked(fetchAllPredefinedStatuses).mockResolvedValue([
{ id: 'meeting', icon: '📅', message: 'In a meeting', clearAt: null },
{ id: 'sick', icon: '🤒', message: 'Out sick', clearAt: null },
])

const pinia = createPinia()
useUserStatusStore(pinia).loadStatusFromServer({ messageId: 'sick' })

const component = render(PredefinedStatusesList, {
global: { plugins: [pinia] },
})

const list = await component.findByRole('list', { name: 'Predefined statuses' })
expect(list.textContent).toContain('In a meeting')
expect(list.textContent).toContain('Out sick')
expect(fetchAllPredefinedStatuses).toHaveBeenCalledOnce()
expect(component.getByRole('radio', { checked: true, name: /Out sick/ })).toBeTruthy()
})
})
15 changes: 7 additions & 8 deletions apps/user_status/src/components/PredefinedStatusesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@

<script>
import { t } from '@nextcloud/l10n'
import { mapGetters, mapState } from 'vuex'
import { mapActions, mapState } from 'pinia'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import PredefinedStatus from './PredefinedStatus.vue'
import { usePredefinedStatusesStore } from '../store/predefinedStatuses.ts'
import { useUserStatusStore } from '../store/userStatus.ts'

export default {
name: 'PredefinedStatusesList',
Expand All @@ -47,12 +49,8 @@ export default {
},

computed: {
...mapState({
predefinedStatuses: (state) => state.predefinedStatuses.predefinedStatuses,
messageId: (state) => state.userStatus.messageId,
}),

...mapGetters(['statusesHaveLoaded']),
...mapState(usePredefinedStatusesStore, ['predefinedStatuses', 'statusesHaveLoaded']),
...mapState(useUserStatusStore, ['messageId']),
},

watch: {
Expand All @@ -69,10 +67,11 @@ export default {
* when this component is mounted
*/
created() {
this.$store.dispatch('loadAllPredefinedStatuses')
this.loadAllPredefinedStatuses()
},

methods: {
...mapActions(usePredefinedStatusesStore, ['loadAllPredefinedStatuses']),
t,

/**
Expand Down
56 changes: 19 additions & 37 deletions apps/user_status/src/components/SetStatusModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
import { showError } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { mapState, mapStores } from 'pinia'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcModal from '@nextcloud/vue/components/NcModal'
import ClearAtSelect from './ClearAtSelect.vue'
Expand All @@ -100,6 +101,8 @@ import PreviousStatus from './PreviousStatus.vue'
import { logger } from '../logger.ts'
import OnlineStatusMixin from '../mixins/OnlineStatusMixin.js'
import { getAllStatusOptions } from '../services/statusOptionsService.js'
import { useUserBackupStatusStore } from '../store/userBackupStatus.ts'
import { useUserStatusStore } from '../store/userStatus.ts'

export default {
name: 'SetStatusModal',
Expand Down Expand Up @@ -141,28 +144,23 @@ export default {
},

computed: {
messageId() {
return this.$store.state.userStatus.messageId
},

icon() {
return this.$store.state.userStatus.icon
},
...mapStores(useUserBackupStatusStore),
...mapState(useUserStatusStore, ['messageId', 'icon']),

message() {
return this.$store.state.userStatus.message || ''
return this.userStatusStore.message || ''
},

hasBackupStatus() {
return this.messageId && (this.backupIcon || this.backupMessage)
},

backupIcon() {
return this.$store.state.userBackupStatus.icon || ''
return this.userBackupStatusStore.icon || ''
},

backupMessage() {
return this.$store.state.userBackupStatus.message || ''
return this.userBackupStatusStore.message || ''
},

absencePageUrl() {
Expand Down Expand Up @@ -209,13 +207,13 @@ export default {
* Loads the current status when a user opens dialog
*/
mounted() {
this.$store.dispatch('fetchBackupFromServer')
this.userBackupStatusStore.fetchBackupFromServer()

this.predefinedMessageId = this.$store.state.userStatus.messageId
if (this.$store.state.userStatus.clearAt !== null) {
this.predefinedMessageId = this.userStatusStore.messageId
if (this.userStatusStore.clearAt !== null) {
this.clearAt = {
type: '_time',
time: this.$store.state.userStatus.clearAt,
time: this.userStatusStore.clearAt,
}
}
},
Expand All @@ -237,11 +235,7 @@ export default {
*/
setIcon(icon) {
this.predefinedMessageId = null
this.$store.dispatch('setCustomMessage', {
message: this.message,
icon,
clearAt: this.clearAt,
})
this.userStatusStore.setCustomMessage(this.message, icon, this.clearAt)
this.$nextTick(() => {
this.$refs.customMessageInput.focus()
})
Expand Down Expand Up @@ -274,10 +268,7 @@ export default {
selectPredefinedMessage(status) {
this.predefinedMessageId = status.id
this.clearAt = status.clearAt
this.$store.dispatch('setPredefinedMessage', {
messageId: status.id,
clearAt: status.clearAt,
})
this.userStatusStore.setPredefinedMessage(status.id, status.clearAt)
},

/**
Expand All @@ -294,16 +285,9 @@ export default {
this.isSavingStatus = true

if (this.predefinedMessageId === null) {
await this.$store.dispatch('setCustomMessage', {
message: this.editedMessage,
icon: this.icon,
clearAt: this.clearAt,
})
await this.userStatusStore.setCustomMessage(this.editedMessage, this.icon, this.clearAt)
} else {
this.$store.dispatch('setPredefinedMessage', {
messageId: this.predefinedMessageId,
clearAt: this.clearAt,
})
await this.userStatusStore.setPredefinedMessage(this.predefinedMessageId, this.clearAt)
}
} catch (err) {
showError(t('user_status', 'There was an error saving the status'))
Expand All @@ -324,7 +308,7 @@ export default {
try {
this.isSavingStatus = true

await this.$store.dispatch('clearMessage')
await this.userStatusStore.clearMessage()
} catch (err) {
showError(t('user_status', 'There was an error clearing the status'))
logger.debug(err)
Expand All @@ -345,9 +329,7 @@ export default {
try {
this.isSavingStatus = true

await this.$store.dispatch('revertBackupFromServer', {
messageId: this.messageId,
})
await this.userBackupStatusStore.revertBackupFromServer(this.messageId)
} catch (err) {
showError(t('user_status', 'There was an error reverting the status'))
logger.debug(err)
Expand All @@ -356,7 +338,7 @@ export default {
}

this.isSavingStatus = false
this.predefinedMessageId = this.$store.state.userStatus?.messageId
this.predefinedMessageId = this.userStatusStore.messageId
},
},
}
Expand Down
7 changes: 4 additions & 3 deletions apps/user_status/src/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
*/

import { subscribe } from '@nextcloud/event-bus'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import UserStatus from './UserStatus.vue'
import store from './store/index.js'

import './user-status-icons.css'

const pinia = createPinia()
const mountPoint = document.getElementById('user_status-menu-entry')

/**
Expand All @@ -26,7 +27,7 @@ function mountMenuEntry() {
mountPoint.replaceWith(transparentMountPoint)

createApp(UserStatus)
.use(store)
.use(pinia)
.mount(transparentMountPoint)
}

Expand All @@ -46,7 +47,7 @@ document.addEventListener('DOMContentLoaded', function() {
createApp(UserStatus, {
inline: true,
})
.use(store)
.use(pinia)
.mount(el)
})
})
16 changes: 9 additions & 7 deletions apps/user_status/src/mixins/OnlineStatusMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@

import { showError } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import { mapState } from 'vuex'
import { mapState, mapStores } from 'pinia'
import { logger } from '../logger.ts'
import { useUserStatusStore } from '../store/userStatus.ts'

export default {
computed: {
...mapState({
statusType: (state) => state.userStatus.status,
statusIsUserDefined: (state) => state.userStatus.statusIsUserDefined,
customIcon: (state) => state.userStatus.icon,
customMessage: (state) => state.userStatus.message,
...mapStores(useUserStatusStore),
...mapState(useUserStatusStore, {
statusType: 'status',
statusIsUserDefined: 'statusIsUserDefined',
customIcon: 'icon',
customMessage: 'message',
}),

/**
Expand Down Expand Up @@ -65,7 +67,7 @@ export default {
*/
async changeStatus(statusType) {
try {
await this.$store.dispatch('setStatus', { statusType })
await this.userStatusStore.setStatus(statusType)
} catch (err) {
showError(t('user_status', 'There was an error saving the new status'))
logger.debug(err)
Expand Down
2 changes: 1 addition & 1 deletion apps/user_status/src/services/predefinedStatusService.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { generateOcsUrl } from '@nextcloud/router'
/**
* Fetches all predefined statuses from the server
*
* @return {Promise<void>}
* @return {Promise<object[]>}
*/
async function fetchAllPredefinedStatuses() {
const url = generateOcsUrl('apps/user_status/api/v1/predefined_statuses?format=json')
Expand Down
Loading
Loading