Chrome Extension Meeting Notes — Developer Guide

11 min read

Build a Meeting Notes Extension

What You’ll Build

A Chrome extension for quick note-taking during online meetings with timestamp tracking and meeting history.

Manifest

{
  "manifest_version": 3,
  "name": "MeetingNotes",
  "version": "1.0.0",
  "permissions": ["sidePanel", "storage", "activeTab", "tabs"],
  "host_permissions": ["*://meet.google.com/*", "*://*.zoom.us/*"],
  "side_panel": { "default_path": "sidepanel.html" },
  "action": { "default_popup": "popup.html" },
  "background": { "service_worker": "background.js" },
  "content_scripts": [
    {
      "matches": ["*://meet.google.com/*", "*://*.zoom.us/*"],
      "js": ["content.js"]
    }
  ],
  "commands": {
    "timestamp": {
      "suggested_key": "Ctrl+T",
      "description": "Insert timestamp"
    }
  },
  "icons": { "16": "icon-16.png", "48": "icon-48.png", "128": "icon-128.png" }
}

Step 1: Meeting Detection

Content script detects meeting pages via URL pattern and extracts meeting info from the DOM.

// content.ts - Detect meeting pages
const MEETING_PATTERNS = [
  /meet\.google\.com/,
  /zoom\.us\/j\/\d+/
];

function detectMeeting(): MeetingInfo | null {
  const url = window.location.href;
  if (!MEETING_PATTERNS.some(p => p.test(url))) return null;

  let title = '';
  
  // Google Meet
  if (url.includes('meet.google.com')) {
    const titleEl = document.querySelector('[data-meeting-title]');
    title = titleEl?.textContent?.trim() || 'Google Meet';
  }
  
  // Zoom
  if (url.includes('zoom.us')) {
    const titleEl = document.querySelector('.meeting-title');
    title = titleEl?.textContent?.trim() || 'Zoom Meeting';
  }

  return { url, title, startTime: Date.now() };
}

// Send message to background to open side panel
const meeting = detectMeeting();
if (meeting) {
  chrome.runtime.sendMessage({ type: 'MEETING_START', meeting });
}

In background.js, handle the message and open the side panel:

// background.js - chrome.sidePanel.open() can only be called from the background service worker
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'MEETING_START') {
    chrome.sidePanel.open({ tabId: sender.tab?.id });
  }
});

Step 2: Side Panel Note Editor

Simple rich text editor with meeting title and auto-save functionality.

// sidepanel.ts - Note editor UI
class NoteEditor {
  private editor: HTMLElement;
  private meetingInfo: MeetingInfo | null = null;
  private saveTimeout: number | null = null;

  constructor() {
    this.editor = document.getElementById('editor')!;
    this.init();
  }

  private async init() {
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    this.meetingInfo = await this.getMeetingInfo(tab);
    
    if (this.meetingInfo) {
      this.renderHeader();
      await this.loadNotes();
      this.setupAutoSave();
    }
  }

  private renderHeader() {
    const header = document.getElementById('header');
    header.innerHTML = `
      <h2>${this.meetingInfo?.title || 'Meeting Notes'}</h2>
      <button id="timestamp-btn">Add Timestamp</button>
    `;
    
    document.getElementById('timestamp-btn')?.addEventListener('click', () => {
      this.insertTimestamp();
    });
  }

