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
13 changes: 13 additions & 0 deletions src/api/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,10 @@ export interface components {
updated_at: string;
/** @description User biography with markdown, HTML, and plain text renderings */
bio?: components["schemas"]["MarkdownContent"] | null;
/** @description Denotes plan or other distinction */
badge: components["schemas"]["UserBadge"] | null;
/** @description User's subscription tier */
tier: components["schemas"]["UserTier"];
counts: components["schemas"]["UserCounts"];
/** @description HATEOAS links for navigation */
_links: components["schemas"]["Links"];
Expand Down Expand Up @@ -1043,6 +1047,15 @@ export interface components {
*/
users: number;
};
/**
* @description Denotes plan or other distinction:
* - `staff`: Are.na staff member
* - `investor`: Investor
* - `supporter`: Supporter subscriber
* - `premium`: Premium subscriber
* @enum {string}
*/
UserBadge: "staff" | "investor" | "supporter" | "premium";
/**
* @description User subscription tier:
* - `guest`: Unauthenticated user
Expand Down
1 change: 1 addition & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type ConnectionSort = Schemas["ConnectionSort"];
export type ChannelContentSort = Schemas["ChannelContentSort"];
export type ContentSort = Schemas["ContentSort"];
export type ConnectionFilter = Schemas["ConnectionFilter"];
export type UserTier = Schemas["UserTier"];

export type PresignedFile = Schemas["PresignedFile"];
export type PaginationMeta = Schemas["PaginationMeta"];
Expand Down
19 changes: 17 additions & 2 deletions src/commands/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { BlockItem } from "../components/BlockItem";
import { Spinner } from "../components/Spinner";
import { useCommand } from "../hooks/use-command";
import { plural } from "../lib/format";
import { openUrl } from "../lib/open";

const BILLING_URL = "https://www.are.na/billing";

interface Props {
query: string;
Expand Down Expand Up @@ -69,11 +72,23 @@ export function SearchCommand({
if (loading) return <Spinner label={`Searching "${query}"`} />;

if (error) {
const isPremiumError = error.includes("Forbidden");

if (isPremiumError) {
openUrl(BILLING_URL);
}

return (
<Box flexDirection="column">
<Text color="red">✕ {error}</Text>
{error.includes("Forbidden") && (
<Text dimColor> Search requires an Are.na Premium subscription</Text>
{isPremiumError && (
<>
<Text dimColor>
{" "}
Search requires an Are.na Premium subscription
</Text>
<Text dimColor> Opening {BILLING_URL}</Text>
</>
)}
</Box>
);
Expand Down
20 changes: 17 additions & 3 deletions src/commands/whoami.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Box, Text } from "ink";
import { arenaApiBaseUrl, client, getData } from "../api/client";
import { Spinner } from "../components/Spinner";
import { useCommand } from "../hooks/use-command";
import { plural } from "../lib/format";
import { formatTier, plural } from "../lib/format";

export function WhoamiCommand() {
const { data, error, loading } = useCommand(() =>
Expand All @@ -15,16 +15,30 @@ export function WhoamiCommand() {

const stats = [
plural(data.counts.channels, "channel"),

`${data.counts.following.toLocaleString()} following`,
plural(data.counts.followers, "follower"),
]
.filter(Boolean)
.join(" · ");

const tierLabel = formatTier(data.tier);
const tierColor =
data.tier === "premium"
? "green"
: data.tier === "supporter"
? "cyan"
: undefined;

return (
<Box flexDirection="column">
<Text bold>{data.name}</Text>
<Box>
<Text bold>{data.name}</Text>
{tierColor ? (
<Text color={tierColor}> [{tierLabel}]</Text>
) : (
<Text dimColor> [{tierLabel}]</Text>
)}
</Box>
<Text dimColor>@{data.slug}</Text>
{stats && <Text dimColor>{stats}</Text>}
<Text dimColor>API: {arenaApiBaseUrl}</Text>
Expand Down
48 changes: 42 additions & 6 deletions src/components/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Box, Text } from "ink";
import { Box, Text, useInput } from "ink";
import { useState } from "react";
import useSWR from "swr";
import { ArenaError, client, getData } from "../api/client";
import type {
Expand All @@ -16,7 +17,9 @@ import { accentColor, mutedColor } from "../lib/theme";
import { BlockItem } from "./BlockItem";
import { useSessionPaletteActive } from "./SessionPaletteContext";
import { Panel, ScreenFrame } from "./ScreenChrome";
import { ScreenEmpty, ScreenError, ScreenLoading } from "./ScreenStates";
import { ScreenEmpty, ScreenLoading } from "./ScreenStates";

const BILLING_URL = "https://www.are.na/billing";

type SearchNavigateView =
| { kind: "channel"; slug: string }
Expand Down Expand Up @@ -133,10 +136,18 @@ export function SearchResults({

if (error) {
const isPermission = error instanceof ArenaError && error.status === 403;
const message = isPermission
? "Search requires Are.na Premium"
: error.message;
return <ScreenError message={message} />;

if (isPermission) {
return <SearchPremiumGate onBack={onBack} />;
}

return (
<ScreenFrame title="Search">
<Box paddingX={1}>
<Text color="red">✕ {error.message}</Text>
</Box>
</ScreenFrame>
);
}

if (items.length === 0) {
Expand Down Expand Up @@ -189,3 +200,28 @@ export function SearchResults({
</ScreenFrame>
);
}

function SearchPremiumGate({ onBack }: { onBack: () => void }) {
const paletteActive = useSessionPaletteActive();
const [opened, setOpened] = useState(false);

useInput((input, key) => {
if (paletteActive) return;
if (input === "q" || key.escape) return onBack();
if (input === "o" && !opened) {
openUrl(BILLING_URL);
setOpened(true);
}
});

return (
<ScreenFrame title="Search">
<Box flexDirection="column" paddingX={1}>
<Text color="red">Search requires an Are.na Premium subscription</Text>
<Text dimColor>
{opened ? `Opened ${BILLING_URL}` : "Press o to open billing page"}
</Text>
</Box>
</ScreenFrame>
);
}
3 changes: 2 additions & 1 deletion src/components/WhoamiScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { User } from "../api/types";
import { plural } from "../lib/format";
import { formatTier, plural } from "../lib/format";
import { EntityProfileScreen } from "./EntityProfileScreen";

export function WhoamiScreen({ me, onBack }: { me: User; onBack: () => void }) {
Expand All @@ -8,6 +8,7 @@ export function WhoamiScreen({ me, onBack }: { me: User; onBack: () => void }) {
name={me.name}
slug={me.slug}
statsLine={`${plural(me.counts.channels, "channel")} · ${me.counts.following.toLocaleString()} following · ${plural(me.counts.followers, "follower")}`}
metaLine={formatTier(me.tier)}
browserUrl={`https://www.are.na/${me.slug}`}
onBack={onBack}
/>
Expand Down
17 changes: 16 additions & 1 deletion src/lib/format.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Followable } from "../api/types";
import type { Followable, UserTier } from "../api/types";

export function timeAgo(dateStr: string): string {
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
Expand Down Expand Up @@ -66,6 +66,21 @@ export function formatFollowable(item: Followable): string {
}
}

const TIER_LABELS: Record<UserTier, string> = {
premium: "Premium",
supporter: "Supporter",
free: "Free",
guest: "Guest",
};

export function formatTier(tier: UserTier): string {
return TIER_LABELS[tier] ?? tier;
}

export function isPremium(tier: UserTier): boolean {
return tier === "premium" || tier === "supporter";
}

export function formatFileSize(bytes?: number | null): string | null {
if (!bytes || bytes <= 0) return null;
if (bytes < 1024) return `${bytes} B`;
Expand Down
Loading