Notify of plugin updates and add update UX (#339)

This commit is contained in:
Gregory Schier
2026-01-02 10:03:08 -08:00
committed by GitHub
parent e751167dfc
commit 0146ee586f
20 changed files with 375 additions and 103 deletions

View File

@@ -5,7 +5,10 @@ import { jotaiStore } from '../lib/jotai';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
export const openSettings = createFastMutation<void, string, SettingsTab | null>({
// Allow tab with optional subtab (e.g., "plugins:installed")
type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}` | null;
export const openSettings = createFastMutation<void, string, SettingsTabWithSubtab>({
mutationKey: ['open_settings'],
mutationFn: async (tab) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
@@ -14,7 +17,7 @@ export const openSettings = createFastMutation<void, string, SettingsTab | null>
const location = router.buildLocation({
to: '/workspaces/$workspaceId/settings',
params: { workspaceId },
search: { tab: tab ?? undefined },
search: { tab: (tab ?? undefined) as SettingsTab | undefined },
});
await invokeCmd('cmd_new_child_window', {

View File

@@ -45,7 +45,9 @@ export type SettingsTab = (typeof tabs)[number];
export default function Settings({ hide }: Props) {
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
const [tab, setTab] = useState<string | undefined>(tabFromQuery);
// Parse tab and subtab (e.g., "plugins:installed")
const [mainTab, subtab] = tabFromQuery?.split(':') ?? [];
const [tab, setTab] = useState<string | undefined>(mainTab || tabFromQuery);
const settings = useAtomValue(settingsAtom);
const plugins = useAtomValue(pluginsAtom);
const licenseCheck = useLicense();
@@ -118,7 +120,7 @@ export default function Settings({ hide }: Props) {
<SettingsTheme />
</TabContent>
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 px-6 !py-4">
<SettingsPlugins />
<SettingsPlugins defaultSubtab={tab === TAB_PLUGINS ? subtab : undefined} />
</TabContent>
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4">
<SettingsProxy />

View File

@@ -43,14 +43,18 @@ function isPluginBundled(plugin: Plugin, vendoredPluginDir: string): boolean {
);
}
export function SettingsPlugins() {
interface SettingsPluginsProps {
defaultSubtab?: string;
}
export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
const [directory, setDirectory] = useState<string | null>(null);
const plugins = useAtomValue(pluginsAtom);
const bundledPlugins = plugins.filter((p) => isPluginBundled(p, appInfo.vendoredPluginDir));
const installedPlugins = plugins.filter((p) => !isPluginBundled(p, appInfo.vendoredPluginDir));
const createPlugin = useInstallPlugin();
const refreshPlugins = useRefreshPlugins();
const [tab, setTab] = useState<string>();
const [tab, setTab] = useState<string | undefined>(defaultSubtab);
return (
<div className="h-full">
<Tabs

View File

@@ -1,7 +1,13 @@
import { emit } from '@tauri-apps/api/event';
import { openUrl } from '@tauri-apps/plugin-opener';
import type { InternalEvent, ShowToastRequest } from '@yaakapp-internal/plugins';
import type { UpdateInfo, UpdateResponse, YaakNotification } from '@yaakapp-internal/tauri';
import { updateAllPlugins } from '@yaakapp-internal/plugins';
import type {
PluginUpdateNotification,
UpdateInfo,
UpdateResponse,
YaakNotification,
} from '@yaakapp-internal/tauri';
import { openSettings } from '../commands/openSettings';
import { Button } from '../components/core/Button';
import { ButtonInfiniteLoading } from '../components/core/ButtonInfiniteLoading';
@@ -44,92 +50,164 @@ export function initGlobalListeners() {
}
});
const UPDATE_TOAST_ID = 'update-info'; // Share the ID to replace the toast
listenToTauriEvent<string>('update_installed', async ({ payload: version }) => {
showToast({
id: UPDATE_TOAST_ID,
color: 'primary',
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Yaak {version} was installed</h2>
<p className="text-text-subtle text-sm">Start using the new version now?</p>
</VStack>
),
action: ({ hide }) => (
<ButtonInfiniteLoading
size="xs"
className="mr-auto min-w-[5rem]"
color="primary"
loadingChildren="Restarting..."
onClick={() => {
hide();
setTimeout(() => invokeCmd('cmd_restart', {}), 200);
}}
>
Relaunch Yaak
</ButtonInfiniteLoading>
),
});
console.log('Got update installed event', version);
showUpdateInstalledToast(version);
});
// Listen for update events
listenToTauriEvent<UpdateInfo>(
'update_available',
async ({ payload: { version, replyEventId, downloaded } }) => {
console.log('Received update available event', { replyEventId, version, downloaded });
jotaiStore.set(updateAvailableAtom, { version, downloaded });
// Acknowledge the event, so we don't time out and try the fallback update logic
await emit<UpdateResponse>(replyEventId, { type: 'ack' });
showToast({
id: UPDATE_TOAST_ID,
color: 'info',
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Yaak {version} is available</h2>
<p className="text-text-subtle text-sm">
{downloaded ? 'Do you want to install' : 'Download and install'} the update?
</p>
</VStack>
),
action: () => (
<HStack space={1.5}>
<ButtonInfiniteLoading
size="xs"
color="info"
className="min-w-[10rem]"
loadingChildren={downloaded ? 'Installing...' : 'Downloading...'}
onClick={async () => {
await emit<UpdateResponse>(replyEventId, { type: 'action', action: 'install' });
}}
>
{downloaded ? 'Install Now' : 'Download and Install'}
</ButtonInfiniteLoading>
<Button
size="xs"
color="info"
variant="border"
rightSlot={<Icon icon="external_link" />}
onClick={async () => {
await openUrl(`https://yaak.app/changelog/${version}`);
}}
>
What&apos;s New
</Button>
</HStack>
),
});
},
);
listenToTauriEvent<UpdateInfo>('update_available', async ({ payload }) => {
console.log('Got update available', payload);
showUpdateAvailableToast(payload);
});
listenToTauriEvent<YaakNotification>('notification', ({ payload }) => {
console.log('Got notification event', payload);
showNotificationToast(payload);
});
// Listen for plugin update events
listenToTauriEvent<PluginUpdateNotification>('plugin_updates_available', ({ payload }) => {
console.log('Got plugin updates event', payload);
showPluginUpdatesToast(payload);
});
}
function showUpdateInstalledToast(version: string) {
const UPDATE_TOAST_ID = 'update-info';
showToast({
id: UPDATE_TOAST_ID,
color: 'primary',
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Yaak {version} was installed</h2>
<p className="text-text-subtle text-sm">Start using the new version now?</p>
</VStack>
),
action: ({ hide }) => (
<ButtonInfiniteLoading
size="xs"
className="mr-auto min-w-[5rem]"
color="primary"
loadingChildren="Restarting..."
onClick={() => {
hide();
setTimeout(() => invokeCmd('cmd_restart', {}), 200);
}}
>
Relaunch Yaak
</ButtonInfiniteLoading>
),
});
}
async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
const UPDATE_TOAST_ID = 'update-info';
const { version, replyEventId, downloaded } = updateInfo;
jotaiStore.set(updateAvailableAtom, { version, downloaded });
// Acknowledge the event, so we don't time out and try the fallback update logic
await emit<UpdateResponse>(replyEventId, { type: 'ack' });
showToast({
id: UPDATE_TOAST_ID,
color: 'info',
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Yaak {version} is available</h2>
<p className="text-text-subtle text-sm">
{downloaded ? 'Do you want to install' : 'Download and install'} the update?
</p>
</VStack>
),
action: () => (
<HStack space={1.5}>
<ButtonInfiniteLoading
size="xs"
color="info"
className="min-w-[10rem]"
loadingChildren={downloaded ? 'Installing...' : 'Downloading...'}
onClick={async () => {
await emit<UpdateResponse>(replyEventId, { type: 'action', action: 'install' });
}}
>
{downloaded ? 'Install Now' : 'Download and Install'}
</ButtonInfiniteLoading>
<Button
size="xs"
color="info"
variant="border"
rightSlot={<Icon icon="external_link" />}
onClick={async () => {
await openUrl(`https://yaak.app/changelog/${version}`);
}}
>
What&apos;s New
</Button>
</HStack>
),
});
}
function showPluginUpdatesToast(updateInfo: PluginUpdateNotification) {
const PLUGIN_UPDATE_TOAST_ID = 'plugin-updates';
const count = updateInfo.updateCount;
const pluginNames = updateInfo.plugins.map((p: { name: string }) => p.name);
showToast({
id: PLUGIN_UPDATE_TOAST_ID,
color: 'info',
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">
{count === 1 ? '1 plugin update' : `${count} plugin updates`} available
</h2>
<p className="text-text-subtle text-sm">
{count === 1
? pluginNames[0]
: `${pluginNames.slice(0, 2).join(', ')}${count > 2 ? `, and ${count - 2} more` : ''}`}
</p>
</VStack>
),
action: ({ hide }) => (
<HStack space={1.5}>
<ButtonInfiniteLoading
size="xs"
color="info"
className="min-w-[5rem]"
loadingChildren="Updating..."
onClick={async () => {
const updated = await updateAllPlugins();
hide();
if (updated.length > 0) {
showToast({
color: 'success',
message: `Successfully updated ${updated.length} plugin${updated.length === 1 ? '' : 's'}`,
});
}
}}
>
Update All
</ButtonInfiniteLoading>
<Button
size="xs"
color="info"
variant="border"
onClick={() => {
hide();
openSettings.mutate('plugins:installed');
}}
>
View Updates
</Button>
</HStack>
),
});
}
function showNotificationToast(n: YaakNotification) {