Skip to content
Closed
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
143 changes: 88 additions & 55 deletions extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,75 @@ const timeFormat = {
hour12: true
}

// --- Download bridge for MV3 service worker -> meetings.html (DOM context) ---
/** @type {chrome.runtime.Port | null} */
let meetingsPort = null
/** @type {number | null} */
let autoOpenedMeetingsTabId = null

chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'meetings') {
meetingsPort = port
port.onDisconnect.addListener(() => {
if (meetingsPort === port) {
meetingsPort = null
autoOpenedMeetingsTabId = null
}
})
}
})

/**
* Ensure we have a DOM-capable extension page connected (meetings.html),
* so we can create Blob URLs / anchor downloads (service workers lack URL.createObjectURL).
* @returns {Promise<chrome.runtime.Port>}
*/
function ensureMeetingsPort() {
return new Promise((resolve, reject) => {
if (meetingsPort) return resolve(meetingsPort)

// Open meetings.html in a background tab (inactive) so it can connect back via runtime.connect
const url = chrome.runtime.getURL('meetings.html')
chrome.tabs.create({ url, active: false }, (tab) => {
autoOpenedMeetingsTabId = tab?.id ?? null

const timeoutMs = 5000
const start = Date.now()
const timer = setInterval(() => {
if (meetingsPort) {
clearInterval(timer)
resolve(meetingsPort)
} else if (Date.now() - start > timeoutMs) {
clearInterval(timer)
reject(new Error('meetings.html did not connect in time'))
}
}, 50)
})
})
}

/**
* Ask meetings.html to download a transcript using Blob + <a download>, which preserves filenames in Brave.
* @param {string} filename
* @param {string} content
*/
function downloadViaMeetingsPage(filename, content) {
return ensureMeetingsPort().then((port) => {
port.postMessage({ type: 'download_transcript_payload', filename, content })

// If we auto-opened the tab just for downloading, close it shortly after triggering download.
if (autoOpenedMeetingsTabId !== null) {
const tabIdToClose = autoOpenedMeetingsTabId
autoOpenedMeetingsTabId = null
setTimeout(() => {
chrome.tabs.remove(tabIdToClose)
}, 2000)
}
})
}
// --- End download bridge ---



