Skip to content

Commit 711b68c

Browse files
committed
wip
1 parent d2acb98 commit 711b68c

File tree

7 files changed

+332
-17
lines changed

7 files changed

+332
-17
lines changed

special-pages/pages/new-tab/app/entry-points/omnibar.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { h } from 'preact';
22
import { Centered } from '../components/Layout.js';
33
import { OmnibarCustomized } from '../omnibar/components/OmnibarCustomized.js';
4-
import { PersistentTermsProvider } from '../omnibar/components/PersistentTermsProvider.js';
4+
import { PersistentModeProvider, PersistentTermsProvider } from '../omnibar/components/PersistentTermsProvider.js';
55

66
export function factory() {
77
return (
88
<Centered data-entry-point="omnibar">
99
<PersistentTermsProvider>
10-
<OmnibarCustomized />
10+
<PersistentModeProvider>
11+
<OmnibarCustomized />
12+
</PersistentModeProvider>
1113
</PersistentTermsProvider>
1214
</Centered>
1315
);

special-pages/pages/new-tab/app/omnibar/components/OmnibarConsumer.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useVisibility } from '../../widget-list/widget-config.provider.js';
66
import { Omnibar } from './Omnibar.js';
77
import { OmnibarContext } from './OmnibarProvider.js';
88
import { ArrowIndentCenteredIcon } from '../../components/Icons.js';
9+
import { ModeProvider, useMode } from './PersistentTermsProvider.js';
910

1011
/**
1112
* @typedef {import('../strings.json')} Strings
@@ -27,7 +28,15 @@ import { ArrowIndentCenteredIcon } from '../../components/Icons.js';
2728
export function OmnibarConsumer() {
2829
const { state } = useContext(OmnibarContext);
2930
if (state.status === 'ready') {
30-
return <OmnibarReadyState config={state.config} key={state.config.tabId} />;
31+
if (state.config.tabId && state.config.tabIds) {
32+
return (
33+
<ModeProvider mode={state.config.mode} tabId={state.config.tabId} tabIds={state.config.tabIds}>
34+
<OmnibarReadyState2 config={state.config} key={state.config.tabId} />
35+
</ModeProvider>
36+
);
37+
} else {
38+
return <OmnibarReadyState config={state.config} key={state.config.tabId} />;
39+
}
3140
}
3241
return null;
3342
}
@@ -46,6 +55,21 @@ function OmnibarReadyState({ config: { enableAi = true, showAiSetting = true, mo
4655
);
4756
}
4857

58+
/**
59+
* @param {object} props
60+
* @param {OmnibarConfig} props.config
61+
*/
62+
function OmnibarReadyState2({ config: { enableAi = true, showAiSetting = true } }) {
63+
const { setEnableAi, setMode } = useContext(OmnibarContext);
64+
const mode = useMode();
65+
return (
66+
<>
67+
{showAiSetting && <AiSetting enableAi={enableAi} setEnableAi={setEnableAi} />}
68+
<Omnibar mode={mode} setMode={setMode} enableAi={showAiSetting && enableAi} />
69+
</>
70+
);
71+
}
72+
4973
/**
5074
* @param {object} props
5175
* @param {boolean} props.enableAi

special-pages/pages/new-tab/app/omnibar/components/PersistentTermsProvider.js

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { h, createContext } from 'preact';
22
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
3-
import { OmnibarTerm } from '../omnibar.term.js';
43
import { OmnibarContext, useOmnibarService } from './OmnibarProvider.js';
4+
import { OmnibarValue } from '../omnibarValue.js';
5+
import { OmnibarTerm } from '../omnibar.term.js';
6+
7+
/**
8+
* @typedef {import("../../../types/new-tab.js").OmnibarConfig["mode"]} Mode
9+
*/
510

611
const InitialTermContext = createContext(/** @type {OmnibarTerm|null} */ (null));
712

