Skip to content

Commit

Permalink
feat: add callbox window
Browse files Browse the repository at this point in the history
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
  • Loading branch information
ShGKme committed Dec 2, 2024
1 parent ab0dbce commit ffa5659
Show file tree
Hide file tree
Showing 15 changed files with 495 additions and 33 deletions.
2 changes: 2 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
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
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 { getScaledWindowSize } from '../app/utils.ts'
import { getBrowserWindowIcon } from '../shared/icons.utils.js'
import { isMac, isWindows } from '../app/system.utils.ts'

export type CallboxParams = {
/** Conversation token */
token: string
/** Conversation name */
name: string
/** Conversation type */
type: string
/** Conversation avatar URL */
avatar: string
}

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

const width = 400
const height = 15 * 1.5 * 2 + 34 + 12 * 3 // 2 text lines + buttons line + 3 paddings around

const window = new BrowserWindow({
...getScaledWindowSize({ width, height }),
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,
enablePreferredSizeMode: true,
},
})

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

params = {
token: 'ot4t92uh',
name: 'Grigorii K. Shartsev',
type: 'one2one',
avatar: 'https://nextcloud.dev.shgk.me/index.php/avatar/shgkme/64',
}

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

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

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

<script setup lang="ts">
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')!

/**
* Handle the call joined/missed outside the callbox
*/
waitCurrentUserHasJoinedCall(token, TIME_LIMIT).then((joined) => {
if (!joined) {
postBroadcast('notifications:missedCall', { name, token, 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" @keydown.esc="dismiss">
<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 {
--spacing-1: 4px;
--spacing-2: 8px;
--spacing-3: 12px;
--spacing-4: 16px;
--golden-ratio: 1.618;
--button-size: 34px;
--height: calc(var(--button-size) * 2 + var(--spacing-2) * 3);
--gap: var(--spacing-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) - 4px) 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')
66 changes: 66 additions & 0 deletions src/callbox/renderer/callbox.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { operations } from '@talk/src/types/openapi/openapi.ts'
import axios from '@nextcloud/axios'
import { getCurrentUser } from '@nextcloud/auth'
import { generateOcsUrl } from '@nextcloud/router'

type CallGetParticipantsForCall = operations['call-get-peers-for-call']['responses'][200]['content']['application/json']

/**
* Get participants of a call in a conversation
* @param token - Conversation token
*/
async function getCallParticipants(token: string) {
const response = await axios.get<CallGetParticipantsForCall>(generateOcsUrl('apps/spreed/api/v4/call/{token}', { token }))
return response.data.ocs.data
}

/**
* Check if the current user has joined the call
* @param token - Conversation token
*/
async function hasCurrentUserJoinedCall(token: string) {
const user = getCurrentUser()
if (!user) {
throw new Error('Cannot check whether current join the call - no current user found')
}
const participants = await getCallParticipants(token)
return participants.some((participant) => user.uid === participant.actorId)
}

/**
* Wait until the current user has joined the call
* @param token - Conversation token
* @param limit - The time limit in milliseconds to wait for the user to join the call, set to falsy to wait indefinitely
* @return Promise<boolean> - Resolved with boolean - true if the user has joined the call, false if the limit has been reached
*/
export function waitCurrentUserHasJoinedCall(token: string, limit?: number): Promise<boolean> {
const POLLING_INTERVAL = 2000

const start = Date.now()

return new Promise((resolve) => {
(async function doCheck() {
// Check for the limit
if (limit && Date.now() - start > limit) {
return resolve(false)
}

try {
// Check if the user has joined the call
if (await hasCurrentUserJoinedCall(token)) {
return resolve(true)
}
} catch (e) {
console.warn('Error while checking if the user has joined the call', e)
}

// Retry
setTimeout(doCheck, POLLING_INTERVAL)
})()
})
}
Loading

0 comments on commit ffa5659

Please sign in to comment.