chrome.runtime.onMessage.addListener(function (messageUnTyped, sender, sendResponse) {
const message = /** @type {ExtensionMessage} */ (messageUnTyped)
Expand Down Expand Up @@ -345,13 +414,9 @@ function downloadTranscript(index, isWebhookEnabled) {
sanitisedMeetingTitle = meeting.title.replaceAll(invalidFilenameRegex, "_")
}

// Format timestamp for human-readable filename and sanitise to prevent invalid filenames
const timestamp = new Date(meeting.meetingStartTimestamp)
const formattedTimestamp = timestamp.toLocaleString("default", timeFormat).replace(/[\/:]/g, "-")

const prefix = meeting.meetingSoftware ? `${meeting.meetingSoftware} transcript` : "Transcript"

const fileName = `TranscripTonic/${prefix}-${sanitisedMeetingTitle} at ${formattedTimestamp} on.txt`
// Use meeting title as the filename.
// (Brave + MV3: we download via meetings.html using <a download>, which cannot create subfolders.)
const fileName = `${sanitisedMeetingTitle}.txt`


// Format transcript and chatMessages content
Expand All @@ -364,57 +429,25 @@ function downloadTranscript(index, isWebhookEnabled) {
content += "Transcript saved using TranscripTonic Chrome extension (https://chromewebstore.google.com/detail/ciepnfnceimjehngolkijpnbappkkiag)"
content += "\n---------------"

const blob = new Blob([content], { type: "text/plain" })

// Read the blob as a data URL
const reader = new FileReader()

// Read the blob
reader.readAsDataURL(blob)

// Download as text file, once blob is read
reader.onload = function (event) {
if (event.target?.result) {
const dataUrl = event.target.result

// Create a download with Chrome Download API
chrome.downloads.download({
// @ts-ignore
url: dataUrl,
filename: fileName,
conflictAction: "uniquify"
}).then(() => {
console.log("Transcript downloaded")
resolve("Transcript downloaded successfully")
// Download via meetings.html (DOM context) because Brave ignores filenames for data: downloads from service workers.
downloadViaMeetingsPage(fileName, content)
.then(() => {
console.log("Transcript downloaded")
resolve("Transcript downloaded successfully")

// Increment anonymous transcript generated count to a Google sheet
fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, {
mode: "no-cors"
})
}).catch((err) => {
console.error(err)
chrome.downloads.download({
// @ts-ignore
url: dataUrl,
filename: "TranscripTonic/Transcript.txt",
conflictAction: "uniquify"
})
console.log("Invalid file name. Transcript downloaded to TranscripTonic directory with simple file name.")
resolve("Transcript downloaded successfully with default file name")
// Increment anonymous transcript generated count to a Google sheet
fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, {
mode: "no-cors"
})
})
.catch((err) => {
console.error(err)
reject({ errorCode: "009", errorMessage: "Failed to download transcript" })

// Logs anonymous errors to a Google sheet for swift debugging
fetch(`https://script.google.com/macros/s/AKfycbwN-bVkVv3YX4qvrEVwG9oSup0eEd3R22kgKahsQ3bCTzlXfRuaiO7sUVzH9ONfhL4wbA/exec?version=${chrome.runtime.getManifest().version}&code=009&error=${encodeURIComponent(err)}&meetingSoftware=${meeting.meetingSoftware}`, { mode: "no-cors" })
// Logs anonymous errors to a Google sheet for swift debugging
fetch(`https://script.google.com/macros/s/AKfycbwN-bVkVv3YX4qvrEVwG9oSup0eEd3R22kgKahsQ3bCTzlXfRuaiO7sUVzH9ONfhL4wbA/exec?version=${chrome.runtime.getManifest().version}&code=009&error=${encodeURIComponent(err)}&meetingSoftware=${meeting.meetingSoftware}`, { mode: "no-cors" })
})

// Increment anonymous transcript generated count to a Google sheet
fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, {
mode: "no-cors"
})
})
}
else {
reject({ errorCode: "009", errorMessage: "Failed to read blob" })
}
}
}
else {
reject({ errorCode: "010", errorMessage: "Meeting at specified index not found" })
Expand Down
34 changes: 34 additions & 0 deletions extension/meetings.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@

let isMeetingsTableExpanded = false

// Keep a live connection to the service worker so it can ask this page (DOM context) to download transcripts.
// Service workers in MV3 do not have URL.createObjectURL, and Brave may ignore filenames for data: downloads.
const meetingsPort = chrome.runtime.connect({ name: 'meetings' })

/**
* Trigger a local download using Blob + <a download>.
* Note: <a download> does not support subdirectories, so we strip any path prefix.
* @param {string} filename
* @param {string} content
*/
function downloadTextFile(filename, content) {
const baseName = filename.split('/').pop() || 'Transcript.txt'
const blob = new Blob([content], { type: 'text/plain' })
// URL.createObjectURL is available in this page context.
// @ts-ignore
const url = URL.createObjectURL(blob)

const a = document.createElement('a')
a.href = url
a.download = baseName
document.body.appendChild(a)
a.click()
a.remove()

// @ts-ignore
URL.revokeObjectURL(url)
}

meetingsPort.onMessage.addListener((msg) => {
if (msg && msg.type === 'download_transcript_payload' && typeof msg.content === 'string' && typeof msg.filename === 'string') {
downloadTextFile(msg.filename, msg.content)
}
})

document.addEventListener("DOMContentLoaded", function () {
const webhookUrlForm = document.querySelector("#webhook-url-form")
const webhookUrlInput = document.querySelector("#webhook-url")
Expand Down