@@ -58,3 +63,59 @@ export function useQueryWithPersistence() {
5863

5964
return /** @type {const} */ ([query, setter]);
6065
}
66+
67+
const InitialModeContext = createContext(/** @type {OmnibarValue|null} */ (null));
68+
const ModeContext = createContext(/** @type {Mode} */ ('search'));
69+
/**
70+
* @param {object} props
71+
* @param {import('preact').ComponentChildren} props.children
72+
*/
73+
export function PersistentModeProvider({ children }) {
74+
const [terms] = useState(() => new OmnibarValue());
75+
return <InitialModeContext.Provider value={terms}>{children}</InitialModeContext.Provider>;
76+
}
77+
78+
/**
79+
* @param {object} props
80+
* @param {import('preact').ComponentChildren} props.children
81+
* @param {Mode} props.mode
82+
* @param {string|null|undefined} props.tabId
83+
* @param {string[]|null|undefined} props.tabIds
84+
*/
85+
export function ModeProvider({ children, mode, tabId, tabIds }) {
86+
const value = useContext(InitialModeContext);
87+
if (!value) throw new Error('unreachable');
88+
89+
const [state, setState] = useState(() => {
90+
if (!Array.isArray(tabIds) || !tabId) throw new Error('must have tabId + tabIds');
91+
if (tabIds.length === 0) throw new Error('tabIds cannot be empty');
92+
if (tabId.length === 0) throw new Error('tabId cannot be empty');
93+
return value.init({ tabIds, value: mode, tabId });
94+
});
95+
96+
const service = useOmnibarService();
97+
98+
useEffect(() => {
99+
const sub1 = value.subscribe(() => {
100+
setState(value.getSnapshot());
101+
});
102+
const sub2 = service?.onConfig((evt) => {
103+
if (evt.source === 'manual') {
104+
value.dispatch({ kind: 'value changed', value: evt.data.mode });
105+
}
106+
if (evt.source === 'subscription' && Array.isArray(evt.data.tabIds) && evt.data.tabId) {
107+
value.dispatch({ kind: 'tabs changed', tabIds: evt.data.tabIds, tabId: evt.data.tabId });
108+
}
109+
});
110+
return () => {
111+
sub1();
112+
sub2?.();
113+
};
114+
}, [service, value]);
115+
116+
return <ModeContext.Provider value={state}>{children}</ModeContext.Provider>;
117+
}
118+
119+
export function useMode() {
120+
return useContext(ModeContext);
121+
}

special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.page.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ export class OmnibarPage {
102102
await expect(this.searchInput()).toHaveValue(value);
103103
}
104104

