diff --git a/packages/app/src/components/workspace-hover-card.tsx b/packages/app/src/components/workspace-hover-card.tsx index 0a833dfc6..749cdb6fa 100644 --- a/packages/app/src/components/workspace-hover-card.tsx +++ b/packages/app/src/components/workspace-hover-card.tsx @@ -13,9 +13,11 @@ import { useTranslation } from "react-i18next"; import { FadeIn, FadeOut } from "react-native-reanimated"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { + Check, CircleCheck, CircleDot, CircleX, + Copy, ExternalLink, Folder, GitBranch, @@ -24,12 +26,14 @@ import { GitHubIcon } from "@/components/icons/github-icon"; import type { Theme } from "@/styles/theme"; import { DiffStat } from "@/components/diff-stat"; import { Pressable } from "react-native"; +import type { GestureResponderEvent } from "react-native"; import { Portal } from "@gorhom/portal"; import { useBottomSheetModalInternal } from "@gorhom/bottom-sheet"; import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list"; import type { PrHint } from "@/git/use-pr-status-query"; import { openExternalUrl } from "@/utils/open-external-url"; import { shortenPath } from "@/utils/shorten-path"; +import { copyToClipboard } from "@/utils/copy-to-clipboard"; import { PrBadge } from "@/components/sidebar-workspace-list"; import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone"; import { useIsCompactFormFactor } from "@/constants/layout"; @@ -276,29 +280,27 @@ function WorkspaceHoverCardContent({ frameStyle={frameStyle} > - + {workspace.name} {workspace.currentBranch ? ( - - - - {workspace.currentBranch} - - + ) : null} {cwdDisplay ? ( - - - - {cwdDisplay} - - + ) : null} {prHint || workspace.diffStat ? ( @@ -330,6 +332,8 @@ const ThemedGitHubIcon = withUnistyles(GitHubIcon); const ThemedCircleCheck = withUnistyles(CircleCheck); const ThemedCircleDot = withUnistyles(CircleDot); const ThemedCircleX = withUnistyles(CircleX); +const ThemedCopy = withUnistyles(Copy); +const ThemedCheck = withUnistyles(Check); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); @@ -337,6 +341,70 @@ const successColorMapping = (theme: Theme) => ({ color: theme.colors.statusSucce const warningColorMapping = (theme: Theme) => ({ color: theme.colors.statusWarning }); const dangerColorMapping = (theme: Theme) => ({ color: theme.colors.statusDanger }); +function CopyableInfoRow({ + icon: Icon, + value, + copyValue, + copyLabel, + testID, +}: { + icon: React.ComponentType>; + value: string; + copyValue: string; + copyLabel: string; + testID: string; +}) { + const [isHovered, setIsHovered] = useState(false); + const [copied, setCopied] = useState(false); + + const handlePressIn = useCallback((event: GestureResponderEvent) => { + event.stopPropagation(); + }, []); + + const handlePress = useCallback(() => { + void copyToClipboard(copyValue); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 2000); + }, [copyValue]); + + const handleHoverIn = useCallback(() => setIsHovered(true), []); + const handleHoverOut = useCallback(() => setIsHovered(false), []); + + let iconUniProps = foregroundMutedColorMapping; + if (copied || isHovered) { + iconUniProps = foregroundColorMapping; + } + const textStyle = copied || isHovered ? cardInfoTextHoveredCombined : styles.cardInfoText; + + return ( + + {(() => { + if (copied) { + return ; + } + if (isHovered) { + return ; + } + return ; + })()} + + {value} + + + ); +} + function getChecksSummaryCounts(checks: NonNullable) { return checks.reduce( (counts, check) => { @@ -501,6 +569,9 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, }, + cardInfoTextHovered: { + color: theme.colors.foreground, + }, separator: { height: 1, backgroundColor: theme.colors.border, @@ -557,3 +628,5 @@ const checksSummaryLabelHoveredCombined = [ styles.checksSummaryLabel, styles.checksSummaryLabelHovered, ]; + +const cardInfoTextHoveredCombined = [styles.cardInfoText, styles.cardInfoTextHovered]; diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 9e0a58043..7999db547 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -337,6 +337,9 @@ export const ar: TranslationResources = { }, hoverCard: { scriptsAccessibility: "البرامج النصية Workspace", + copyPath: "نسخ المسار", + copyBranchName: "نسخ اسم الفرع", + copied: "تم النسخ", }, fileExplorer: { sort: { diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 0282b2425..85e30d5b1 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -336,6 +336,9 @@ export const en = { }, hoverCard: { scriptsAccessibility: "Workspace scripts", + copyPath: "Copy path", + copyBranchName: "Copy branch name", + copied: "Copied", }, fileExplorer: { sort: { diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index f4978c73c..ce0672e5b 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -340,6 +340,9 @@ export const es: TranslationResources = { }, hoverCard: { scriptsAccessibility: "GuionesWorkspace", + copyPath: "Copiar ruta", + copyBranchName: "Copiar nombre de rama", + copied: "Copiado", }, fileExplorer: { sort: { diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index ff8c3383c..6966020db 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -340,6 +340,9 @@ export const fr: TranslationResources = { }, hoverCard: { scriptsAccessibility: "ScriptsWorkspace", + copyPath: "Copier le chemin", + copyBranchName: "Copier le nom de la branche", + copied: "Copié", }, fileExplorer: { sort: { diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index fcba2642d..e1c9b5d34 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -339,6 +339,9 @@ export const ru: TranslationResources = { }, hoverCard: { scriptsAccessibility: "Скрипты Workspace", + copyPath: "Копировать путь", + copyBranchName: "Копировать имя ветки", + copied: "Скопировано", }, fileExplorer: { sort: { diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 98be203d5..cecf2d1a0 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -337,6 +337,9 @@ export const zhCN: TranslationResources = { }, hoverCard: { scriptsAccessibility: "Workspace scripts", + copyPath: "复制路径", + copyBranchName: "复制分支名称", + copied: "已复制", }, fileExplorer: { sort: { diff --git a/packages/app/src/utils/copy-to-clipboard.ts b/packages/app/src/utils/copy-to-clipboard.ts new file mode 100644 index 000000000..2ee7f96e2 --- /dev/null +++ b/packages/app/src/utils/copy-to-clipboard.ts @@ -0,0 +1,5 @@ +import * as Clipboard from "expo-clipboard"; + +export async function copyToClipboard(text: string): Promise { + await Clipboard.setStringAsync(text); +}