Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(notifications): disable sounds on DND #674

Merged
merged 4 commits into from
Jun 10, 2024
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
85 changes: 85 additions & 0 deletions src/shared/initialState.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Escape unicode characters in string with \\u{code}
* @param {string} str - source string
* @return {string} - string with escaped unicode characters
*/
const escapeUnicode = (str) => str.split('').map(char => {
const codePoint = char.codePointAt(0)
if (codePoint <= 0x7F) {
return char
} else if (codePoint <= 0xFFFF) {
return '\\u' + codePoint.toString(16).padStart(4, '0')
} else {
return '\\u{' + codePoint.toString(16) + '}'
}
}).join('')

/**
* Find or create and add to body if not exists a container for initial state input elements
* @return {HTMLTemplateElement} - the container template element
*/
function findOrCreateInitialStateContainer() {
let container = document.getElementById('initial-state')

if (!container) {
// <template id="initial-state"></template>
container = document.createElement('template')
container.id = 'initial-state'
document.body.prepend(container)
}

return container
}

/**
* Find or create an input element for initial state data
* @param {string} app - application name
* @param {string} key - state key
* @param {HTMLElement} [container] - the container element with all items
* @return {HTMLInputElement} - the input element
*/
function findOrCreateInitialStateElement(app, key, container = document.body) {
let input = container.querySelector(`#initial-state-${app}-${key}`)
if (!input) {
// <input id="initial-state-{app}-{key}" type="hidden" />
input = document.createElement('input')
input.id = `initial-state-${app}-${key}`
input.type = 'hidden'
container.appendChild(input)
}
return input
}

/**
* Set initial state data for app
* @param {string} app - application name
* @param {string} key - state key
* @param {any} data - serializable state data
* @param {HTMLElement} [container] - the container element with all items
*/
export function setInitialState(app, key, data, container) {
const input = findOrCreateInitialStateElement(app, key, container)
input.value = btoa(escapeUnicode(JSON.stringify(data)))
}

/**
* Initialize initial state data for all apps and keys from initial object
*
* @param {Record<string,Record<string,any>>} initialState - initial state data
*/
export function setupInitialState(initialState) {
const container = findOrCreateInitialStateContainer()

for (const [app, appInitialState] of Object.entries(initialState)) {
for (const [key, data] of Object.entries(appInitialState)) {
if (data !== undefined) {
setInitialState(app, key, data, container)
}
}
}
}
50 changes: 3 additions & 47 deletions src/shared/setupWebPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import axios from '@nextcloud/axios'
import { applyBodyThemeAttrs } from './theme.utils.js'
import { appData } from '../app/AppData.js'
import { initGlobals } from './globals/globals.js'
import { setupInitialState } from './initialState.service.js'

/**
* @param {string} lang - language code, TS type: `${lang}_${countryCode}`|`${lang}`
Expand Down Expand Up @@ -139,7 +140,7 @@ function getInitialStateFromCapabilities(capabilities, userMetadata) {
circles_enabled: false, // TODO: Missed in Capabilities. Is it a problem?
guests_accounts_enabled: true, // TODO: Missed in Capabilities. It is a problem
read_status_privacy: capabilities?.spreed?.config?.chat?.['read-privacy'],
play_sounds: true, // TODO: Missed in Capabilities. Is it a problem?
play_sounds: true, // Consider playing sound enabled by default on desktop until we have settings
attachment_folder: capabilities?.spreed?.config?.attachments?.folder,
attachment_folder_free_space: userMetadata?.quota?.free ?? 0, // TODO: Is User's Quota free equal to attachment_folder_free_space
enable_matterbridge: false, // TODO: Missed in Capabilities. Is it a problem?
Expand Down Expand Up @@ -176,7 +177,6 @@ function getInitialStateFromCapabilities(capabilities, userMetadata) {
sound_notification: true, // TODO
},
}

}

/**
Expand All @@ -185,51 +185,7 @@ function getInitialStateFromCapabilities(capabilities, userMetadata) {
*/
export function applyInitialState() {
const initialState = getInitialStateFromCapabilities(appData.capabilities, appData.userMetadata)

const findOrCreateInitialStateContainer = () => {
const container = document.getElementById('initial-state')
if (container) {
return container
}

const newContainer = document.createElement('template')
newContainer.id = 'initial-state'
document.body.prepend(newContainer)
return newContainer
}

const container = findOrCreateInitialStateContainer()

const createInitialStateItem = (app, key, data) => {
let input = document.querySelector(`#initial-state-${app}-${key}`)
if (!input) {
input = document.createElement('input')
input.id = `initial-state-${app}-${key}`
input.type = 'hidden'
container.appendChild(input)
}
const escapeUnicode = (str) => str.split('').map(char => {
const codePoint = char.codePointAt(0)
if (codePoint <= 0x7F) {
return char
} else if (codePoint <= 0xFFFF) {
return '\\u' + codePoint.toString(16).padStart(4, '0')
} else {
return '\\u{' + codePoint.toString(16) + '}'
}
}).join('')

console.log({ app, key, data, json: JSON.stringify(data) })
input.value = btoa(escapeUnicode(JSON.stringify(data)))
}

for (const [app, appInitialState] of Object.entries(initialState)) {
for (const [key, data] of Object.entries(appInitialState)) {
if (data !== undefined) {
createInitialStateItem(app, key, data)
}
}
}
setupInitialState(initialState)
}