105+
/**
106+
* @param {string} value
107+
*/
108+
async expectChatValue(value) {
109+
await expect(this.chatInput()).toHaveValue(value);
110+
}
111+
105112
/**
106113
* @param {number} startIndex
107114
* @param {number} endIndex

special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.persistence.spec.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,23 @@ test.describe('omnibar widget persistence', () => {
2828
await omnibar.didSwitchToTab('02', ['01', '02']);
2929
await omnibar.expectInputValue('dresses');
3030
});
31+
test('remembers `mode` across tabs', async ({ page }, workerInfo) => {
32+
const ntp = NewtabPage.create(page, workerInfo);
33+
const omnibar = new OmnibarPage(ntp);
34+
await ntp.reducedMotion();
35+
await ntp.openPage({ additional: { omnibar: true, 'omnibar.tabs': true } });
36+
await omnibar.ready();
37+
38+
// first fill
39+
await omnibar.searchInput().fill('shoes');
40+
await page.getByRole('tab', { name: 'Duck.ai' }).click();
41+
42+
// new tab, should remain on duck.ai
43+
await omnibar.didSwitchToTab('02', ['01', '02']);
44+
await omnibar.expectChatValue('');
45+
46+
// switch back
47+
await omnibar.didSwitchToTab('01', ['01', '02']);
48+
await omnibar.expectChatValue('shoes');
49+
});
3150
});

special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,58 @@ import { TestTransportConfig } from '@duckduckgo/messaging';
22
import { getMockSuggestions } from './omnibar.mocks.js';
33

44
const url = typeof window !== 'undefined' ? new URL(window.location.href) : new URL('https://example.com');
5+
const VERSION_PREFIX = '__ntp.omnibar_1__.';
56

67
export function omnibarMockTransport() {
8+
let channel;
9+
10+
if (typeof globalThis.BroadcastChannel !== 'undefined') {
11+
channel = new BroadcastChannel('ntp.omnibar');
12+
}
13+
14+
function broadcast(named) {
15+
setTimeout(() => {
16+
channel?.postMessage({
17+
change: named,
18+
});
19+
}, 100);
20+
}
21+
22+
/**
23+
* @param {string} name
24+
* @return {any}
25+
*/
26+
function read(name) {
27+
// console.log('*will* read from LS', name)
28+
try {
29+
const item = localStorage.getItem(VERSION_PREFIX + name);
30+
if (!item) return null;
31+
// console.log('did read from LS', item)
32+
return JSON.parse(item);
33+
} catch (e) {
34+
console.error('Failed to parse initialSetup from localStorage', e);
35+
return null;
36+
}
37+
}
38+
39+
/**
40+
* @param {string} name
41+
* @param {Record<string, any>} value
42+
*/
43+
function write(name, value) {
44+
try {
45+
localStorage.setItem(VERSION_PREFIX + name, JSON.stringify(value));
46+
// console.log('✅ did write')
47+
} catch (e) {
48+
console.error('Failed to write', e);
49+
}
50+
}
51+
752
/** @type {import('../../../types/new-tab.ts').OmnibarConfig} */
8-
const config = {
53+
const defaultConfig = {
954
mode: 'search',
1055
};
1156

12-
/** @type {Map<string, (d: any) => void>} */
1357
const subs = new Map();
1458

1559
return new TestTransportConfig({
@@ -18,8 +62,11 @@ export function omnibarMockTransport() {
1862
const msg = /** @type {any} */ (_msg);
1963
switch (msg.method) {
2064
case 'omnibar_setConfig': {
21-
Object.assign(config, msg.params);
22-
subs.get('omnibar_onConfigUpdate')?.(config);
65+
const before = read('omnibar_config') || config(defaultConfig);
66+
const after = { ...before, ...msg.params };
67+
write('omnibar_config', after);
68+
subs.get('omnibar_onConfigUpdate')?.(after);
69+
broadcast('omnibar_config');
2370
break;
2471
}
2572
default: {
@@ -30,9 +77,39 @@ export function omnibarMockTransport() {
3077
subscribe(_msg, cb) {
3178
/** @type {import('../../../types/new-tab.ts').NewTabMessages['subscriptions']['subscriptionEvent']} */
3279
const sub = /** @type {any} */ (_msg.subscriptionName);
80+
subs.set(sub, cb);
3381
if (sub === 'omnibar_onConfigUpdate') {
34-
subs.set(sub, cb);
35-
return () => {};
82+
const controller = new AbortController();
83+
channel?.addEventListener(
84+
'message',
85+
(msg) => {
86+
if (msg.data.change === 'omnibar_config') {
87+
const values = read('omnibar_config');
88+
if (values) {
89+
subs?.get(sub)?.cb(values);
90+
}
91+
}
92+
},
93+
{ signal: controller.signal },
94+
);
95+
// @ts-expect-error - it's a fake API
96+
window._api = {
97+
add: (id) => {
98+
const next = config(read('omnibar_config') || defaultConfig);
99+
next.tabId = id;
100+
next.tabIds ??= [];
101+
next.tabIds.push(id);
102+
write('omnibar_config', next);
103+
cb(next);
104+
},
105+
switch: (id) => {
106+
const next = config(read('omnibar_config') || defaultConfig);
107+
next.tabId = id;
108+
write('omnibar_config', next);
109+
cb(next);
110+
},
111+
};
112+
return () => controller.abort();
36113
}
37114
console.warn('unhandled sub', sub);
38115
return () => {};
@@ -42,23 +119,30 @@ export function omnibarMockTransport() {
42119
const msg = /** @type {any} */ (_msg);
43120
switch (msg.method) {
44121
case 'omnibar_getConfig': {
122+
const current = (() => {
123+
const prev = read('omnibar_config');
124+
if (prev) return prev;
125+
const next = defaultConfig;
126+
return next;
127+
})();
45128
const modeOverride = url.searchParams.get('omnibar.mode');
46129
if (modeOverride === 'search' || modeOverride === 'ai') {
47-
config.mode = modeOverride;
130+
current.mode = modeOverride;
48131
}
49132
const enableAiOverride = url.searchParams.get('omnibar.enableAi');
50133
if (enableAiOverride === 'true' || enableAiOverride === 'false') {
51-
config.enableAi = enableAiOverride === 'true';
134+
current.enableAi = enableAiOverride === 'true';
52135
}
53136
const showAiSettingOverride = url.searchParams.get('omnibar.showAiSetting');
54137
if (showAiSettingOverride === 'true' || showAiSettingOverride === 'false') {
55-
config.showAiSetting = showAiSettingOverride === 'true';
138+
current.showAiSetting = showAiSettingOverride === 'true';
56139
}
57-
if (url.searchParams.has('omnibar.tabs')) {
58-
config.tabId = '01';
59-
config.tabIds = ['01'];
140+
if (url.searchParams.has('omnibar.tabs') && !current.tabId) {
141+
current.tabId = '01';
142+
current.tabIds = ['01'];
60143
}
61-
return config;
144+
write('omnibar_config', current);
145+
return current;
62146
}
63147
case 'omnibar_getSuggestions': {
64148
await new Promise((resolve) => setTimeout(resolve, 100)); // Simulate network delay
@@ -71,3 +155,10 @@ export function omnibarMockTransport() {
71155
},
72156
});
73157
}
158+
159+
/**
160+
* @param {import("../../../types/new-tab.js").OmnibarConfig} c
161+
*/
162+
function config(c) {
163+
return c;
164+
}

0 commit comments

Comments
 (0)