Skip to content

Commit

Permalink
Fix ipynb files not being viewable on github
Browse files Browse the repository at this point in the history
- The file is rendered in an iframe which crashes when it receives a
  message via window.postMessage
- This change switches to using a push vs pull method for obtaining
  frame IDs. Child iframes broadcast their frame ID info instead of the
  parent requesting it obviating the need to use window.postMessage on
  the child iframe.
  • Loading branch information
killergerbah committed Feb 25, 2024
1 parent eb30da8 commit 772643b
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 101 deletions.
125 changes: 66 additions & 59 deletions extension/src/services/frame-info.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,93 @@
import { v4 as uuidv4 } from 'uuid';

export class FrameInfoListener {
export class FrameInfoBroadcaster {
readonly frameId = uuidv4();
private _windowMessageListener?: (event: MessageEvent) => void;
private _broadcastInterval?: NodeJS.Timeout;
private _bound = false;

bind() {
this._windowMessageListener = (event) => {
if (event.data.sender !== 'asbplayer-video') {
return;
}
if (this._bound) {
return;
}

if (event.source !== window.parent) {
return;
}
this._broadcastInterval = setInterval(() => this._broadcast(), 10000);
this._broadcast();
this._bound = true;
}

switch (event.data.message.command) {
case 'frameId': {
window.parent.postMessage(
{
sender: 'asbplayer-video',
message: {
requestId: event.data.message.requestId,
frameId: this.frameId,
},
},
'*'
);
}
}
};
window.addEventListener('message', this._windowMessageListener);
private _broadcast() {
window.parent.postMessage(
{
sender: 'asbplayer-video',
message: {
frameId: this.frameId,
},
},
'*'
);
}

unbind() {
if (this._windowMessageListener) {
window.removeEventListener('message', this._windowMessageListener);
if (this._broadcastInterval !== undefined) {
clearInterval(this._broadcastInterval);
this._broadcastInterval = undefined;
}

this._bound = false;
}
}

export const fetchFrameId = (frame: HTMLIFrameElement): Promise<string | undefined> => {
return new Promise((resolve, reject) => {
if (!frame.contentWindow) {
return resolve(undefined);
export class FrameInfoListener {
readonly iframesById: { [key: string]: HTMLIFrameElement } = {};
private _listener?: (event: MessageEvent) => void;
private _bound = false;

bind() {
if (this._bound) {
return;
}

const requestId = uuidv4();
let timeoutId: NodeJS.Timeout | undefined;
const listener = (event: MessageEvent) => {
if (event.source !== frame.contentWindow) {
this._listener = (event: MessageEvent) => {
if (event.data?.sender !== 'asbplayer-video') {
return;
}

if (event.data.message?.requestId !== requestId) {
const sourceIframe = this._sourceIframeForEvent(event);

if (sourceIframe === undefined) {
return;
}

if (typeof event.data.message?.frameId === 'string') {
if (timeoutId) {
clearTimeout(timeoutId);
}
const frameId = event.data.message.frameId;

window.removeEventListener('message', listener);
resolve(event.data.message.frameId);
if (!frameId) {
return;
}

this.iframesById[frameId] = sourceIframe;
};

window.addEventListener('message', listener);
frame.contentWindow.postMessage(
{
sender: 'asbplayer-video',
message: {
command: 'frameId',
requestId: requestId,
},
},
'*'
);
window.addEventListener('message', this._listener);
this._bound = true;
}

timeoutId = setTimeout(() => {
window.removeEventListener('message', listener);
resolve(undefined);
}, 1000);
});
};
unbind() {
if (this._listener !== undefined) {
window.removeEventListener('message', this._listener);
this._listener = undefined;
}

this._bound = false;
}

private _sourceIframeForEvent(event: MessageEvent) {
const iframes = document.getElementsByTagName('iframe');
for (const iframe of iframes) {
if (iframe.contentWindow === event.source) {
return iframe;
}
}

return undefined;
}
}
59 changes: 17 additions & 42 deletions extension/src/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,14 @@ import {
ToggleSidePanelMessage,
} from '@project/common';
import { SettingsProvider } from '@project/common/settings';
import { FrameInfoListener, fetchFrameId } from './services/frame-info';
import { FrameInfoBroadcaster, FrameInfoListener } from './services/frame-info';
import { cropAndResize } from '@project/common/src/image-transformer';
import { TabAnkiUiController } from './controllers/tab-anki-ui-controller';
import { ExtensionSettingsStorage } from './services/extension-settings-storage';
import { DefaultKeyBinder } from '@project/common/key-binder';

const extensionSettingsStorage = new ExtensionSettingsStorage();
const settingsProvider = new SettingsProvider(extensionSettingsStorage);
const iframesByFrameId: { [frameId: string]: HTMLIFrameElement } = {};

const cacheIframesByFrameId = () => {
const iframes = document.getElementsByTagName('iframe');

for (let i = 0; i < iframes.length; ++i) {
const iframe = iframes[i];

if (!Object.values(iframesByFrameId).find((f) => iframe === f)) {
fetchFrameId(iframe).then((frameId) => {
if (frameId) {
iframesByFrameId[frameId] = iframe;
}
});
}
}

for (const frameId of Object.keys(iframesByFrameId)) {
let iframeExists = false;

for (let i = 0; i < iframes.length; ++i) {
const iframe = iframes[i];

if (iframe.isSameNode(iframesByFrameId[frameId])) {
iframeExists = true;
}
}

if (!iframeExists) {
delete iframesByFrameId[frameId];
}
}
};

let unbindToggleSidePanel: (() => void) | undefined;

Expand Down Expand Up @@ -95,11 +62,16 @@ const bind = () => {
const page = currentPageDelegate();
let subSyncAvailable = page !== undefined;
let frameInfoListener: FrameInfoListener | undefined;
let frameInfoBroadcaster: FrameInfoBroadcaster | undefined;
const isParentDocument = window.self === window.top;

if (window.self !== window.top) {
// Inside iframe, listen for frame ID requests
if (isParentDocument) {
// Parent document, listen for child iframe info
frameInfoListener = new FrameInfoListener();
frameInfoListener.bind();
} else {
// Child iframe, broadcast frame info
frameInfoBroadcaster = new FrameInfoBroadcaster();
}

const bindToVideoElements = () => {
Expand All @@ -110,7 +82,7 @@ const bind = () => {
const bindingExists = bindings.filter((b) => b.video.isSameNode(videoElement)).length > 0;

if (!bindingExists && hasValidVideoSource(videoElement) && !page?.shouldIgnore(videoElement)) {
const b = new Binding(videoElement, subSyncAvailable, frameInfoListener?.frameId);
const b = new Binding(videoElement, subSyncAvailable, frameInfoBroadcaster?.frameId);
b.bind();
bindings.push(b);
}
Expand All @@ -134,18 +106,21 @@ const bind = () => {
b.unbind();
}
}

if (bindings.length === 0) {
frameInfoBroadcaster?.unbind();
} else {
frameInfoBroadcaster?.bind();
}
};

bindToVideoElements();
cacheIframesByFrameId();
const videoInterval = setInterval(bindToVideoElements, 1000);
const iframeInterval = setInterval(cacheIframesByFrameId, 10000);

const videoSelectController = new VideoSelectController(bindings);
videoSelectController.bind();

const ankiUiController = new TabAnkiUiController(settingsProvider);
const isParentDocument = window.self === window.top;

if (isParentDocument) {
bindToggleSidePanel();
Expand Down Expand Up @@ -179,7 +154,7 @@ const bind = () => {
let rect = cropAndResizeMessage.rect;

if (cropAndResizeMessage.frameId !== undefined) {
const iframe = iframesByFrameId[cropAndResizeMessage.frameId];
const iframe = frameInfoListener?.iframesById?.[cropAndResizeMessage.frameId];

if (iframe !== undefined) {
const iframeRect = iframe.getBoundingClientRect();
Expand Down Expand Up @@ -224,9 +199,9 @@ const bind = () => {
bindings.length = 0;

clearInterval(videoInterval);
clearInterval(iframeInterval);
videoSelectController.unbind();
frameInfoListener?.unbind();
frameInfoBroadcaster?.unbind();
unbindToggleSidePanel?.();
chrome.runtime.onMessage.removeListener(messageListener);
});
Expand Down

0 comments on commit 772643b

Please sign in to comment.