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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  

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

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

Binary file added apps/mobile/assets/link-icons/azure-devops.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile/assets/link-icons/bitbucket.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile/assets/link-icons/forgejo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile/assets/link-icons/gitcafe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/mobile/assets/link-icons/github.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile/assets/link-icons/gitlab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"config:prod": "APP_VARIANT=production expo config",
"profile:android:hermes": "mkdir -p profiles/review && react-native profile-hermes profiles/review",
"sync:chip-icons": "node scripts/sync-context-chip-icons.mjs",
"sync:link-icons": "node scripts/sync-link-icons.ts",
"sync:pierre-icons": "node scripts/sync-pierre-file-icons.mjs",
"measure-renders": "node scripts/measure-renders.mts",
"generate": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/generate-uniwind-themes.mts",
Expand Down Expand Up @@ -81,6 +82,7 @@
"@t3tools/provider-opencode": "workspace:*",
"@t3tools/provider-pi": "workspace:*",
"@t3tools/shared": "workspace:*",
"@t3tools/source-control-definitions": "workspace:*",
"@tabler/icons-react-native": "^3.44.0",
"clsx": "^2.1.1",
"diff": "8.0.3",
Expand Down
79 changes: 79 additions & 0 deletions apps/mobile/scripts/sync-link-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// The native markdown renderer draws a link's icon from a raster image, so each source control
// host's mark is rendered from its definition's artwork into a bundled PNG.
import * as NodeFS from "node:fs";
import * as NodeModule from "node:module";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";

import {
sourceControlClients,
type SourceControlIcon,
} from "@t3tools/client-runtime/source-control-clients";

const appDirectory = NodePath.resolve(
NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)),
"..",
);
const outputDirectory = NodePath.join(appDirectory, "assets/link-icons");
const generatedModulePath = NodePath.join(appDirectory, "src/lib/markdownLinkIcons.generated.ts");
const sharp = NodeModule.createRequire(
NodePath.resolve(appDirectory, "../../scripts/package.json"),
)("sharp");

const SIZE = 64;

function attribute(name: string, value: string | number | undefined): string {
return value === undefined ? "" : ` ${name}="${value}"`;
}

