Skip to content

feat: Loom importer extension #366

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

Open
wants to merge 7 commits 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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ target
*.sln
*.sw?
.turbo
pnpm-lock.yaml
.zed
.output
.vinxi
Expand Down
5 changes: 5 additions & 0 deletions apps/loom-importer-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
19 changes: 19 additions & 0 deletions apps/loom-importer-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Cap Loom Importer

This is a Chrome extension that allows you to import your Loom videos into Cap.

## Structure

```
├── src
│ ├── background.ts # Background script for handling auth
│ ├── content_scripts
│ │ └── main.tsx # Import UI injected on Loom's website
│ ├── popup
│ └── popup.tsx # Popup for the extension (shown when the extension is clicked)
└── vite.config.ts
```

## Development

Go to chrome://extensions/ and click "Load unpacked" and select the `dist` folder.
14 changes: 14 additions & 0 deletions apps/loom-importer-extension/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- Do not move this file's location, it must be in the root of the project -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cap Loom Importer</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/content_scripts/main.tsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions apps/loom-importer-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@cap/loom-importer-extension",
"version": "0.0.0",
"scripts": {
"dev": "dotenv -e ../../.env -- vite",
"build": "dotenv -e ../../.env -- tsc && vite build",
"watch": "dotenv -e ../../.env -- vite build --watch",
"type-check": "tsc --noEmit",
"dev:extension": "echo '⚡ Building in watch mode. Load the extension from the dist folder in Chrome extensions page (chrome://extensions/)' && pnpm watch",
"preview": "vite preview"
},
"dependencies": {
"@cap/ui": "workspace:^",
"@cap/ui-solid": "workspace:^",
"js-confetti": "^0.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-frame-component": "^5.2.1",
"vite-tsconfig-paths": "^3.3.17",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/chrome": "^0.0.176",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^1.0.7",
"hot-reload-extension-vite": "^1.0.13",
"tailwindcss": "^3.4.16",
"typescript": "^4.4.4",
"vite": "^2.7.2"
}
}
12 changes: 12 additions & 0 deletions apps/loom-importer-extension/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!-- Do not move this file's location, it must be in the root of the project -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Cap Loom Importer Popup</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/popup/popup.tsx"></script>
</body>
</html>
6 changes: 6 additions & 0 deletions apps/loom-importer-extension/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions apps/loom-importer-extension/public/assets/js/initializeUI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
(async () => {
const app = document.createElement("div");
app.id = "root";
document.body.append(app);

const src = chrome?.runtime?.getURL("/react/main.js");
await import(src);
})();
45 changes: 45 additions & 0 deletions apps/loom-importer-extension/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"manifest_version": 3,
"name": "Cap Loom Importer",
"description": "Import your Loom data to Cap",
"version": "1.0.0",
"icons": {
"16": "assets/images/icons/icon16.png",
"48": "assets/images/icons/icon48.png",
"128": "assets/images/icons/icon128.png"
},
"content_scripts": [
{
"matches": ["https://www.loom.com/*"],
"js": ["/assets/js/initializeUI.js"],
"run_at": "document_end",
"all_frames": true
}
],
"background": {
"service_worker": "react/background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html"
},
"web_accessible_resources": [
{
"resources": [
"/react/main.js",
"/react/vendor.js",
"/react/main.css",
"/react/jsx-runtime.js",
"/react/client.js",
"/react/urls.js"
],
"matches": ["https://www.loom.com/*"]
}
],
"permissions": ["cookies", "storage", "tabs", "scripting"],
"host_permissions": [
"https://cap.so/*",
"http://localhost:3000/*",
"https://www.loom.com/*"
]
}
130 changes: 130 additions & 0 deletions apps/loom-importer-extension/src/api/cap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { LoomExportData } from "../types/loom";
import { getApiBaseUrl } from "../utils/urls";

interface ApiResponse<T> {
data: T;
error?: string;
}

interface UserResponse {
user: User;
expires: string;
}
interface User {
name: string;
email: string;
image: string;
id: string;
}

interface LoomImportResponse {
success: boolean;
message: string;
}

interface Workspace {
id: string;
name: string;
ownerId: string;
metadata: null;
allowedEmailDomain: null;
customDomain: null;
domainVerified: null;
createdAt: string;
updatedAt: string;
workosOrganizationId: null;
workosConnectionId: null;
}

interface WorkspaceResponse {
workspaces: Workspace[];
}

export class CapApi {
private baseUrl = getApiBaseUrl();
private headers: HeadersInit = {
accept: "*/*",
"content-type": "application/json",
};

private async getAuthToken(): Promise<string | null> {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ action: "getAuthStatus" }, (response) => {
resolve(response?.token || null);
});
});
}

private async getHeaders(): Promise<HeadersInit> {
const token = await this.getAuthToken();
return {
...this.headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}

private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
try {
const headers = await this.getHeaders();
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
...headers,
...options.headers,
},
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || "API request failed");
}

return { data: data as T };
} catch (error) {
return {
data: {} as T,
error:
error instanceof Error ? error.message : "Unknown error occurred",
};
}
}

public async getUser(): Promise<UserResponse> {
const response = await this.request<UserResponse>("/auth/session");
return response.data;
}

/**
* Sends imported Loom data to Cap.so
* @param loomData The exported Loom data to import into Cap.so
* @returns Response with import status
*/
public async sendLoomData(
loomData: LoomExportData
): Promise<LoomImportResponse> {
const response = await this.request<LoomImportResponse>("/import/loom", {
method: "POST",
body: JSON.stringify(loomData),
});

if (response.error) {
return {
success: false,
message: response.error,
};
}

return response.data;
}

public async getWorkspaceDetails(): Promise<WorkspaceResponse> {
const response = await this.request<WorkspaceResponse>(
"/settings/workspace/details"
);
return response.data;
}
}
Loading
Loading