Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ resolver = "2"
[workspace.package]
edition = "2021"
license = "LGPL-3.0-or-later"
version = "0.21.0-beta.1"
version = "0.21.0-beta.2"
15 changes: 15 additions & 0 deletions apps/desktop/electron/main/ipc/market-ipc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IPC, ErrorCodes, trustedExtensionCommandId, trustedExtensionCommandName } from "@pi-desktop/shared";
import type { PluginReviewQuery } from "@pi-desktop/shared";
import { builtinPaletteItems } from "../builtin-commands";
import type { AgentExtensionBridge } from "../agent-extensions";
import type { HostProcess } from "../host-process";
Expand Down Expand Up @@ -103,6 +104,20 @@ export function registerMarketIpc({
return host.call<{ cancelled: boolean; id: string }>("market.cancelInstall", payload);
});

// Public reviews are read anonymously through the host: the renderer names
// plugin ids or one query, and host-core owns the trusted center origin,
// the bounds, and the error mapping. Nothing here authenticates or mutates.
handle(IPC.invoke.marketReviewSummaries, async (payload?: { pluginIds?: unknown }) => {
if (!host) throw new Error("host unavailable");
const pluginIds = Array.isArray(payload?.pluginIds) ? payload.pluginIds : [];
return host.call("market.reviewSummaries", { pluginIds });
});

handle(IPC.invoke.marketReviews, async (payload?: Partial<PluginReviewQuery>) => {
if (!host) throw new Error("host unavailable");
return host.call("market.reviews", { ...(payload ?? {}) });
});

handle(IPC.invoke.commandPaletteSearch, async (query: string) => {
const q = (query || "").toLowerCase();
const builtin = builtinPaletteItems();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pi-desktop/desktop",
"version": "0.21.0-beta.1",
"version": "0.21.0-beta.2",
"desktopName": "pi-desktop.desktop",
"private": true,
"description": "PI-Desktop Electron application",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/resources/models.dev/api.json

Large diffs are not rendered by default.

278 changes: 149 additions & 129 deletions apps/desktop/src/features/plugins/InstalledPluginsPanel.tsx

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion apps/desktop/src/features/plugins/MarketplacePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button, cx } from "../../components/ui";
import { IconCheck, IconSearch, IconShield } from "../../components/icons";
import { PermissionChips } from "./presentation";
import { PermissionChips, RatingChip } from "./presentation";
import {
formatDate,
monogram,
Expand All @@ -25,6 +25,7 @@ export function MarketplacePanel({
openDetail,
busyId,
queueInstall,
reviewSummaries,
}: PluginsPageModel) {
return (
<div
Expand Down Expand Up @@ -150,6 +151,10 @@ export function MarketplacePanel({
</span>
</>
) : null}
<RatingChip
summary={reviewSummaries[item.id]}
locale={locale}
/>
</span>
</span>
</span>
Expand Down
68 changes: 67 additions & 1 deletion apps/desktop/src/features/plugins/PluginDetailSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactNode } from "react";
import { useEffect, useRef, type ReactNode } from "react";
import { Button, TooltipButton, cx, portalOverlay } from "../../components/ui";
import { useBlockingOverlay } from "../../lib/blocking-overlay";
import {
Expand All @@ -22,6 +22,8 @@ import {
versionInstallable,
versionWithdrawn,
} from "./model";
import { ReviewSummaryBlock } from "./presentation";
import { PluginReviewsDrawer } from "./PluginReviewsDrawer";
import type { PluginsPageModel } from "./usePluginsPage";

function PluginDetailBlockingHost({ children }: { children: ReactNode }) {
Expand All @@ -46,7 +48,24 @@ export function PluginDetailSheet({
busyId,
queueInstall,
setSelectedVersion,
reviewSummaries,
reviewsStatus,
reviewsId,
openReviews,
closeReviews,
}: PluginsPageModel) {
const reviewSummary = detail ? reviewSummaries[detail.id] : undefined;
const reviewsOpen = selectedId !== null && reviewsId === selectedId;
/** The button that opened the drawer; focus returns here when it closes. */
const reviewsOpenerRef = useRef<HTMLButtonElement | null>(null);
const wasReviewsOpen = useRef(false);
useEffect(() => {
if (wasReviewsOpen.current && !reviewsOpen) {
reviewsOpenerRef.current?.focus();
}
wasReviewsOpen.current = reviewsOpen;
}, [reviewsOpen]);

return selectedId
? portalOverlay(
<PluginDetailBlockingHost>
Expand All @@ -56,12 +75,17 @@ export function PluginDetailSheet({
className="plugins-sheet-scrim"
aria-label={t("plugins.closeDetail")}
onClick={closeDetail}
inert={reviewsOpen}
/>
<aside
className="plugins-sheet"
role="dialog"
aria-modal="true"
aria-label={t("plugins.detailTitle")}
/* The review drawer is the top layer while it covers this sheet:
the sheet stays mounted (and keeps its scroll position) but is
not reachable by pointer or keyboard until the drawer leaves. */
inert={reviewsOpen}
>
<header className="plugins-sheet-head">
<span className="plugins-card-glyph is-large" aria-hidden>
Expand Down Expand Up @@ -279,6 +303,41 @@ export function PluginDetailSheet({
)}
</section>

{/* Ratings and reviews, between permissions and versions:
what other users said, then the release history it
belongs to. Reviews themselves open in the drawer that
covers this sheet. */}
<section className="plugins-sheet-section">
<h3 className="plugins-sheet-section-title">
{t("plugins.reviews.summaryTitle")}
</h3>
{reviewSummary && reviewSummary.ratingCount > 0 ? (
<ReviewSummaryBlock summary={reviewSummary} locale={locale} />
) : reviewSummary ? (
<p className="plugins-sheet-desc">{t("plugins.reviews.empty")}</p>
) : reviewsStatus === "loading" ? (
<p className="plugins-sheet-desc">{t("plugins.reviews.loading")}</p>
) : (
<p className="plugins-sheet-desc">
{t("plugins.reviews.unavailable")}
</p>
)}
{/* The entry stays reachable even when the summary read
failed: the drawer is where the list is retried, and
it always carries the disabled composer plus the
link out to the centre's own page. */}
<div className="plugins-sheet-reviews-action">
<Button
variant="secondary"
size="sm"
ref={reviewsOpenerRef}
onClick={() => openReviews(detail.id)}
>
{t("plugins.reviews.viewAll")}
</Button>
</div>
</section>

<section className="plugins-sheet-section">
<h3 className="plugins-sheet-section-title">
{t("plugins.versions")}
Expand Down Expand Up @@ -361,6 +420,13 @@ export function PluginDetailSheet({
</>
)}
</aside>
{reviewsOpen && detail ? (
<PluginReviewsDrawer
pluginId={detail.id}
pluginName={detail.name}
onClose={closeReviews}
/>
) : null}
</div>
</PluginDetailBlockingHost>,
)
Expand Down
Loading
Loading