Skip to content
Open
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
12 changes: 12 additions & 0 deletions src/app/AppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { isMac } from './system.utils.ts'

const APP_CONFIG_FILE_NAME = 'config.json'

type WindowBounds = {
x: number
y: number
width: number
height: number
}

// Memoize the path to the application config file
let APP_CONFIG_FILE_PATH: string | null = null

Expand Down Expand Up @@ -41,6 +48,11 @@ export type AppConfig = {
*/
lastAppVersion?: string

/**
* Last position and size of the main Talk window.
*/
talkWindowBounds?: WindowBounds

/**
* List of the accounts in the app in the {user}@{server} or {server} format,
* E.g. nextcloud.tld, alice@nextcloud.tld, bob@company.tld@company.tld/nextcloud.
Expand Down
116 changes: 106 additions & 10 deletions src/talk/talk.window.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

const { BrowserWindow } = require('electron')
const { BrowserWindow, screen } = require('electron')
const { setupTray } = require('../app/app.tray.js')
const { getAppConfig } = require('../app/AppConfig.ts')
const { getAppConfig, setAppConfig } = require('../app/AppConfig.ts')
const { applyContextMenu } = require('../app/applyContextMenu.js')
const { applyDownloadHandler } = require('../app/downloads.ts')
const { applyExternalLinkHandler } = require('../app/externalLinkHandlers.ts')
Expand All @@ -15,18 +15,115 @@ const { TITLE_BAR_HEIGHT } = require('../constants.js')
const { BUILD_CONFIG } = require('../shared/build.config.ts')
const { getBrowserWindowIcon } = require('../shared/icons.utils.js')

const SAVE_WINDOW_BOUNDS_DELAY = 500

/**
* Check whether a value can be used as Electron window bounds.
*
* @param {unknown} bounds - Candidate window bounds
* @return {boolean} Whether the value is a valid window bounds object
*/
function isValidWindowBounds(bounds) {
return typeof bounds === 'object'
&& bounds !== null
&& Number.isFinite(bounds.x)
&& Number.isFinite(bounds.y)
&& Number.isFinite(bounds.width)
&& Number.isFinite(bounds.height)
&& bounds.width > 0
&& bounds.height > 0
}

/**
* Check whether saved bounds still overlap with an available display.
*
* @param {import('electron').Rectangle} bounds - Window bounds to check
* @return {boolean} Whether the bounds are visible on any display
*/
function isVisibleOnAnyDisplay(bounds) {
return screen.getAllDisplays().some(({ workArea }) => (
bounds.x < workArea.x + workArea.width
&& bounds.x + bounds.width > workArea.x
&& bounds.y < workArea.y + workArea.height
&& bounds.y + bounds.height > workArea.y
))
}

/**
* Get saved Talk window bounds if they are still usable.
*
* @param {{ minWidth: number, minHeight: number }} minimumSize - Minimum Talk window size
* @return {import('electron').Rectangle | undefined} Restorable bounds, if available
*/
function getSavedTalkWindowBounds(minimumSize) {
const bounds = getAppConfig('talkWindowBounds')
if (!isValidWindowBounds(bounds)) {
return undefined
}

const savedBounds = {
...bounds,
width: Math.max(bounds.width, minimumSize.minWidth),
height: Math.max(bounds.height, minimumSize.minHeight),
}

return isVisibleOnAnyDisplay(savedBounds) ? savedBounds : undefined
}

/**
* Persist Talk window bounds as they change.
*
* @param {import('electron').BrowserWindow} window - Talk window to persist
*/
function applyTalkWindowBoundsPersistence(window) {
let saveWindowBoundsTimeout

/**
* Save the current normal bounds immediately.
*/
function saveWindowBounds() {
clearTimeout(saveWindowBoundsTimeout)
if (window.isDestroyed()) {
return
}

setAppConfig('talkWindowBounds', window.getNormalBounds())
}

/**
* Debounce repeated move and resize events.
*/
function scheduleSaveWindowBounds() {
clearTimeout(saveWindowBoundsTimeout)
saveWindowBoundsTimeout = setTimeout(saveWindowBounds, SAVE_WINDOW_BOUNDS_DELAY)
}

window.on('move', scheduleSaveWindowBounds)
window.on('moved', scheduleSaveWindowBounds)
window.on('resize', scheduleSaveWindowBounds)
window.on('resized', scheduleSaveWindowBounds)
window.on('close', saveWindowBounds)
window.on('closed', () => clearTimeout(saveWindowBoundsTimeout))
}

/**
* @return {import('electron').BrowserWindow}
*/
function createTalkWindow() {
const zoomFactor = getAppConfig('zoomFactor')
const minimumSize = getScaledWindowMinSize({
minWidth: 600,
minHeight: 400,
})
const defaultSize = getScaledWindowSize({
width: 1400,
height: 900,
})
const savedBounds = getSavedTalkWindowBounds(minimumSize)

const talkWindowOptions = {
title: buildTitle(),
...getScaledWindowMinSize({
minWidth: 600,
minHeight: 400,
}),
...minimumSize,
backgroundColor: BUILD_CONFIG.backgroundColor,
autoHideMenuBar: true,
webPreferences: {
Expand All @@ -49,10 +146,8 @@ function createTalkWindow() {

const window = new BrowserWindow({
...talkWindowOptions,
...getScaledWindowSize({
width: 1400,
height: 900,
}),
...defaultSize,
...savedBounds,
show: false,
})

Expand All @@ -75,6 +170,7 @@ function createTalkWindow() {
applyDownloadHandler(window)
applyWheelZoom(window)
applyZoom(window)
applyTalkWindowBoundsPersistence(window)

setupTray(window)

Expand Down