Skip to content

Commit

Permalink
Merge pull request #868 from nextcloud/feat/call-popup
Browse files Browse the repository at this point in the history
feat: call notification popup (callbox)
  • Loading branch information
ShGKme authored Dec 3, 2024
2 parents 6a24502 + 06186e6 commit ccfac04
Show file tree
Hide file tree
Showing 19 changed files with 561 additions and 41 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module.exports = {
// Electron Forge build vars
AUTHENTICATION_WINDOW_WEBPACK_ENTRY: 'readonly',
AUTHENTICATION_WINDOW_PRELOAD_WEBPACK_ENTRY: 'readonly',
CALLBOX_WINDOW_PRELOAD_WEBPACK_ENTRY: 'readonly',
CALLBOX_WINDOW_WEBPACK_ENTRY: 'readonly',
TALK_WINDOW_WEBPACK_ENTRY: 'readonly',
TALK_WINDOW_PRELOAD_WEBPACK_ENTRY: 'readonly',
HELP_WINDOW_WEBPACK_ENTRY: 'readonly',
Expand Down Expand Up @@ -48,6 +50,8 @@ module.exports = {
// It works fine on server because by default in @nextcloud/eslint-config .vue files are not inspected via eslint-plugin-import thus import/extensions doesn't include .vue
// See: https://github.com/import-js/eslint-plugin-import/blob/main/README.md#importextensions
'import/default': 'off',
// import/namespace is not compatible with TS/JS mix
'import/namespace': 'off',
/**
* ESLint
*/
Expand Down
8 changes: 8 additions & 0 deletions forge.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,14 @@ module.exports = {
js: './src/preload.js',
},
},
{
name: 'callbox_window',
html: './src/callbox/renderer/callbox.html',
js: './src/callbox/renderer/callbox.main.ts',
preload: {
js: './src/preload.js',
},
},
],
},
},
Expand Down
8 changes: 8 additions & 0 deletions src/app/AppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ export type AppConfig = {
* - 'never': disable notification sound
*/
playSoundCall: 'always' | 'respect-dnd' | 'never'
/**
* Whether to show a popup when a call notification is received.
* - 'always': always show the popup
* - 'respect-dnd': show the popup only if user status is not Do-Not-Disturb [default]
* - 'never': disable the call popup
*/
enableCallbox: 'always' | 'respect-dnd' | 'never'
}

export type AppConfigKey = keyof AppConfig
Expand All @@ -102,6 +109,7 @@ const defaultAppConfig: AppConfig = {
zoomFactor: 1,
playSoundChat: 'respect-dnd',
playSoundCall: 'respect-dnd',
enableCallbox: 'respect-dnd',
}

/** Local cache of the config file mixed with the default values */
Expand Down
72 changes: 72 additions & 0 deletions src/callbox/callbox.window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { BrowserWindow, screen } from 'electron'
import { applyZoom, getScaledWindowSize } from '../app/utils.ts'
import { getBrowserWindowIcon } from '../shared/icons.utils.js'
import { isMac, isWindows } from '../app/system.utils.ts'
import { getAppConfig } from '../app/AppConfig.ts'

export type CallboxParams = {
/** Conversation token */
token: string
/** Conversation name */
name: string
/** Conversation type */
type: 'one2one' | 'group' | 'public'
/** Conversation avatar URL */
avatar: string
}

/**
*
* @param mainWindow
* @param notification
* @param params
*/
export function createCallboxWindow(params: CallboxParams) {
const { width: screenWidth } = screen.getPrimaryDisplay().workAreaSize

const { width, height } = getScaledWindowSize({
width: 400,
// 2 text lines + buttons line + 3 paddings around
height: 15 * 1.5 * 2 + 34 + 12 * 3,
})

const window = new BrowserWindow({
height,
width,
acceptFirstMouse: true,
alwaysOnTop: true,
autoHideMenuBar: true,
backgroundColor: '#00679E',
frame: false,
fullscreenable: false,
icon: getBrowserWindowIcon(),
maximizable: false,
minimizable: false,
resizable: false,
show: false,
skipTaskbar: true,
titleBarStyle: 'hidden',
type: isWindows ? 'toolbar' : isMac ? 'panel' : 'normal',
useContentSize: false,
webPreferences: {
preload: CALLBOX_WINDOW_PRELOAD_WEBPACK_ENTRY,
zoomFactor: getAppConfig('zoomFactor'),
},
})

window.removeMenu()
window.setPosition(Math.round((screenWidth - width) / 2), Math.round(height / 2))

applyZoom(window)

window.loadURL(CALLBOX_WINDOW_WEBPACK_ENTRY + '?' + new URLSearchParams(params))

window.once('ready-to-show', () => window.showInactive())

return window
}
177 changes: 177 additions & 0 deletions src/callbox/renderer/CallboxApp.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import IconClose from 'vue-material-design-icons/Close.vue'
import IconPhone from 'vue-material-design-icons/Phone.vue'
import IconPhoneHangup from 'vue-material-design-icons/PhoneHangup.vue'
import { t } from '@nextcloud/l10n'
import { waitCurrentUserHasJoinedCall } from './callbox.service.ts'
import { postBroadcast } from '../../shared/broadcast.service.ts'

