Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Open Deep Link modal #640

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions packages/vscode-extension/src/common/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export interface ProjectInterface {

resetAppPermissions(permissionType: AppPermissionType): Promise<void>;

getDeepLinksHistory(): Promise<string[]>;
openDeepLink(link: string): Promise<void>;

captureReplay(): Promise<RecordingData>;

dispatchTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down"): Promise<void>;
Expand Down
20 changes: 20 additions & 0 deletions packages/vscode-extension/src/devices/AndroidEmulatorDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,26 @@ export class AndroidEmulatorDevice extends DeviceBase {
return true; // Android will terminate the process if any of the permissions were granted prior to reset-permissions call
}

async sendDeepLink(link: string, build: BuildResult) {
if (build.platform !== DevicePlatform.Android) {
throw new Error("Invalid platform");
}

await exec(ADB_PATH, [
"-s",
this.serial!,
"shell",
"am",
"start",
"-W",
"-a",
"android.intent.action.VIEW",
"-d",
link,
build.packageName,
]);
}

makePreview(): Preview {
return new Preview(["android", "--id", this.serial!]);
}
Expand Down
1 change: 1 addition & 0 deletions packages/vscode-extension/src/devices/DeviceBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export abstract class DeviceBase implements Disposable {
appPermission: AppPermissionType,
buildResult: BuildResult
): Promise<boolean>;
abstract sendDeepLink(link: string, buildResult: BuildResult): Promise<void>;

async acquire() {
const acquired = await tryAcquiringLock(this.lockFilePath);
Expand Down
15 changes: 15 additions & 0 deletions packages/vscode-extension/src/devices/IosSimulatorDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,21 @@ export class IosSimulatorDevice extends DeviceBase {
return false;
}

async sendDeepLink(link: string, build: BuildResult) {
if (build.platform !== DevicePlatform.IOS) {
throw new Error("Invalid platform");
}

await exec("xcrun", [
"simctl",
"--set",
getOrCreateDeviceSet(this.deviceUDID),
"openurl",
this.deviceUDID,
link,
]);
}

makePreview(): Preview {
return new Preview([
"ios",
Expand Down
6 changes: 6 additions & 0 deletions packages/vscode-extension/src/project/deviceSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ export class DeviceSession implements Disposable {
return false;
}

public async sendDeepLink(link: string) {
if (this.maybeBuildResult) {
return this.device.sendDeepLink(link, this.maybeBuildResult);
}
}

public async captureReplay() {
return this.device.captureReplay();
}
Expand Down
19 changes: 19 additions & 0 deletions packages/vscode-extension/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import { PlatformBuildCache } from "../builders/PlatformBuildCache";
const DEVICE_SETTINGS_KEY = "device_settings_v4";
const LAST_SELECTED_DEVICE_KEY = "last_selected_device";
const PREVIEW_ZOOM_KEY = "preview_zoom";
const DEEP_LINKS_HISTORY_KEY = "deep_links_history";

const DEEP_LINKS_HISTORY_LIMIT = 50;

export class Project
implements Disposable, MetroDelegate, EventDelegate, DebugSessionDelegate, ProjectInterface
Expand Down Expand Up @@ -369,6 +372,22 @@ export class Project
}
}

async getDeepLinksHistory() {
return extensionContext.workspaceState.get<string[] | undefined>(DEEP_LINKS_HISTORY_KEY) ?? [];
}

async openDeepLink(link: string) {
const history = await this.getDeepLinksHistory();
if (history.length === 0 || link !== history[0]) {
extensionContext.workspaceState.update(
DEEP_LINKS_HISTORY_KEY,
[link, ...history.filter((s) => s !== link)].slice(0, DEEP_LINKS_HISTORY_LIMIT)
);
}

this.deviceSession?.sendDeepLink(link);
}

public async dispatchTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down") {
this.deviceSession?.sendTouches(touches, type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { useModal } from "../providers/ModalProvider";
import { DevicePlatform } from "../../common/DeviceManager";
import { KeybindingInfo } from "./shared/KeybindingInfo";
import { DeviceLocalizationView } from "../views/DeviceLocalizationView";
import { extensionContext } from "../../utilities/extensionContext";
import { OpenDeepLinkView } from "../views/OpenDeepLinkView";

const contentSizes = [
"xsmall",
Expand Down Expand Up @@ -172,6 +174,13 @@ function DeviceSettingsDropdown({ children, disabled }: DeviceSettingsDropdownPr
<Switch.Thumb className="switch-thumb" />
</Switch.Root>
</div>
<Label>Deep Links</Label>
<DropdownMenu.Item
className="dropdown-menu-item"
onSelect={() => openModal("Open Deep Link", <OpenDeepLinkView />)}>
<span className="codicon codicon-link" />
Open Deep Link
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
Expand Down
11 changes: 11 additions & 0 deletions packages/vscode-extension/src/webview/styles/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@
/* Inspector */
--inspect-area-background: rgba(56, 172, 221, 0.85);
--dimensions-box-background: #404040;

/* Open Deep Link */
--deep-link-history-query-match-color: #c19420;
}

/* Light theme */
Expand Down Expand Up @@ -282,6 +285,10 @@ body[data-vscode-theme-kind="vscode-light"] {
--swm-replay-len-select-background: var(--grey-light-60);
--swm-replay-len-select-hover-background: var(--navy-light-transparent);
--swm-replay-len-select-highlighted-background: var(--grey-light-80);

/* Open Deep Link */
--deep-link-history-border: var(--navy-light-10);
--deep-link-history-selected-item-background: var(--navy-light-10);
}

/* Dark theme */
Expand Down Expand Up @@ -435,4 +442,8 @@ body[data-vscode-theme-kind="vscode-dark"] {
--swm-replay-len-select-background: var(--background-dark-160);
--swm-replay-len-select-hover-background: var(--background-dark-120);
--swm-replay-len-select-highlighted-background: var(--background-dark-80);

/* Open Deep Link */
--deep-link-history-border: var(--background-dark-100);
--deep-link-history-selected-item-background: var(--background-dark-100);
}
72 changes: 72 additions & 0 deletions packages/vscode-extension/src/webview/views/OpenDeepLinkView.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.input-url {
width: 100%;
display: inline-flex;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
border-radius: 4px;
padding: 0 8px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1;
height: 26px;
color: var(--swm-default-text);
background-color: var(--swm-select-background);
box-shadow: var(--swm-select-shadow);
}

.search-bar-wrapper {
display: flex;
gap: 12px;
}

.search-icon {
margin-top: 4px;
}

.history-container {
max-height: 200px;
overflow: auto;
border: 2px solid var(--deep-link-history-border);
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: start;
}

.history-option {
width: calc(100% - 16px);
max-width: calc(100% - 16px);
padding: 8px;
cursor: pointer;
user-select: none;
}

.history-option span {
width: 100%;
word-wrap: break-word;
}

.empty-history {
background-color: var(--deep-link-history-selected-item-background);
width: 100%;
text-align: center;
padding: 8px 0;
font-style: italic;
color: var(--swm-disabled-text);
}

.url-highlighted {
color: var(--deep-link-history-query-match-color);
}

.selected-element {
background-color: var(--deep-link-history-selected-item-background);
}

.submit-button-container {
width: 100%;
margin-top: 12px;
display: flex;
justify-content: center;
}
147 changes: 147 additions & 0 deletions packages/vscode-extension/src/webview/views/OpenDeepLinkView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
import Label from "../components/shared/Label";
import "./OpenDeepLinkView.css";
import Button from "../components/shared/Button";
import { useProject } from "../providers/ProjectProvider";
import { useModal } from "../providers/ModalProvider";

type HistoryElement = {
id: number;
url: string;
};

export const OpenDeepLinkView = () => {
const { project } = useProject();
const { closeModal } = useModal();

const [history, setHistory] = useState<HistoryElement[]>([]);
const [displayedHistory, setDisplayedHistory] = useState<HistoryElement[]>([]);
const [url, setUrl] = useState<string>("");
const [query, setQuery] = useState<string>("");
const [selectedElement, setSelectedElement] = useState<number | undefined>(undefined);
const highlightedSegment = useRef<Map<number, [number, number]>>(new Map());

useEffect(() => {
(async () => {
const deepLinksHistory = await project.getDeepLinksHistory();
const historyWithIds = deepLinksHistory.map((element, index) => ({
id: index,
url: element,
}));
setHistory(historyWithIds);
setDisplayedHistory(historyWithIds);

if (deepLinksHistory.length > 0) {
setUrl(deepLinksHistory[0]);
setSelectedElement(0);
}
})();
}, []);

const updateUrl = (newUrl: string, selectedElementId?: number) => {
setUrl(newUrl);

if (selectedElementId !== undefined) {
setSelectedElement(selectedElementId);
} else {
setSelectedElement(undefined);
}
};

useEffect(() => {
if (!query) {
setDisplayedHistory(history);
highlightedSegment.current.clear();
return;
}

let newDisplayedHistory: HistoryElement[] = [];

history.forEach((historyElement) => {
const queryPosition = historyElement.url.indexOf(query);

if (queryPosition !== -1) {
newDisplayedHistory.push(historyElement);
highlightedSegment.current.set(historyElement.id, [
queryPosition,
queryPosition + query.length,
]);
}
});

setDisplayedHistory(newDisplayedHistory);
}, [query]);

const openDeepLink = async () => {
if (!url) {
return;
}

await project.openDeepLink(url);

closeModal();
};

const getHistoryUrlWithHighlight = (element: HistoryElement): ReactNode => {
const segment = highlightedSegment.current.get(element.id);
if (segment === undefined) {
return <span>{element.url}</span>;
} else {
const [left, right] = segment;
return (
<>
<span>
{element.url.substring(0, left)}
<span className="url-highlighted">{element.url.substring(left, right)}</span>
{element.url.substring(right)}
</span>
</>
);
}
};

return (
<>
<Label>URL</Label>
<input
className="input-url"
type="string"
value={url}
placeholder="Input Deep Link URL"
onChange={(e) => updateUrl(e.target.value)}
/>
<Label>History</Label>
<div className="search-bar-wrapper">
<span className="codicon codicon-search search-icon" />
<input
className="input-url"
type="string"
value={query}
placeholder="Type to search"
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<div className="history-container">
{displayedHistory.length > 0 ? (
displayedHistory.map((historyElement) => (
<div
key={`history-element-${historyElement.id}`}
className={`history-option ${
selectedElement === historyElement.id && "selected-element"
}`}
onClick={() => updateUrl(historyElement.url, historyElement.id)}>
{getHistoryUrlWithHighlight(historyElement)}
</div>
))
) : (
<div className="empty-history">no entries found</div>
)}
</div>
<div className="submit-button-container">
<Button className="submit-button" type="secondary" onClick={openDeepLink}>
Open
</Button>
</div>
</>
);
};
Loading