  private insertTimestamp() {
    if (!this.meetingInfo) return;
    
    const elapsed = Date.now() - this.meetingInfo.startTime;
    const minutes = Math.floor(elapsed / 60000);
    const seconds = Math.floor((elapsed % 60000) / 1000);
    const timestamp = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}]`;
    
    this.editor.focus();
    document.execCommand('insertText', false, timestamp + ' ');
  }

  private setupAutoSave() {
    this.editor.addEventListener('input', () => {
      if (this.saveTimeout) clearTimeout(this.saveTimeout);
      this.saveTimeout = window.setTimeout(() => this.saveNotes(), 500);
    });
  }
}

Step 3: Timestamping

Track meeting start time and provide quick timestamp insertion via button or keyboard shortcut.

// sidepanel.ts - Keyboard shortcut and timestamp utilities
function setupTimestampShortcut() {
  chrome.commands.onCommand.addListener(async (command) => {
    if (command === 'timestamp') {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      chrome.tabs.sendMessage(tab.id!, { type: 'INSERT_TIMESTAMP' });
    }
  });
}

// Make timestamps clickable in review mode
function makeTimestampsClickable() {
  const editor = document.getElementById('editor');
  const regex = /\[(\d{2}):(\d{2})\]/g;
  
  editor.addEventListener('click', (e) => {
    const target = e.target as HTMLElement;
    if (target.classList.contains('timestamp')) {
      const time = target.dataset.time;
      // Jump to position in video if available
      chrome.runtime.sendMessage({ type: 'SEEK_VIDEO', time });
    }
  });
}

Step 4: Note Storage

Save per-meeting notes using @theluckystrike/webext-storage with auto-save and retention limits.

// storage.ts - Note persistence
import { createStorage, defineSchema } from '@theluckystrike/webext-storage';

const meetingSchema = defineSchema({
  meetings: 'object',      // Map of meetingUrl -> MeetingNotes
  settings: 'object'       // User preferences
});

const storage = createStorage(meetingSchema, 'local');

interface MeetingNotes {
  meetingUrl: string;
  title: string;
  date: number;
  notes: string;
  duration: number;
}

const MAX_MEETINGS = 50;

async function saveMeetingNotes(notes: MeetingNotes) {
  const meetings = await storage.get('meetings') || {};
  meetings[notes.meetingUrl] = notes;
  
  // Enforce retention limit
  const sorted = Object.values(meetings)
    .sort((a, b) => b.date - a.date)
    .slice(0, MAX_MEETINGS);
  
  const trimmed: Record<string, MeetingNotes> = {};
  sorted.forEach(m => trimmed[m.meetingUrl] = m);
  
  await storage.set('meetings', trimmed);
}

async function loadMeetingNotes(url: string): Promise<MeetingNotes | null> {
  const meetings = await storage.get('meetings') || {};
  return meetings[url] || null;
}

Step 5: Meeting History

List past meeting notes in popup with search and delete capabilities.

// popup.ts - Meeting history view
async function renderMeetingHistory() {
  const meetings = await storage.get('meetings') || {};
  const sorted = Object.values(meetings).sort((a, b) => b.date - a.date);
  
  const container = document.getElementById('history-list');
  container.innerHTML = sorted.map(meeting => `
    <div class="meeting-item" data-url="${meeting.meetingUrl}">
      <div class="title">${meeting.title}</div>
      <div class="date">${new Date(meeting.date).toLocaleDateString()}</div>
      <div class="preview">${meeting.notes.substring(0, 100)}...</div>
      <button class="delete-btn" data-url="${meeting.meetingUrl}">Delete</button>
    </div>
  `).join('');
}

// Search functionality
function filterMeetings(query: string, meetings: MeetingNotes[]): MeetingNotes[] {
  const lower = query.toLowerCase();
  return meetings.filter(m => 
    m.title.toLowerCase().includes(lower) ||
    m.notes.toLowerCase().includes(lower) ||
    new Date(m.date).toLocaleDateString().includes(lower)
  );
}

Step 6: Export

Export notes as markdown, copy to clipboard, or share via email.

// export.ts - Export utilities
async function exportAsMarkdown(meeting: MeetingNotes): Promise<string> {
  const date = new Date(meeting.date).toLocaleString();
  return `# ${meeting.title}

**Date:** ${date}
**Duration:** ${Math.round(meeting.duration / 60000)} minutes
**Meeting URL:** ${meeting.meetingUrl}

---

${meeting.notes}
`;
}

async function copyToClipboard(text: string) {
  await navigator.clipboard.writeText(text);
}

function shareViaEmail(meeting: MeetingNotes) {
  const markdown = exportAsMarkdown(meeting);
  const body = encodeURIComponent(markdown);
  window.open(`mailto:?subject=${encodeURIComponent(meeting.title)}&body=${body}`);
}

async function exportAllMeetings() {
  const meetings = await storage.get('meetings') || {};
  const allMarkdown = Object.values(meetings)
    .sort((a, b) => b.date - a.date)
    .map(m => exportAsMarkdown(m))
    .join('\n\n---\n\n');
  
  downloadFile('meeting-notes.md', allMarkdown);
}

Cross-references

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.

No previous article
No next article