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
36 changes: 31 additions & 5 deletions src/app/components/SyncConnectionStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react';
import classNames from 'classnames';
import { Box, config, Text } from 'folds';
import { AnimatePresence, motion, useReducedMotion, type Variants } from 'framer-motion';
Expand All @@ -10,6 +11,8 @@ const TITLEBAR_EASE_OUT_SOFT: [number, number, number, number] = [0.24, 0.72, 0.

type SyncConnectionStatusProps = {
status: TitlebarStatusView | null;
onClick?: () => void;
icon?: React.ReactNode;
};

export function getSyncConnectionStatusView(
Expand Down Expand Up @@ -157,7 +160,7 @@ export function SyncConnectionStatusBanner({ status }: SyncConnectionStatusProps
);
}

export function SyncConnectionStatusTitlebar({ status }: SyncConnectionStatusProps) {
export function SyncConnectionStatusTitlebar({ status, onClick, icon }: SyncConnectionStatusProps) {
const shouldReduceMotion = useReducedMotion();
const progress = status?.progress;
const pillVariants = shouldReduceMotion
Expand Down Expand Up @@ -233,34 +236,57 @@ export function SyncConnectionStatusTitlebar({ status }: SyncConnectionStatusPro
};

return (
<AnimatePresence mode="sync" initial={false}>
<AnimatePresence mode="popLayout" initial={false}>
{status && (
<motion.span
key={status.variant}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
className={classNames(
'tauri-titlebar-status__label',
status.variant === 'Success' && 'tauri-titlebar-status__label--success',
status.variant === 'Warning' && 'tauri-titlebar-status__label--warning',
status.variant === 'Critical' && 'tauri-titlebar-status__label--critical',
status.variant === 'Primary' && 'tauri-titlebar-status__label--primary'
)}
onClick={onClick}
onKeyDown={
onClick
? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}
: undefined
}
style={{
transformOrigin: 'center top',
position: 'relative',
overflow: 'hidden',
willChange: shouldReduceMotion ? 'opacity' : 'transform, opacity, clip-path',
cursor: onClick ? 'pointer' : undefined,
}}
variants={pillVariants}
initial="hidden"
animate="visible"
exit="exit"
>
<motion.span
className="tauri-titlebar-status__text"
style={{ willChange: 'opacity' }}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
willChange: 'opacity',
}}
variants={textVariants}
>
{status.text}
{icon && (
<span style={{ display: 'flex' }} aria-hidden>
{icon}
</span>
)}
<span className="tauri-titlebar-status__text">{status.text}</span>
</motion.span>
{progress !== undefined && (
<div
Expand Down
6 changes: 6 additions & 0 deletions src/app/components/app-shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { type as osType } from '@tauri-apps/plugin-os';
import { TauriFrontendReady } from '$components/tauri/TauriFrontendReady';
import { DesktopTitleBar } from '$components/tauri/DesktopTitleBar';
import { MacTitleBar } from '$components/tauri/MacTitleBar';
import { DesktopUpdater } from '$pages/client/DesktopUpdater';
import { WebUpdater } from '$pages/client/WebUpdater';
import { GlobalBannerRenderer } from '$components/global-banner/GlobalBannerRenderer';
import { Toast } from '$components/toast/Toast';
import type { ScreenSize } from '$hooks/useScreenSize';
import { ScreenSizeProvider } from '$hooks/useScreenSize';
Expand Down Expand Up @@ -88,6 +91,9 @@ function AppShellFrame({ children, portalContainer, onPortalContainerChange }: A
>
{titlebarKind === 'desktop' && <DesktopTitleBar />}
{titlebarKind === 'mac' && <MacTitleBar />}
<DesktopUpdater />
<WebUpdater />
<GlobalBannerRenderer />
<div
style={{
display: 'flex',
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/icons/phosphor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
DotsThreeIcon,
DotsThreeOutlineVerticalIcon,
DownloadIcon,
DownloadSimpleIcon,
EnvelopeSimpleIcon,
EyeIcon,
EyeSlashIcon,
Expand Down Expand Up @@ -343,6 +344,7 @@ export {
DotsThreeIcon as DotsThree,
DotsThreeOutlineVerticalIcon,
DownloadIcon as Download,
DownloadSimpleIcon as DownloadSimple,
EnvelopeSimpleIcon as EnvelopeSimple,
EyeIcon as Eye,
EyeSlashIcon as EyeSlash,
Expand Down
4 changes: 4 additions & 0 deletions src/app/components/tauri/DesktopTitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAtomValue } from 'jotai';
import { createLogger } from '$utils/debug';
import { titlebarStatusAtom } from '$state/titlebarStatus';
import { SyncConnectionStatusTitlebar } from '$components/SyncConnectionStatus';
import { DesktopUpdatePill } from './DesktopUpdatePill';
import {
hideSnapOverlay as hideSnapOverlayCommand,
showSnapOverlay as showSnapOverlayCommand,
Expand Down Expand Up @@ -271,6 +272,9 @@ export function DesktopTitleBar() {
<SyncConnectionStatusTitlebar status={titlebarStatus} />
</div>

<div className="tauri-titlebar__update">
<DesktopUpdatePill />
</div>
<div className="tauri-titlebar__controls">
<button
type="button"
Expand Down
34 changes: 34 additions & 0 deletions src/app/components/tauri/DesktopUpdatePill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useAtom, useAtomValue } from 'jotai';
import { hasCustomDesktopTitlebar } from '$utils/tauriTitlebar';
import { updatePhaseAtom, updateBannerVisibleAtom } from '$state/desktopUpdate';
import type { UpdatePhase } from '$state/desktopUpdate';
import type { TitlebarStatusView } from '$state/titlebarStatus';
import { SyncConnectionStatusTitlebar } from '$components/SyncConnectionStatus';
import { DownloadSimple } from '$components/icons/phosphor';

function phaseToStatusView(phase: UpdatePhase): TitlebarStatusView | null {
switch (phase.type) {
case 'downloading':
return { text: 'Update Downloading', variant: 'Warning', progress: phase.progress };
case 'ready':
return { text: 'Update Available', variant: 'Success' };
default:
return null;
}
}

export function DesktopUpdatePill() {
const phase = useAtomValue(updatePhaseAtom);
const [bannerVisible, setBannerVisible] = useAtom(updateBannerVisibleAtom);
const status = !bannerVisible ? phaseToStatusView(phase) : null;

if (!hasCustomDesktopTitlebar()) return null;

return (
<SyncConnectionStatusTitlebar
status={status}
onClick={() => setBannerVisible(true)}
icon={<DownloadSimple size={12} />}
/>
);
}
10 changes: 9 additions & 1 deletion src/app/components/tauri/MacTitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type as osType } from '@tauri-apps/plugin-os';
import { useAtomValue } from 'jotai';
import { titlebarStatusAtom } from '$state/titlebarStatus';
import { SyncConnectionStatusTitlebar } from '$components/SyncConnectionStatus';
import { DesktopUpdatePill } from './DesktopUpdatePill';

// Draggable strip that reserves space for the native traffic lights (floating
// over the content via the transparent titlebar) and hosts the sync pill.
Expand All @@ -13,10 +14,17 @@ export function MacTitleBar() {
if (!isMac) return null;

return (
<nav className="tauri-titlebar tauri-titlebar--mac" data-tauri-drag-region>
<nav
className="tauri-titlebar tauri-titlebar--mac"
data-tauri-drag-region
style={{ position: 'relative' }}
>
<div className="tauri-titlebar__status" data-tauri-drag-region>
<SyncConnectionStatusTitlebar status={titlebarStatus} />
</div>
<div className="tauri-titlebar__update">
<DesktopUpdatePill />
</div>
</nav>
);
}
93 changes: 92 additions & 1 deletion src/app/features/settings/about/About.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Box, Text, Scroll, Button, config, toRem, Spinner } from 'folds';
import { Code, Heart, menuIcon } from '$components/icons/phosphor';
import { PageContent } from '$components/page';
Expand All @@ -10,6 +10,15 @@ import { useMatrixClient } from '$hooks/useMatrixClient';
import { SequenceCardStyle } from '$features/settings/styles.css';
import { Method } from '$types/matrix-sdk';
import { useOpenBugReportModal } from '$state/hooks/bugReportModal';
import { isDesktopTauri } from '$utils/platform';
import {
updatePhaseAtom,
updateBannerVisibleAtom,
triggerUpdateCheckAtom,
desktopUpdateLastCheckedAtom,
} from '$state/desktopUpdate';
import dayjs from 'dayjs';
import { useAtomValue, useSetAtom } from 'jotai';
import { SettingsSectionPage } from '../SettingsSectionPage';

type VersionResult =
Expand Down Expand Up @@ -180,6 +189,60 @@ export function About({ requestBack, requestClose }: Readonly<AboutProps>) {
const devLabel = IS_RELEASE_TAG ? '' : '-dev';
const buildLabel = BUILD_HASH ? ` (${BUILD_HASH})` : '';
const openBugReport = useOpenBugReportModal();
const updatePhase = useAtomValue(updatePhaseAtom);
const setBannerVisible = useSetAtom(updateBannerVisibleAtom);
const triggerCheck = useSetAtom(triggerUpdateCheckAtom);
const lastChecked = useAtomValue(desktopUpdateLastCheckedAtom);
const [checking, setChecking] = useState(false);
const [resultText, setResultText] = useState<string | null>(null);
const resultTimerRef = useRef<number | null>(null);

const lastCheckedText = lastChecked
? `Last checked: ${dayjs(lastChecked).format('HH:mm')}`
: null;

const clearResultTimer = useCallback(() => {
if (resultTimerRef.current !== null) {
clearTimeout(resultTimerRef.current);
resultTimerRef.current = null;
}
}, []);

const showResult = useCallback(
(text: string) => {
setChecking(false);
setResultText(text);
clearResultTimer();
resultTimerRef.current = window.setTimeout(() => {
setResultText(null);
resultTimerRef.current = null;
}, 3000);
},
[clearResultTimer]
);

useEffect(
() => () => {
clearResultTimer();
},
[clearResultTimer]
);

useEffect(() => {
if (!checking) return;
if (updatePhase.type === 'ready') {
showResult(`Update ${updatePhase.version} ready!`);
setBannerVisible(true);
} else if (updatePhase.type === 'idle') {
showResult('Up to date');
}
}, [updatePhase, checking, showResult, setBannerVisible]);

const handleCheckForUpdates = useCallback(() => {
setChecking(true);
setResultText(null);
triggerCheck((n) => n + 1);
}, [triggerCheck]);

return (
<SettingsSectionPage title="About" requestBack={requestBack} requestClose={requestClose}>
Expand Down Expand Up @@ -236,6 +299,34 @@ export function About({ requestBack, requestClose }: Readonly<AboutProps>) {
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Options</Text>
{isDesktopTauri() && (
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Check for Updates"
focusId="check-for-updates"
description={`${resultText || 'Check for a new version.'}${lastCheckedText ? ` ${lastCheckedText}` : ''}`}
after={
<Button
onClick={handleCheckForUpdates}
variant="Secondary"
fill="Soft"
size="300"
radii="300"
outlined
disabled={checking}
before={checking ? <Spinner variant="Secondary" size="300" /> : undefined}
>
<Text size="B300">Check</Text>
</Button>
}
/>
</SequenceCard>
)}
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isTauri } from '@tauri-apps/api/core';
import { type as osType } from '@tauri-apps/plugin-os';

import { isDesktopTauri } from '$utils/platform';
export type NotificationPluginListener = {
unregister: () => Promise<void> | void;
};
Expand Down Expand Up @@ -65,10 +65,6 @@ export async function ensureTauriNotificationPermission(): Promise<boolean> {
return permissionPromise;
}

// Desktop webviews can't show web notifications (WKWebView lacks the API; the
// Linux CEF runtime never grants it), so desktop routes through the native plugin.
const DESKTOP_TAURI_OS = new Set(['linux', 'macos', 'windows']);
export const isDesktopTauri = (): boolean => isTauri() && DESKTOP_TAURI_OS.has(osType());
export const isIosTauri = (): boolean => isTauri() && osType() === 'ios';
// Platforms where OS notifications go through the native plugin instead of web APIs.
export const isNativeNotificationTauri = (): boolean => isDesktopTauri() || isIosTauri();
Expand Down Expand Up @@ -104,3 +100,5 @@ export async function sendNativeTauriNotification({
extra,
});
}

export { isDesktopTauri };
1 change: 1 addition & 0 deletions src/app/features/settings/settingsLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ const settingsLinkFocusIdsBySection: Record<SettingsSectionId, readonly string[]
],
about: [
'base-url',
'check-for-updates',
'clear-cache-and-reload',
'domain',
'federation-url',
Expand Down
2 changes: 1 addition & 1 deletion src/app/generated/tauri/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Auto-generated TypeScript bindings for Tauri commands
* Generated by tauri-typegen v0.5.0
* Generated at: 2026-07-22T13:55:36.079802+00:00
* Generated at: 2026-07-23T09:59:48.348722900+00:00
* Generator: none
*
* Do not edit manually - regenerate using: cargo tauri-typegen generate
Expand Down
4 changes: 3 additions & 1 deletion src/app/generated/tauri/events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Auto-generated TypeScript bindings for Tauri commands
* Generated by tauri-typegen v0.5.0
* Generated at: 2026-07-22T13:55:36.081813+00:00
* Generated at: 2026-07-23T09:59:48.349621200+00:00
* Generator: none
*
* Do not edit manually - regenerate using: cargo tauri-typegen generate
Expand All @@ -26,3 +26,5 @@ export async function onOpenSettings(
handler(event.payload);
});
}


2 changes: 1 addition & 1 deletion src/app/generated/tauri/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Auto-generated TypeScript bindings for Tauri commands
* Generated by tauri-typegen v0.5.0
* Generated at: 2026-07-22T13:55:36.082561+00:00
* Generated at: 2026-07-23T09:59:48.349994700+00:00
* Generator: none
*
* Do not edit manually - regenerate using: cargo tauri-typegen generate
Expand Down
3 changes: 2 additions & 1 deletion src/app/generated/tauri/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Auto-generated TypeScript bindings for Tauri commands
* Generated by tauri-typegen v0.5.0
* Generated at: 2026-07-22T13:55:36.075263+00:00
* Generated at: 2026-07-23T09:59:48.347422900+00:00
* Generator: none
*
* Do not edit manually - regenerate using: cargo tauri-typegen generate
Expand Down Expand Up @@ -145,3 +145,4 @@ export interface UploadWriteChunkParams {
chunk: string;
[key: string]: unknown;
}

Loading
Loading