/**
Expand Down
11 changes: 8 additions & 3 deletions src/talk/renderer/UserStatus/userStatus.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,14 @@ const restorePredefinedStatuses = () => JSON.parse(localStorage.getItem('TalkDes

export const useUserStatusStore = defineStore('userStatus', () => {
/** @type {import('vue').Ref<import('./userStatus.types.ts').UserStatus|null>} */
const userStatus = ref(restoreUserStatus())
const userStatus = ref(null)

/** @type {import('vue').Ref<import('./userStatus.types.ts').PredefinedUserStatus[]|null>} */
const predefinedStatuses = ref(restorePredefinedStatuses())

/** @type {import('vue').Ref<null|object>} */
const backupStatus = ref(null)

watch(userStatus, (newUserStatus) => cacheUserStatus(newUserStatus), { deep: true })

const emitUserStatusUpdated = () => emit('user_status:status.updated', {
status: userStatus.value.status,
message: userStatus.value.message,
Expand Down Expand Up @@ -93,6 +91,13 @@ export const useUserStatusStore = defineStore('userStatus', () => {
}
}

const cachedStatus = restoreUserStatus()
if (cachedStatus) {
setUserStatus(cachedStatus, true)
}

watch(userStatus, (newUserStatus) => cacheUserStatus(newUserStatus), { deep: true })

const initPromise = (async () => {
await updateUserStatusWithHeartbeat(false, true)

Expand Down
21 changes: 21 additions & 0 deletions src/talk/renderer/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import { loadServerCss } from '../../shared/resource.utils.js'
import { appData } from '../../app/AppData.js'
import { getCapabilities } from '../../shared/ocs.service.js'
import { setInitialState } from '../../shared/initialState.service.js'
import { subscribe } from '@nextcloud/event-bus'
import { getCurrentUser } from '@nextcloud/auth'

/**
* @return {Promise<void>}
Expand Down Expand Up @@ -156,3 +159,21 @@ export function initTalkHashIntegration(talkInstance) {
},
})
}

/**
* Enable or disable the sound for notifications and calls on DND user status change
*/
export function initPlaySoundManagementOnUserStatus() {
subscribe('user_status:status.updated', (userStatus) => {
if (userStatus.userId !== getCurrentUser().uid) {
return
}
// TODO: add setting to define the default value for playSound
const playSoundDefault = true
// Disable if DND
const playSound = userStatus.status === 'dnd' ? false : playSoundDefault
// Notification's sound in the Notifications app is controlled via initial state only
setInitialState('notifications', 'sound_notification', playSound)
setInitialState('notifications', 'sound_talk', playSound)
})
}
2 changes: 2 additions & 0 deletions src/talk/renderer/notifications/notifications.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export function createNotificationStore() {
body: notification.message,
icon: notification.icon,
tag: notification.notificationId,
// We have a custom sound
silent: true,
})
n.addEventListener('click', () => {
const event = {
Expand Down
8 changes: 7 additions & 1 deletion src/talk/renderer/talk.main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import '@talk/css/icons.css'
import './assets/styles.css'

import 'regenerator-runtime' // TODO: Why isn't it added on bundling
import { initLocalStyles, initServerStyles, initTalkHashIntegration } from './init.js'
import {
initLocalStyles,
initPlaySoundManagementOnUserStatus,
initServerStyles,
initTalkHashIntegration,
} from './init.js'
import { setupWebPage } from '../../shared/setupWebPage.js'
import { createViewer } from './Viewer/Viewer.js'
import { createDesktopApp } from './desktop.app.js'
Expand All @@ -21,6 +26,7 @@ await setupWebPage()

await initServerStyles()
await initLocalStyles()
initPlaySoundManagementOnUserStatus()

createDesktopApp()

Expand Down
Loading