const AVATAR_SIZE = 15 * 1.5 * 2 // 2 lines
const TIME_LIMIT = 45 * 1000

const params = new URLSearchParams(window.location.search)

const token = params.get('token')!
const name = params.get('name')!
const avatar = params.get('avatar')!
const type = params.get('type')! as 'one2one' | 'group' | 'public'

useEventListener('keydown', (event) => {
if (event.key === 'Escape') {
dismiss()
}
})

/**
* Handle the call joined/missed outside the callbox
*/
waitCurrentUserHasJoinedCall(token, TIME_LIMIT).then((joined) => {
if (!joined) {
postBroadcast('notifications:missedCall', { name, token, type, avatar })
}
window.close()
})

/**
* Join the call
*/
async function join() {
postBroadcast('talk:conversation:open', { token, directCall: true })
window.close()
}

/**
* Dismiss the call
*/
function dismiss() {
window.close()
}
</script>

<template>
<div class="callbox">
<div class="callbox__info">
<NcAvatar class="callbox__avatar"
:url="avatar"
:size="AVATAR_SIZE" />
<div class="callbox__text">
<div class="callbox__title">
{{ name }}
</div>
<div class="callbox__subtitle">
{{ t('talk_desktop', 'Incoming call') }}
</div>
</div>
<NcButton class="callbox__quit"
:aria-label="t('talk_desktop', 'Close')"
type="tertiary-no-background"
size="small"
@click="dismiss">
<template #icon>
<IconClose />
</template>
</NcButton>
</div>

<div class="callbox__actions">
<NcButton type="error"
alignment="center"
wide
@click="dismiss">
<template #icon>
<IconPhoneHangup :size="20" />
</template>
{{ t('talk_desktop', 'Dismiss') }}
</NcButton>
<NcButton type="success"
alignment="center"
wide
@click="join">
<template #icon>
<IconPhone :size="20" />
</template>
{{ t('talk_desktop', 'Join call') }}
</NcButton>
</div>
</div>
</template>

<style>
* {
box-sizing: border-box;
}
</style>

<style scoped>
.callbox {
--height: calc(var(--default-clickable-area) * 2 + var(--default-grid-baseline) * 2 * 3);
--gap: calc(var(--default-grid-baseline) * 3);
display: flex;
flex-direction: column;
gap: var(--gap);
padding: var(--gap);
height: 100vh;
user-select: none;
backdrop-filter: blur(12px);
background: rgba(0, 0, 0, .2);
color: var(--color-background-plain-text);
-webkit-app-region: drag;
}

.callbox button {
-webkit-app-region: no-drag;
}

.callbox__info {
display: flex;
align-items: center;
gap: var(--gap);
}

.callbox__avatar {
animation: pulse-shadow 2s infinite;
}

.callbox__text {
overflow: hidden;
flex: 1 0;
display: flex;
flex-direction: column;
}

.callbox__title {
overflow: hidden;
font-weight: bold;
text-wrap: nowrap;
text-overflow: ellipsis;
}

.callbox__quit {
color: var(--color-background-plain-text) !important;
align-self: flex-start;
}

.callbox__actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--gap);
}

@keyframes pulse-shadow {
0% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
}
100% {
box-shadow: 0 0 0 calc(var(--gap) - var(--default-grid-baseline)) rgba(255, 255, 255, 0);
}
}
</style>
14 changes: 14 additions & 0 deletions src/callbox/renderer/callbox.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"></div>
</body>
</html>
12 changes: 12 additions & 0 deletions src/callbox/renderer/callbox.main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import '../../shared/assets/global.styles.css'
import Vue from 'vue'
import { setupWebPage } from '../../shared/setupWebPage.js'
import CallboxApp from './CallboxApp.vue'

await setupWebPage()

new Vue(CallboxApp).$mount('#app')
Loading

0 comments on commit ccfac04

Please sign in to comment.