Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
46ceab0
feat: add inline post preview embed flow
tomeredlich Apr 12, 2026
aea2846
feat: responsive inline article preview with animated toggle
tomeredlich Apr 12, 2026
3b0e46b
fix: keep browsing prompt viewport-centered while scrolling
tomeredlich Apr 12, 2026
0ea7b2d
feat: add mobile article preview drawer with responsive UI
tomeredlich Apr 13, 2026
f3b5e24
fix: stabilize narrow preview floating behavior and transitions
tomeredlich Apr 13, 2026
bbe96c0
feat: update webapp fallback prompt to install extension
tomeredlich Apr 13, 2026
c480bb9
fix: address PR review feedback for inline article preview
tomeredlich Apr 13, 2026
fb4fc5e
chore: add comment explaining extension ID precedence
tomeredlich Apr 13, 2026
c2ffaf2
Merge branch 'main' of github.com:dailydotdev/apps into url-preview-o…
rebelchris Apr 16, 2026
42e78fa
fix: minor changes to flow
rebelchris Apr 17, 2026
28e5382
fix: styling cleanup
rebelchris Apr 17, 2026
c2a9fe4
fix: styling cleanup
rebelchris Apr 17, 2026
2508e56
Merge remote-tracking branch 'origin/main' into url-preview-on-posts
rebelchris Apr 17, 2026
76f72bb
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 17, 2026
f1e1844
fix: styling cleanup
rebelchris Apr 17, 2026
20dbf2a
Merge remote-tracking branch 'origin/url-preview-on-posts' into url-p…
rebelchris Apr 17, 2026
cf4d689
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 17, 2026
8d8aa3a
feat: implement ReaderPostModal and related components for enhanced a…
tsahimatsliah Apr 17, 2026
3ff2ea3
feat: enhance Feed component with reader eligibility and click handling
tsahimatsliah Apr 20, 2026
e660de6
merge: override remote branch with local changes
tsahimatsliah Apr 20, 2026
06bd0a7
Merge branch 'main' of github.com:dailydotdev/apps into url-preview-o…
rebelchris Apr 21, 2026
d3c85f9
fix: restore un-needed changes
rebelchris Apr 21, 2026
7740e7f
fix: cleanup unused imports
rebelchris Apr 21, 2026
5f2a865
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 21, 2026
2c6f14e
fix: missed removal
rebelchris Apr 21, 2026
b8fc1b5
fix: build issue
rebelchris Apr 21, 2026
969f856
fix: move to flags
rebelchris Apr 21, 2026
056670d
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 21, 2026
0cb9afc
fix: test and ts-strict
rebelchris Apr 21, 2026
36a5506
fix: more cleanup
rebelchris Apr 22, 2026
cef431f
fix: evaluation
rebelchris Apr 22, 2026
1891e08
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 22, 2026
4b199fe
fix: cleanup
rebelchris Apr 22, 2026
252ff36
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 22, 2026
a70def0
fix: optimizations
rebelchris Apr 23, 2026
df2f8af
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 23, 2026
65a52b1
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 23, 2026
9db80e6
fix: feedback round
rebelchris Apr 23, 2026
d374f8c
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 23, 2026
fd800d3
fix: feedback round
rebelchris Apr 23, 2026
2f959fd
chore: different ux flow with polling and some hotfixes
rebelchris Apr 28, 2026
de69e42
Merge remote-tracking branch 'origin/main' into url-preview-on-posts
rebelchris Apr 28, 2026
5e41275
chore: apply lint:fix formatting after merge
rebelchris Apr 28, 2026
e8f16bb
fix: add go back feature
rebelchris Apr 28, 2026
df23a58
Merge branch 'main' into url-preview-on-posts
rebelchris Apr 28, 2026
2f8d8cf
fix: linting
rebelchris Apr 28, 2026
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
8 changes: 7 additions & 1 deletion packages/extension/rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ const mainConfig = {
import: path.join(sourcePath, 'frame', 'index.ts'),
runtime: false,
},
ping: {
import: path.join(sourcePath, 'ping', 'index.ts'),
runtime: false,
},
newtab: {
import: path.join(sourcePath, 'newtab', 'index.tsx'),
runtime: 'runtime',
Expand Down Expand Up @@ -253,7 +257,9 @@ const mainConfig = {
...baseConfig.optimization,
splitChunks: {
chunks(chunk) {
return !['content', 'companion', 'manifest'].includes(chunk.name);
return !['content', 'companion', 'manifest', 'ping'].includes(
chunk.name,
);
},
maxSize: 244000,
cacheGroups: {
Expand Down
46 changes: 25 additions & 21 deletions packages/extension/src/content/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,34 @@ import browser from 'webextension-polyfill';
import { removeLinkTargetElement } from '@dailydotdev/shared/src/lib/strings';
import { ExtensionMessageType } from '@dailydotdev/shared/src/lib/extension';

const isRendered = !!document.querySelector('daily-companion-app');
// Keep the companion out of embedded article/reader iframes so our injected
// host element and CSS can never alter the page being previewed.
if (window.top === window.self) {
const isRendered = !!document.querySelector('daily-companion-app');

if (!isRendered) {
// Inject app div
const appContainer = document.createElement('daily-companion-app');
document.body.appendChild(appContainer);
if (!isRendered) {
// Inject app div
const appContainer = document.createElement('daily-companion-app');
document.body.appendChild(appContainer);

// Create shadow dom
const shadow = document
.querySelector('daily-companion-app')
.attachShadow({ mode: 'open' });
// Create shadow dom
const shadow = appContainer.attachShadow({ mode: 'open' });

const wrapper = document.createElement('div');
wrapper.id = 'daily-companion-wrapper';
shadow.appendChild(wrapper);
const wrapper = document.createElement('div');
wrapper.id = 'daily-companion-wrapper';
shadow.appendChild(wrapper);

browser.runtime.sendMessage({ type: ExtensionMessageType.ContentLoaded });
browser.runtime.sendMessage({ type: ExtensionMessageType.ContentLoaded });

let lastUrl = removeLinkTargetElement(window.location.href);
new MutationObserver(() => {
const current = removeLinkTargetElement(window.location.href);
if (current !== lastUrl) {
lastUrl = current;
browser.runtime.sendMessage({ type: ExtensionMessageType.ContentLoaded });
}
}).observe(document, { subtree: true, childList: true });
let lastUrl = removeLinkTargetElement(window.location.href);
new MutationObserver(() => {
const current = removeLinkTargetElement(window.location.href);
if (current !== lastUrl) {
lastUrl = current;
browser.runtime.sendMessage({
type: ExtensionMessageType.ContentLoaded,
});
}
}).observe(document, { subtree: true, childList: true });
}
}
90 changes: 90 additions & 0 deletions packages/extension/src/frame/controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { extensionSiteEmbedFrameEvent } from '@dailydotdev/shared/src/features/extensionEmbed/common';
import browser from 'webextension-polyfill';
import { initializeFrame } from './controller';
import {
enableFrameEmbeddingViaBackground,
hasFrameEmbeddingPermissions,
requestFrameEmbeddingPermissions,
} from '../lib/frameEmbedding';
import { renderMessage, renderPermissionPrompt } from './render';

jest.mock('../lib/frameEmbedding', () => ({
enableFrameEmbeddingViaBackground: jest.fn(),
hasFrameEmbeddingPermissions: jest.fn(),
requestFrameEmbeddingPermissions: jest.fn(),
}));

jest.mock('webextension-polyfill', () => ({
runtime: {
reload: jest.fn(),
},
}));

jest.mock('./render', () => ({
renderMessage: jest.fn(),
renderPermissionPrompt: jest.fn(),
}));

describe('initializeFrame', () => {
const root = document.createElement('div');
const target = new URL('https://example.com/article');
const sendParentMessage = jest.fn();
const onEmbeddingEnabled = jest.fn();

beforeEach(() => {
jest.useFakeTimers();
jest.clearAllMocks();
});

afterEach(() => {
jest.useRealTimers();
});

it('requests an extension reload after permission is granted', async () => {
(hasFrameEmbeddingPermissions as jest.Mock).mockResolvedValue(false);
(requestFrameEmbeddingPermissions as jest.Mock).mockResolvedValue(true);

let requestPermission:
| (() => Promise<'granted' | 'dismissed' | 'failed'>)
| undefined;
(renderPermissionPrompt as jest.Mock).mockImplementation(
({
onRequestPermission,
}: {
onRequestPermission: () => Promise<'granted' | 'dismissed' | 'failed'>;
}) => {
requestPermission = onRequestPermission;
},
);

await initializeFrame({
root,
target,
sendParentMessage,
onEmbeddingEnabled,
});

expect(requestPermission).toBeDefined();
await expect(requestPermission?.()).resolves.toBe('granted');

expect(sendParentMessage).toHaveBeenCalledWith(
extensionSiteEmbedFrameEvent.Error,
{
reason: 'missing-permission',
target: target.href,
},
);
expect(sendParentMessage).toHaveBeenCalledWith(
extensionSiteEmbedFrameEvent.ReloadRequested,
{
target: target.href,
},
);
expect(enableFrameEmbeddingViaBackground).not.toHaveBeenCalled();
expect(onEmbeddingEnabled).not.toHaveBeenCalled();
expect(renderMessage).not.toHaveBeenCalled();
expect(browser.runtime.reload).toHaveBeenCalledTimes(0);
jest.runOnlyPendingTimers();
expect(browser.runtime.reload).toHaveBeenCalledTimes(1);
});
});
132 changes: 78 additions & 54 deletions packages/extension/src/frame/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,50 @@ const errorLog = (...args: unknown[]): void => {

type SendParentMessage = (type: string, detail: Record<string, string>) => void;

type PermissionRequestOutcome = 'granted' | 'dismissed' | 'failed';

const prepareEmbedding = async ({
root,
target,
sendParentMessage,
onEmbeddingEnabled,
}: {
root: HTMLDivElement;
target: URL;
sendParentMessage: SendParentMessage;
onEmbeddingEnabled: () => void;
}): Promise<boolean> => {
sendParentMessage(extensionSiteEmbedFrameEvent.PermissionsReady, {
target: target.href,
});

// The visible site iframe only mounts after the tab-scoped rule is live.
// That avoids a flash of the browser's built-in frame-blocked error page.
renderMessage(
root,
'Preparing embedded browsing',
'Configuring this tab so the requested site can be embedded safely.',
);

const result = await enableFrameEmbeddingViaBackground();
if (!result.enabled) {
sendParentMessage(extensionSiteEmbedFrameEvent.Error, {
reason: 'enable-frame-embedding-failed',
target: target.href,
error: result.error ?? 'Unknown frame embedding error',
});
return false;
}

// The webapp keeps this frame mounted invisibly after success so it can
// tear the tab-scoped rule down when the page unloads or a new target starts.
onEmbeddingEnabled();
sendParentMessage(extensionSiteEmbedFrameEvent.EmbeddingReady, {
target: target.href,
});
return true;
};

export const createFrameCleanupController = () => {
let shouldDisableEmbeddingOnCleanup = false;

Expand Down Expand Up @@ -53,39 +97,41 @@ export const initializeFrame = async ({
const hasPermissions = await hasFrameEmbeddingPermissions();

if (!hasPermissions) {
renderPermissionPrompt({
root,
onRequestPermission: async () => {
try {
const granted = await requestFrameEmbeddingPermissions();

if (!granted) {
sendParentMessage(extensionSiteEmbedFrameEvent.Error, {
reason: 'permission-denied',
target: target.href,
});
return 'dismissed';
}

sendParentMessage(extensionSiteEmbedFrameEvent.ReloadRequested, {
target: target.href,
});

// After the optional permission prompt resolves, the fresh extension
// context is much more reliable for enabling the DNR session rule.
globalThis.setTimeout(() => {
browser.runtime.reload();
}, 100);
const onRequestPermission = async (): Promise<PermissionRequestOutcome> => {
try {
const granted = await requestFrameEmbeddingPermissions();

return 'granted';
} catch {
if (!granted) {
sendParentMessage(extensionSiteEmbedFrameEvent.Error, {
reason: 'permission-request-failed',
reason: 'permission-denied',
target: target.href,
});
return 'failed';
return 'dismissed';
}
},

sendParentMessage(extensionSiteEmbedFrameEvent.ReloadRequested, {
target: target.href,
});

// After the optional permission prompt resolves, the fresh extension
// context is much more reliable for enabling the DNR session rule.
globalThis.setTimeout(() => {
browser.runtime.reload();
}, 100);

return 'granted';
} catch {
sendParentMessage(extensionSiteEmbedFrameEvent.Error, {
reason: 'permission-request-failed',
target: target.href,
});
return 'failed';
}
};

renderPermissionPrompt({
root,
onRequestPermission,
});

sendParentMessage(extensionSiteEmbedFrameEvent.Error, {
Expand All @@ -95,32 +141,10 @@ export const initializeFrame = async ({
return;
}

sendParentMessage(extensionSiteEmbedFrameEvent.PermissionsReady, {
target: target.href,
});

// The visible site iframe only mounts after the tab-scoped rule is live.
// That avoids a flash of the browser's built-in frame-blocked error page.
renderMessage(
await prepareEmbedding({
root,
'Preparing embedded browsing',
'Configuring this tab so the requested site can be embedded safely.',
);

const result = await enableFrameEmbeddingViaBackground();
if (!result.enabled) {
sendParentMessage(extensionSiteEmbedFrameEvent.Error, {
reason: 'enable-frame-embedding-failed',
target: target.href,
error: result.error ?? 'Unknown frame embedding error',
});
return;
}

// The webapp keeps this frame mounted invisibly after success so it can
// tear the tab-scoped rule down when the page unloads or a new target starts.
onEmbeddingEnabled();
sendParentMessage(extensionSiteEmbedFrameEvent.EmbeddingReady, {
target: target.href,
target,
sendParentMessage,
onEmbeddingEnabled,
});
};
17 changes: 17 additions & 0 deletions packages/extension/src/frame/parentMessaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,26 @@ const isAllowedParentHost = (hostname: string): boolean =>
hostname.endsWith('.daily.dev') ||
hostname.endsWith('.local.fylla.dev');

const isSelfExtensionOrigin = (origin: URL): boolean => {
if (
origin.protocol !== 'chrome-extension:' &&
origin.protocol !== 'moz-extension:'
) {
return false;
}
// Only accept the frame's own extension origin — this allows the new tab
// page (same extension) to embed frame.html, but rejects other extensions.
return (
typeof window !== 'undefined' && origin.origin === window.location.origin
);
};

const getAllowedOrigin = (value: string): string | null => {
try {
const origin = new URL(value);
if (isSelfExtensionOrigin(origin)) {
return origin.origin;
}
return isAllowedParentHost(origin.hostname) ? origin.origin : null;
} catch {
return null;
Expand Down
Loading
Loading