// A monochrome mark is tinted by the renderer, which only reads its alpha.
function svgMarkup(icon: SourceControlIcon): string {
const paint = (value: SourceControlIcon["paths"][number]["fill"]) =>
value === undefined
? "none"
: typeof value === "string"
? value.replace("currentColor", "#ffffff")
: `url(#${value.gradient})`;
const gradients = (icon.gradients ?? [])
.map(
(gradient) =>
`<linearGradient id="${gradient.id}" x1="${gradient.x1}" y1="${gradient.y1}" x2="${gradient.x2}" y2="${gradient.y2}" gradientUnits="userSpaceOnUse"${attribute("gradientTransform", gradient.transform)}>${gradient.stops
.map(
(stop) =>
`<stop offset="${stop.offset}" stop-color="${stop.color}"${attribute("stop-opacity", stop.opacity)}/>`,
)
.join("")}</linearGradient>`,
)
.join("");
const paths = icon.paths
.map(
(path) =>
`<path d="${path.d}" fill="${paint(path.fill)}"${attribute("fill-rule", path.fillRule)}${attribute("fill-opacity", path.fillOpacity)}${attribute("stroke", path.stroke === undefined ? undefined : paint(path.stroke))}${attribute("stroke-width", path.strokeWidth)}/>`,
)
.join("");
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="${icon.viewBox}" fill="none"><defs>${gradients}</defs>${paths}</svg>`;
}

NodeFS.rmSync(outputDirectory, { recursive: true, force: true });
NodeFS.mkdirSync(outputDirectory, { recursive: true });

const kinds: string[] = [];
for (const definition of sourceControlClients.definitions) {
if (definition.icon === null) continue;
await sharp(Buffer.from(svgMarkup(definition.icon)), { density: 72 * 4 })
.resize(SIZE, SIZE, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toFile(NodePath.join(outputDirectory, `${definition.kind}.png`));
kinds.push(definition.kind);
}

NodeFS.writeFileSync(
generatedModulePath,
`import type { ImageSourcePropType } from "react-native";\n\nexport const SOURCE_CONTROL_LINK_ICON_SOURCES: Readonly<Record<string, ImageSourcePropType>> = {\n${kinds
.toSorted()
.map(
(kind) =>
` ${/^\w+$/u.test(kind) ? kind : `"${kind}"`}: require("../../assets/link-icons/${kind}.png"),`,
)
.join("\n")}\n};\n`,
);
13 changes: 9 additions & 4 deletions apps/mobile/src/components/ComposerContextSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
ScopedThreadRef,
} from "@t3tools/contracts";
import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments";
import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
import { videoMimeType } from "@t3tools/shared/video";
import { Platform, Pressable, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
Expand All @@ -22,7 +23,7 @@ import {
type DraftComposerAttachment,
} from "../lib/composerImages";
import { contextImageGallery } from "../lib/contextImageGallery";
import { composerObjectLinkNoun } from "@t3tools/shared/composerObjectLinks";
import { composerObjectLinkNoun } from "@t3tools/source-control-definitions/composerObjectLinks";
import { tryOpenExternalUrl } from "../lib/openExternalUrl";
import {
contextRepositoryOutcomeLabel,
Expand Down Expand Up @@ -299,6 +300,10 @@ export function ComposerContextSheet(props: {
record?.kind === "review-comment" && "pullRequest" in record
? record.pullRequest?.url
: undefined;
const changeRequestTerm = (
(pullRequestUrl ? sourceControlClients.findByChangeRequestUrl(pullRequestUrl) : undefined) ??
sourceControlClients.get(undefined)
).changeRequest.singular;
const terminal = record?.kind === "terminal" && !("payload" in record) ? record : null;
return (
<Sheet title={terminal?.terminalLabel ?? props.label} onClose={props.onClose}>
Expand Down Expand Up @@ -835,12 +840,12 @@ export function ComposerContextSheet(props: {
onPress={() => {
props.onClose();
void openPullRequest(pullRequestUrl).catch(() =>
Dialog.alert("Could not open pull request", "Try again when connected."),
Dialog.alert(`Could not open ${changeRequestTerm}`, "Try again when connected."),
);
}}
className="rounded-xl bg-subtle p-4"
>
<Text className="text-foreground">Open pull request</Text>
<Text className="text-foreground">Open {changeRequestTerm}</Text>
</Pressable>
) : null}
{props.onOpenAttachment ? (
Expand All @@ -853,7 +858,7 @@ export function ComposerContextSheet(props: {
) : null}
{props.onOpenPullRequest && !pullRequestUrl ? (
<Button
label="Open pull request"
label={`Open ${changeRequestTerm}`}
tone="secondary"
onPress={props.onOpenPullRequest}
fullWidth
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/components/DiscordMessagePickerSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DiscordMessageSummary, EnvironmentId, ThreadId } from "@t3tools/co
import { useAtomValue } from "@effect/atom-react";
import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences";
import { discordThreadContextRecord } from "@t3tools/shared/integrationContextRecords";
import { discordThreadContextRecord } from "@t3tools/source-control-definitions/integrationContextRecords";
import * as Cause from "effect/Cause";
import * as Option from "effect/Option";
import { AsyncResult } from "effect/reactivity";
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/components/LinearIssuePickerSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useAtomValue } from "@effect/atom-react";
import { threadsForLinearIssue } from "@t3tools/client-runtime/state/linear";
import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences";
import { linearIssueContextRecord } from "@t3tools/shared/integrationContextRecords";
import { linearIssueContextRecord } from "@t3tools/source-control-definitions/integrationContextRecords";
import * as Cause from "effect/Cause";
import * as Option from "effect/Option";
import { AsyncResult } from "effect/reactivity";
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/components/SlackMessagePickerSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
import { useAtomValue } from "@effect/atom-react";
import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences";
import { slackThreadContextRecord } from "@t3tools/shared/integrationContextRecords";
import { slackThreadContextRecord } from "@t3tools/source-control-definitions/integrationContextRecords";
import * as Cause from "effect/Cause";
import * as Option from "effect/Option";
import { AsyncResult } from "effect/reactivity";
Expand Down
8 changes: 4 additions & 4 deletions apps/mobile/src/connection/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
ConnectionTransientError,
CredentialStore,
ProfileStore,
GitHubRoutingPermissions,
makeGitHubRoutingPermissions,
PullRequestRoutingPermissions,
makePullRequestRoutingPermissions,
} from "@t3tools/client-runtime/connection";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -42,7 +42,7 @@ function targetPersistenceError(
export const layer = Layer.effectContext(
Effect.gen(function* () {
const catalog = yield* CatalogStore.make();
const githubRoutingPermissions = yield* makeGitHubRoutingPermissions({
const githubRoutingPermissions = yield* makePullRequestRoutingPermissions({
read: catalog.read.pipe(Effect.map((document) => document.githubRoutingPermissions ?? [])),
write: (githubRoutingPermissions) =>
catalog.update((document) => ({ ...document, githubRoutingPermissions })),
Expand Down Expand Up @@ -150,7 +150,7 @@ export const layer = Layer.effectContext(
})),
});
return Context.make(Persistence.ConnectionTargetStore, targetStore).pipe(
Context.add(GitHubRoutingPermissions, githubRoutingPermissions),
Context.add(PullRequestRoutingPermissions, githubRoutingPermissions),
Context.add(Persistence.ConnectionRegistrationStore, registrationStore),
Context.add(ProfileStore.ConnectionProfileStore, profileStore),
Context.add(CredentialStore.ConnectionCredentialStore, credentialStore),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AndroidScreenHeader } from "../../components/ui/AndroidScreenHeader";
import { useRemoteConnections } from "../../state/use-remote-environment-registry";
import { LocalEnvironmentList } from "./LocalEnvironmentList";
import { GitHubRoutingSettings } from "./GitHubRoutingSettings";
import { PullRequestRoutingSettings } from "./PullRequestRoutingSettings";

export function ConnectionsRouteScreen() {
const {
Expand Down Expand Up @@ -69,7 +69,7 @@ export function ConnectionsRouteScreen() {
onSetEnabled={onSetEnvironmentEnabled}
onUpdate={onUpdateEnvironment}
/>
<GitHubRoutingSettings />
<PullRequestRoutingSettings />
</ScrollView>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import * as Dialog from "../../components/ui/Dialog";
import { useAtomValue } from "@effect/atom-react";
import {
connectionCatalogDisplayUrl,
gitHubRoutingConnectionKey,
gitHubRoutingPermissionFor,
type GitHubRoutingPermission,
pullRequestRoutingConnectionKey,
pullRequestRoutingPermissionFor,
type PullRequestRoutingPermission,
} from "@t3tools/client-runtime/connection";
import { useState } from "react";
import { Pressable, View } from "react-native";
Expand All @@ -17,11 +17,11 @@ import { useAtomCommand } from "../../state/use-atom-command";
import { SettingsSection } from "../settings/components/SettingsSection";

const options: ReadonlyArray<{
value: GitHubRoutingPermission;
value: PullRequestRoutingPermission;
label: string;
description: string;
}> = [
{ value: "off", label: "Off", description: "Keep GitHub requests on this environment." },
{ value: "off", label: "Off", description: "Keep source control requests on this environment." },
{
value: "read",
label: "Read PRs",
Expand All @@ -30,30 +30,32 @@ const options: ReadonlyArray<{
{
value: "read-write",
label: "Read and act",
description: "Actions may use broader GitHub permissions than the original environment.",
description:
"Actions may use broader source control permissions than the original environment.",
},
];

export function GitHubRoutingSettings() {
export function PullRequestRoutingSettings() {
const catalog = useAtomValue(environmentCatalog.catalogValueAtom);
const permissions = useAtomValue(environmentCatalog.githubRoutingPermissionsValueAtom);
const update = useAtomCommand(environmentCatalog.setGitHubRoutingPermission);
const permissions = useAtomValue(environmentCatalog.pullRequestRoutingPermissionsValueAtom);
const update = useAtomCommand(environmentCatalog.setPullRequestRoutingPermission);
const [expanded, setExpanded] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
if (catalog.entries.size === 0) return null;

return (
<View className="mt-5 gap-3">
<SettingsSection title="GitHub routing">
<SettingsSection title="Pull request routing">
{[...catalog.entries.values()].map((entry) => {
const environmentId = entry.target.environmentId;
const selected = gitHubRoutingPermissionFor(entry, permissions);
const disabled = !catalog.isReady || saving || gitHubRoutingConnectionKey(entry) === null;
const selected = pullRequestRoutingPermissionFor(entry, permissions);
const disabled =
!catalog.isReady || saving || pullRequestRoutingConnectionKey(entry) === null;
return (
<View key={environmentId}>
<Pressable
accessibilityRole="button"
accessibilityLabel={`${entry.target.label} GitHub routing`}
accessibilityLabel={`${entry.target.label} pull request routing`}
accessibilityState={{ expanded: expanded === environmentId }}
className="flex-row items-center gap-3 p-4"
onPress={() => setExpanded(expanded === environmentId ? null : environmentId)}
Expand Down Expand Up @@ -89,7 +91,7 @@ export function GitHubRoutingSettings() {
setSaving(false);
if (result._tag === "Failure")
Dialog.alert(
"Could not save GitHub routing permission",
"Could not save pull request routing permission",
"Try again before leaving this screen.",
);
});
Expand All @@ -102,8 +104,8 @@ export function GitHubRoutingSettings() {
})}
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
Choose environments you trust to share PR data and use each other's GitHub access. Enable
both environments. This applies only to this client.
Choose environments you trust to share PR data and use each other's source control access.
Enable both environments. This applies only to this client.
</Text>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StackActions, useNavigation } from "@react-navigation/native";
import { resolveThreadReferenceCopyTarget } from "@t3tools/shared/threadReference";
import { resolveThreadReferenceCopyTarget } from "@t3tools/client-runtime/thread-reference";
import {
createContext,
use,
Expand Down
9 changes: 5 additions & 4 deletions apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getNewProjectPublishTarget,
getNewProjectPublishTargets,
getNewProjectRepository,
defaultHostCloneUrl,
normalizePastedCloneUrl,
parseAddProjectRemoteSource,
resolveAddProjectPath,
Expand Down Expand Up @@ -474,7 +475,7 @@ function SourceControlRow(props: {
/>
) : (
<SourceControlIcon
kind={sourceControlClients.get(props.source).icon}
icon={sourceControlClients.get(props.source).icon}
size={Platform.OS === "android" ? 24 : 18}
colorClassName="accent-icon"
/>
Expand Down Expand Up @@ -821,7 +822,7 @@ export function AddProjectRepositoryScreen(props: {
autoCorrect={false}
placeholder={
source === "url"
? "https://github.com/org/repo.git"
? (defaultHostCloneUrl("org/repo") ?? "https://example.com/org/repo.git")
: addProjectRemoteSourcePathHint(source)
}
returnKeyType="next"
Expand Down Expand Up @@ -1112,7 +1113,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s
}
icon={
<SourceControlIcon
kind={publishTarget.definition.icon}
icon={publishTarget.definition.icon}
size={Platform.OS === "android" ? 24 : 18}
colorClassName="accent-icon"
/>
Expand All @@ -1136,7 +1137,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s
title={target.definition.label}
icon={
<SourceControlIcon
kind={target.definition.icon}
icon={target.definition.icon}
size={Platform.OS === "android" ? 24 : 17}
colorClassName="accent-icon"
/>
Expand Down
Loading