electrobun 0.0.19-beta.99 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api/browser/webviewtag.ts +54 -2
- package/dist/api/bun/ElectrobunConfig.ts +171 -0
- package/dist/api/bun/core/BrowserWindow.ts +4 -0
- package/dist/api/bun/core/Tray.ts +14 -0
- package/dist/api/bun/core/Updater.ts +4 -3
- package/dist/api/bun/index.ts +2 -0
- package/dist/api/bun/proc/native.ts +107 -5
- package/dist/main.js +5 -4
- package/package.json +4 -2
- package/src/cli/index.ts +621 -151
- package/templates/hello-world/bun.lock +164 -2
- package/templates/hello-world/electrobun.config.ts +28 -0
- package/templates/hello-world/src/bun/index.ts +2 -2
- package/templates/hello-world/src/mainview/index.html +5 -6
- package/templates/hello-world/src/mainview/index.ts +1 -5
- package/templates/interactive-playground/README.md +26 -0
- package/templates/interactive-playground/assets/tray-icon.png +0 -0
- package/templates/interactive-playground/electrobun.config.ts +36 -0
- package/templates/interactive-playground/package-lock.json +36 -0
- package/templates/interactive-playground/package.json +15 -0
- package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
- package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
- package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
- package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
- package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
- package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
- package/templates/interactive-playground/src/bun/index.ts +124 -0
- package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
- package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
- package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
- package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
- package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
- package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
- package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
- package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
- package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
- package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
- package/templates/interactive-playground/src/mainview/index.css +538 -0
- package/templates/interactive-playground/src/mainview/index.html +103 -0
- package/templates/interactive-playground/src/mainview/index.ts +238 -0
- package/templates/multitab-browser/README.md +34 -0
- package/templates/multitab-browser/bun.lock +224 -0
- package/templates/multitab-browser/electrobun.config.ts +32 -0
- package/templates/multitab-browser/package-lock.json +20 -0
- package/templates/multitab-browser/package.json +12 -0
- package/templates/multitab-browser/src/bun/index.ts +144 -0
- package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
- package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
- package/templates/multitab-browser/src/mainview/index.css +487 -0
- package/templates/multitab-browser/src/mainview/index.html +94 -0
- package/templates/multitab-browser/src/mainview/index.ts +634 -0
- package/templates/photo-booth/README.md +108 -0
- package/templates/photo-booth/bun.lock +239 -0
- package/templates/photo-booth/electrobun.config.ts +32 -0
- package/templates/photo-booth/package.json +17 -0
- package/templates/photo-booth/src/bun/index.ts +92 -0
- package/templates/photo-booth/src/mainview/index.css +465 -0
- package/templates/photo-booth/src/mainview/index.html +124 -0
- package/templates/photo-booth/src/mainview/index.ts +499 -0
- package/tests/bun.lock +14 -0
- package/tests/electrobun.config.ts +45 -0
- package/tests/package-lock.json +36 -0
- package/tests/package.json +13 -0
- package/tests/src/bun/index.ts +100 -0
- package/tests/src/bun/test-runner.ts +508 -0
- package/tests/src/mainview/index.html +110 -0
- package/tests/src/mainview/index.ts +458 -0
- package/tests/src/mainview/styles/main.css +451 -0
- package/tests/src/testviews/tray-test.html +57 -0
- package/tests/src/testviews/webview-mask.html +114 -0
- package/tests/src/testviews/webview-navigation.html +36 -0
- package/tests/src/testviews/window-create.html +17 -0
- package/tests/src/testviews/window-events.html +29 -0
- package/tests/src/testviews/window-focus.html +37 -0
- package/tests/src/webviewtag/index.ts +11 -0
- package/templates/hello-world/electrobun.config +0 -18
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import Electrobun, { BrowserWindow, BrowserView } from "electrobun/bun";
|
|
2
|
+
|
|
3
|
+
console.log("🌐 Multitab Browser starting...");
|
|
4
|
+
|
|
5
|
+
// Simplified tab management - demo without real webviews
|
|
6
|
+
const tabs = new Map();
|
|
7
|
+
let nextTabId = 1;
|
|
8
|
+
let mainRPC: any = null; // Will be set after window creation
|
|
9
|
+
|
|
10
|
+
// Set up RPC using the correct API pattern from interactive-playground
|
|
11
|
+
const rpc = BrowserView.defineRPC({
|
|
12
|
+
maxRequestTime: 10000,
|
|
13
|
+
handlers: {
|
|
14
|
+
requests: {
|
|
15
|
+
createTab: async ({ url }: { url?: string }) => {
|
|
16
|
+
const id = `tab-${nextTabId++}`;
|
|
17
|
+
|
|
18
|
+
const tab = {
|
|
19
|
+
id,
|
|
20
|
+
title: "New Tab",
|
|
21
|
+
url: url || "https://electrobun.dev",
|
|
22
|
+
canGoBack: false,
|
|
23
|
+
canGoForward: false,
|
|
24
|
+
isLoading: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
tabs.set(id, tab);
|
|
28
|
+
|
|
29
|
+
// Simulate getting the title after a delay
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
if (mainRPC) {
|
|
32
|
+
tab.title = new URL(tab.url).hostname || "New Tab";
|
|
33
|
+
mainRPC.send("tabUpdated", tab);
|
|
34
|
+
}
|
|
35
|
+
}, 500);
|
|
36
|
+
|
|
37
|
+
return tab;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
closeTab: async ({ id }: { id: string }) => {
|
|
41
|
+
tabs.delete(id);
|
|
42
|
+
return;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
activateTab: async ({ tabId }: { tabId: string }) => {
|
|
46
|
+
// Just return the tab info - the iframe switching happens in the frontend
|
|
47
|
+
return tabs.get(tabId);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
navigateTo: async ({ tabId, url }: { tabId: string; url: string }) => {
|
|
51
|
+
const tab = tabs.get(tabId);
|
|
52
|
+
|
|
53
|
+
if (tab) {
|
|
54
|
+
// Process URL - add https if needed, or search
|
|
55
|
+
let processedUrl = url;
|
|
56
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
57
|
+
if (url.includes(".") && !url.includes(" ")) {
|
|
58
|
+
processedUrl = `https://${url}`;
|
|
59
|
+
} else {
|
|
60
|
+
processedUrl = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
tab.url = processedUrl;
|
|
65
|
+
tab.isLoading = false;
|
|
66
|
+
|
|
67
|
+
// Simulate title update
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
if (mainRPC && tab) {
|
|
70
|
+
try {
|
|
71
|
+
tab.title = new URL(processedUrl).hostname || "New Tab";
|
|
72
|
+
} catch {
|
|
73
|
+
tab.title = processedUrl;
|
|
74
|
+
}
|
|
75
|
+
mainRPC.send("tabUpdated", tab);
|
|
76
|
+
}
|
|
77
|
+
}, 500);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return tab;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
goBack: async ({ tabId }: { tabId: string }) => {
|
|
84
|
+
// In a real implementation, we'd track history
|
|
85
|
+
console.log("Go back for tab:", tabId);
|
|
86
|
+
return tabs.get(tabId);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
goForward: async ({ tabId }: { tabId: string }) => {
|
|
90
|
+
// In a real implementation, we'd track history
|
|
91
|
+
console.log("Go forward for tab:", tabId);
|
|
92
|
+
return tabs.get(tabId);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
reload: async ({ tabId }: { tabId: string }) => {
|
|
96
|
+
const tab = tabs.get(tabId);
|
|
97
|
+
if (tab) {
|
|
98
|
+
tab.isLoading = true;
|
|
99
|
+
if (mainRPC) {
|
|
100
|
+
mainRPC.send("tabUpdated", tab);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
tab.isLoading = false;
|
|
105
|
+
if (mainRPC) {
|
|
106
|
+
mainRPC.send("tabUpdated", tab);
|
|
107
|
+
}
|
|
108
|
+
}, 1000);
|
|
109
|
+
}
|
|
110
|
+
return tab;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
messages: {
|
|
114
|
+
"*": (messageName: string, payload: any) => {
|
|
115
|
+
console.log(`📨 Browser message: ${messageName}`, payload);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Create main browser window with RPC
|
|
122
|
+
const mainWindow = new BrowserWindow({
|
|
123
|
+
title: "Multitab Browser",
|
|
124
|
+
url: "views://mainview/index.html",
|
|
125
|
+
frame: {
|
|
126
|
+
width: 1400,
|
|
127
|
+
height: 900,
|
|
128
|
+
x: 100,
|
|
129
|
+
y: 100,
|
|
130
|
+
},
|
|
131
|
+
rpc,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Store reference to mainWindow RPC for sending messages
|
|
135
|
+
mainRPC = mainWindow.webview.rpc;
|
|
136
|
+
|
|
137
|
+
// Listen for window close event and exit the app
|
|
138
|
+
// For this browser app, we want to exit when the main window is closed
|
|
139
|
+
mainWindow.on("close", () => {
|
|
140
|
+
console.log("🚪 Main window closed - exiting app");
|
|
141
|
+
process.exit(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
console.log("✅ Multitab Browser initialized");
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { BrowserView } from "electrobun/bun";
|
|
2
|
+
import { type Tab, type Bookmark } from "./types/rpc";
|
|
3
|
+
|
|
4
|
+
export class TabManager {
|
|
5
|
+
private tabs: Map<string, Tab> = new Map();
|
|
6
|
+
private webviews: Map<string, BrowserView> = new Map();
|
|
7
|
+
private bookmarks: Map<string, Bookmark> = new Map();
|
|
8
|
+
private nextTabId = 1;
|
|
9
|
+
|
|
10
|
+
public onTabUpdate?: (tab: Tab) => void;
|
|
11
|
+
public onLoadingStateChange?: (tabId: string, isLoading: boolean) => void;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
// Load bookmarks from storage if available
|
|
15
|
+
this.loadBookmarks();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async createTab(url: string): Promise<Tab> {
|
|
19
|
+
const id = `tab-${this.nextTabId++}`;
|
|
20
|
+
|
|
21
|
+
// Create a BrowserView for this tab
|
|
22
|
+
const webview = new BrowserView({
|
|
23
|
+
url,
|
|
24
|
+
frame: {
|
|
25
|
+
x: 0,
|
|
26
|
+
y: 100, // Leave space for tab bar and navigation
|
|
27
|
+
width: 1400,
|
|
28
|
+
height: 800,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Set up webview event handlers
|
|
33
|
+
webview.on("page-title-updated", (event) => {
|
|
34
|
+
const tab = this.tabs.get(id);
|
|
35
|
+
if (tab) {
|
|
36
|
+
tab.title = event.data.title || "New Tab";
|
|
37
|
+
this.onTabUpdate?.(tab);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
webview.on("did-start-loading", () => {
|
|
42
|
+
const tab = this.tabs.get(id);
|
|
43
|
+
if (tab) {
|
|
44
|
+
tab.isLoading = true;
|
|
45
|
+
this.onLoadingStateChange?.(id, true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
webview.on("did-stop-loading", () => {
|
|
50
|
+
const tab = this.tabs.get(id);
|
|
51
|
+
if (tab) {
|
|
52
|
+
tab.isLoading = false;
|
|
53
|
+
this.onLoadingStateChange?.(id, false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
webview.on("did-navigate", (event) => {
|
|
58
|
+
const tab = this.tabs.get(id);
|
|
59
|
+
if (tab && event.data.url) {
|
|
60
|
+
tab.url = event.data.url;
|
|
61
|
+
// Update navigation state
|
|
62
|
+
this.updateNavigationState(id);
|
|
63
|
+
this.onTabUpdate?.(tab);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const tab: Tab = {
|
|
68
|
+
id,
|
|
69
|
+
title: "New Tab",
|
|
70
|
+
url,
|
|
71
|
+
canGoBack: false,
|
|
72
|
+
canGoForward: false,
|
|
73
|
+
isLoading: true,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this.tabs.set(id, tab);
|
|
77
|
+
this.webviews.set(id, webview);
|
|
78
|
+
|
|
79
|
+
return tab;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async closeTab(id: string): Promise<void> {
|
|
83
|
+
const webview = this.webviews.get(id);
|
|
84
|
+
if (webview) {
|
|
85
|
+
webview.destroy();
|
|
86
|
+
this.webviews.delete(id);
|
|
87
|
+
}
|
|
88
|
+
this.tabs.delete(id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async navigateTo(tabId: string, url: string): Promise<void> {
|
|
92
|
+
const webview = this.webviews.get(tabId);
|
|
93
|
+
if (webview) {
|
|
94
|
+
// Ensure URL has protocol
|
|
95
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
96
|
+
// Check if it looks like a domain
|
|
97
|
+
if (url.includes(".") && !url.includes(" ")) {
|
|
98
|
+
url = `https://${url}`;
|
|
99
|
+
} else {
|
|
100
|
+
// Treat as search query
|
|
101
|
+
url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await webview.loadURL(url);
|
|
106
|
+
const tab = this.tabs.get(tabId);
|
|
107
|
+
if (tab) {
|
|
108
|
+
tab.url = url;
|
|
109
|
+
tab.isLoading = true;
|
|
110
|
+
this.onTabUpdate?.(tab);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async goBack(tabId: string): Promise<void> {
|
|
116
|
+
const webview = this.webviews.get(tabId);
|
|
117
|
+
if (webview) {
|
|
118
|
+
await webview.goBack();
|
|
119
|
+
this.updateNavigationState(tabId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async goForward(tabId: string): Promise<void> {
|
|
124
|
+
const webview = this.webviews.get(tabId);
|
|
125
|
+
if (webview) {
|
|
126
|
+
await webview.goForward();
|
|
127
|
+
this.updateNavigationState(tabId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async reload(tabId: string): Promise<void> {
|
|
132
|
+
const webview = this.webviews.get(tabId);
|
|
133
|
+
if (webview) {
|
|
134
|
+
await webview.reload();
|
|
135
|
+
const tab = this.tabs.get(tabId);
|
|
136
|
+
if (tab) {
|
|
137
|
+
tab.isLoading = true;
|
|
138
|
+
this.onTabUpdate?.(tab);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async getTabInfo(tabId: string): Promise<Tab | undefined> {
|
|
144
|
+
return this.tabs.get(tabId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getAllTabs(): Tab[] {
|
|
148
|
+
return Array.from(this.tabs.values());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async updateNavigationState(tabId: string): Promise<void> {
|
|
152
|
+
const webview = this.webviews.get(tabId);
|
|
153
|
+
const tab = this.tabs.get(tabId);
|
|
154
|
+
|
|
155
|
+
if (webview && tab) {
|
|
156
|
+
// Note: These methods might not be available in current Electrobun API
|
|
157
|
+
// This is a placeholder for navigation state management
|
|
158
|
+
tab.canGoBack = false; // Would need webview.canGoBack()
|
|
159
|
+
tab.canGoForward = false; // Would need webview.canGoForward()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Bookmark management
|
|
164
|
+
addBookmark(title: string, url: string): Bookmark {
|
|
165
|
+
const id = `bookmark-${Date.now()}`;
|
|
166
|
+
const bookmark: Bookmark = {
|
|
167
|
+
id,
|
|
168
|
+
title,
|
|
169
|
+
url,
|
|
170
|
+
createdAt: Date.now(),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
this.bookmarks.set(id, bookmark);
|
|
174
|
+
this.saveBookmarks();
|
|
175
|
+
return bookmark;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getBookmarks(): Bookmark[] {
|
|
179
|
+
return Array.from(this.bookmarks.values()).sort((a, b) => b.createdAt - a.createdAt);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
removeBookmark(id: string): void {
|
|
183
|
+
this.bookmarks.delete(id);
|
|
184
|
+
this.saveBookmarks();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private loadBookmarks(): void {
|
|
188
|
+
// In a real app, load from persistent storage
|
|
189
|
+
// For demo, we'll start with some default bookmarks
|
|
190
|
+
this.addBookmark("Google", "https://www.google.com");
|
|
191
|
+
this.addBookmark("GitHub", "https://github.com");
|
|
192
|
+
this.addBookmark("Electrobun", "https://electrobun.dev");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private saveBookmarks(): void {
|
|
196
|
+
// In a real app, save to persistent storage
|
|
197
|
+
// For demo, we'll just log
|
|
198
|
+
console.log("Bookmarks saved:", this.getBookmarks());
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { RPCSchema } from "electrobun/bun";
|
|
2
|
+
|
|
3
|
+
export type Tab = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
url: string;
|
|
7
|
+
canGoBack: boolean;
|
|
8
|
+
canGoForward: boolean;
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
favicon?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Bookmark = {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
url: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type BrowserRPC = {
|
|
21
|
+
bun: RPCSchema<{
|
|
22
|
+
requests: {
|
|
23
|
+
createTab: {
|
|
24
|
+
params: { url?: string };
|
|
25
|
+
response: Tab;
|
|
26
|
+
};
|
|
27
|
+
closeTab: {
|
|
28
|
+
params: { id: string };
|
|
29
|
+
response: void;
|
|
30
|
+
};
|
|
31
|
+
navigateTo: {
|
|
32
|
+
params: { tabId: string; url: string };
|
|
33
|
+
response: void;
|
|
34
|
+
};
|
|
35
|
+
goBack: {
|
|
36
|
+
params: { tabId: string };
|
|
37
|
+
response: void;
|
|
38
|
+
};
|
|
39
|
+
goForward: {
|
|
40
|
+
params: { tabId: string };
|
|
41
|
+
response: void;
|
|
42
|
+
};
|
|
43
|
+
reload: {
|
|
44
|
+
params: { tabId: string };
|
|
45
|
+
response: void;
|
|
46
|
+
};
|
|
47
|
+
getTabInfo: {
|
|
48
|
+
params: { tabId: string };
|
|
49
|
+
response: Tab;
|
|
50
|
+
};
|
|
51
|
+
getAllTabs: {
|
|
52
|
+
params: {};
|
|
53
|
+
response: Tab[];
|
|
54
|
+
};
|
|
55
|
+
addBookmark: {
|
|
56
|
+
params: { title: string; url: string };
|
|
57
|
+
response: Bookmark;
|
|
58
|
+
};
|
|
59
|
+
getBookmarks: {
|
|
60
|
+
params: {};
|
|
61
|
+
response: Bookmark[];
|
|
62
|
+
};
|
|
63
|
+
removeBookmark: {
|
|
64
|
+
params: { id: string };
|
|
65
|
+
response: void;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
messages: {
|
|
69
|
+
tabUpdated: Tab;
|
|
70
|
+
tabClosed: { id: string };
|
|
71
|
+
loadingStateChanged: { tabId: string; isLoading: boolean };
|
|
72
|
+
};
|
|
73
|
+
}>;
|
|
74
|
+
webview: RPCSchema<{
|
|
75
|
+
requests: {};
|
|
76
|
+
messages: {};
|
|
77
|
+
}>;
|
|
78
|
+
};
|