Skip to content

Commit 0115447

Browse files
committed
Merge branch 'main' of github.com:valory-xyz/olas-operate-app
2 parents 9f30e38 + 02b474d commit 0115447

12 files changed

+195
-17
lines changed

electron/main.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ let tray,
6060
nextAppProcess,
6161
nextAppProcessPid;
6262

63+
function showNotification(title, body) {
64+
new Notification({ title, body }).show();
65+
}
66+
6367
async function beforeQuit() {
6468
if (operateDaemonPid) {
6569
try {
@@ -220,12 +224,8 @@ const createMainWindow = () => {
220224
mainWindow.setSize(width, height);
221225
});
222226

223-
ipcMain.on('notify-agent-running', () => {
224-
if (!mainWindow.isVisible()) {
225-
new Notification({
226-
title: 'Your agent is now running!',
227-
}).show();
228-
}
227+
ipcMain.on('show-notification', (title, description) => {
228+
showNotification(title, description || undefined);
229229
});
230230

231231
mainWindow.webContents.on('did-fail-load', () => {

electron/preload.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
55
closeApp: () => ipcRenderer.send('close-app'),
66
minimizeApp: () => ipcRenderer.send('minimize-app'),
77
setAppHeight: (height) => ipcRenderer.send('set-height', height),
8-
notifyAgentRunning: () => ipcRenderer.send('notify-agent-running'),
8+
showNotification: (title, description) =>
9+
ipcRenderer.send('show-notification', title, description),
910
});

frontend/components/Main/MainHeader.tsx

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { InfoCircleOutlined } from '@ant-design/icons';
22
import { Badge, Button, Flex, Popover, Typography } from 'antd';
33
import { formatUnits } from 'ethers/lib/utils';
4-
import get from 'lodash/get';
54
import Image from 'next/image';
65
import { useCallback, useEffect, useMemo, useState } from 'react';
76

87
import { Chain, DeploymentStatus } from '@/client';
98
import { setTrayIcon } from '@/common-util';
109
import { COLOR, LOW_BALANCE, SERVICE_TEMPLATES } from '@/constants';
1110
import { useBalance, useServiceTemplates } from '@/hooks';
11+
import { useElectronApi } from '@/hooks/useElectronApi';
1212
import { useServices } from '@/hooks/useServices';
1313
import { useWallet } from '@/hooks/useWallet';
1414
import { ServicesService } from '@/service';
@@ -25,13 +25,9 @@ enum ServiceButtonLoadingState {
2525
NotLoading,
2626
}
2727

28-
const notifyAgentRunning = () => {
29-
const fn = get(window, 'electronAPI.notifyAgentRunning') ?? (() => null);
30-
return fn();
31-
};
32-
3328
export const MainHeader = () => {
3429
const { services, serviceStatus, setServiceStatus } = useServices();
30+
const { showNotification } = useElectronApi();
3531
const { getServiceTemplates } = useServiceTemplates();
3632
const { wallets, masterSafeAddress } = useWallet();
3733
const {
@@ -118,7 +114,7 @@ export const MainHeader = () => {
118114
setServiceStatus(DeploymentStatus.DEPLOYED);
119115
setIsBalancePollingPaused(false);
120116
setServiceButtonState(ServiceButtonLoadingState.NotLoading);
121-
notifyAgentRunning();
117+
showNotification?.('Your agent is now running!');
122118
});
123119
} catch (error) {
124120
setIsBalancePollingPaused(false);
@@ -130,6 +126,7 @@ export const MainHeader = () => {
130126
setIsBalancePollingPaused,
131127
setServiceStatus,
132128
wallets,
129+
showNotification,
133130
]);
134131

135132
const handlePause = useCallback(() => {

frontend/components/Main/MainRewards.tsx

+105-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { Col, Flex, Row, Skeleton, Tag, Typography } from 'antd';
1+
import { Button, Col, Flex, Modal, Row, Skeleton, Tag, Typography } from 'antd';
2+
import Image from 'next/image';
3+
import { useCallback, useEffect, useState } from 'react';
24
import styled from 'styled-components';
35

46
import { balanceFormat } from '@/common-util';
57
import { COLOR } from '@/constants';
68
import { useBalance } from '@/hooks';
9+
import { useElectronApi } from '@/hooks/useElectronApi';
710
import { useReward } from '@/hooks/useReward';
811

9-
const { Text } = Typography;
12+
import { ConfettiAnimation } from '../common/ConfettiAnimation';
13+
14+
const { Text, Title } = Typography;
1015

1116
const RewardsRow = styled(Row)`
1217
margin: 0 -24px;
@@ -70,8 +75,106 @@ const DisplayRewards = () => {
7075
);
7176
};
7277

78+
const NotifyRewards = () => {
79+
const { isEligibleForRewards, availableRewardsForEpochEth } = useReward();
80+
const { totalOlasBalance } = useBalance();
81+
const { showNotification } = useElectronApi();
82+
83+
const [canShowNotification, setCanShowNotification] = useState(false);
84+
85+
useEffect(() => {
86+
// TODO: Implement this once state persistence is available
87+
const hasAlreadyNotified = true;
88+
89+
if (!isEligibleForRewards) return;
90+
if (hasAlreadyNotified) return;
91+
if (!availableRewardsForEpochEth) return;
92+
93+
setCanShowNotification(true);
94+
}, [isEligibleForRewards, availableRewardsForEpochEth, showNotification]);
95+
96+
// hook to show app notification
97+
useEffect(() => {
98+
if (!canShowNotification) return;
99+
100+
showNotification?.(
101+
'Your agent earned its first staking rewards!',
102+
`Congratulations! Your agent just got the first reward for you! Your current balance: ${availableRewardsForEpochEth} OLAS`,
103+
);
104+
}, [canShowNotification, availableRewardsForEpochEth, showNotification]);
105+
106+
const closeNotificationModal = useCallback(() => {
107+
setCanShowNotification(false);
108+
// TODO: add setter for hasAlreadyNotified
109+
}, []);
110+
111+
if (!canShowNotification) return null;
112+
113+
return (
114+
<Modal
115+
open={canShowNotification}
116+
width={400}
117+
onCancel={closeNotificationModal}
118+
footer={[
119+
<Button
120+
key="back"
121+
type="primary"
122+
block
123+
size="large"
124+
className="mt-8"
125+
disabled
126+
// TODO: add twitter share functionality
127+
>
128+
<Flex align="center" justify="center" gap={2}>
129+
Share on
130+
<Image
131+
src="/twitter.svg"
132+
width={24}
133+
height={24}
134+
alt="Share on twitter"
135+
/>
136+
</Flex>
137+
</Button>,
138+
]}
139+
>
140+
<ConfettiAnimation />
141+
142+
<Flex align="center" justify="center">
143+
<Image
144+
src="/splash-robot-head.png"
145+
width={100}
146+
height={100}
147+
alt="OLAS logo"
148+
/>
149+
</Flex>
150+
151+
<Title level={5} className="mt-12">
152+
Your agent just earned the first reward!
153+
</Title>
154+
155+
<Flex vertical gap={16}>
156+
<Text>
157+
Congratulations! Your agent just earned the first
158+
<Text strong>
159+
{` ${balanceFormat(availableRewardsForEpochEth, 2)} OLAS `}
160+
</Text>
161+
for you!
162+
</Text>
163+
164+
<Text>
165+
Your current balance:
166+
<Text strong>{` ${balanceFormat(totalOlasBalance, 2)} OLAS `}</Text>
167+
</Text>
168+
169+
<Text>Keep it running to get even more!</Text>
170+
</Flex>
171+
</Modal>
172+
);
173+
};
174+
73175
export const MainRewards = () => (
74176
<>
75177
<DisplayRewards />
178+
<NotifyRewards />
76179
</>
77180
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useCallback, useRef } from 'react';
2+
import ReactCanvasConfetti from 'react-canvas-confetti';
3+
import { useInterval } from 'usehooks-ts';
4+
5+
const canvasStyles = {
6+
position: 'fixed',
7+
pointerEvents: 'none',
8+
width: '100%',
9+
height: '100%',
10+
top: 0,
11+
left: 0,
12+
};
13+
14+
export const ConfettiAnimation = () => {
15+
const animationInstance = useRef(null);
16+
17+
const makeShot = useCallback((particleRatio, opts) => {
18+
if (!animationInstance.current) return;
19+
20+
animationInstance.current({
21+
...opts,
22+
origin: { y: 0.45 },
23+
particleCount: Math.floor(200 * particleRatio),
24+
});
25+
}, []);
26+
27+
const fire = useCallback(() => {
28+
makeShot(0.25, { spread: 26, startVelocity: 55 });
29+
makeShot(0.2, { spread: 60 });
30+
makeShot(0.35, { spread: 80, decay: 0.91, scalar: 0.8 });
31+
makeShot(0.1, { spread: 100, startVelocity: 25, decay: 0.92, scalar: 1.2 });
32+
makeShot(0.1, { spread: 100, startVelocity: 45 });
33+
}, [makeShot]);
34+
35+
const getInstance = useCallback((instance) => {
36+
animationInstance.current = instance;
37+
}, []);
38+
39+
// Fire confetti every 2.5 seconds
40+
useInterval(() => fire(), 2500);
41+
42+
return <ReactCanvasConfetti refConfetti={getInstance} style={canvasStyles} />;
43+
};

frontend/context/ElectronApiProvider.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ type ElectronApiContextProps = {
55
setAppHeight?: (height: number) => void;
66
closeApp?: () => void;
77
minimizeApp?: () => void;
8+
showNotification?: (title: string, body?: string) => void;
89
};
910

1011
export const ElectronApiContext = createContext<ElectronApiContextProps>({
1112
setAppHeight: undefined,
1213
closeApp: undefined,
1314
minimizeApp: undefined,
15+
showNotification: undefined,
1416
});
1517

1618
const getElectronApiFunction = (functionNameInWindow: string) => {
@@ -33,6 +35,7 @@ export const ElectronApiProvider = ({ children }: PropsWithChildren) => {
3335
setAppHeight: getElectronApiFunction('setAppHeight'),
3436
closeApp: getElectronApiFunction('closeApp'),
3537
minimizeApp: getElectronApiFunction('minimizeApp'),
38+
showNotification: getElectronApiFunction('showNotification'),
3639
}}
3740
>
3841
{children}

frontend/hooks/useElectronApi.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { useContext } from 'react';
33
import { ElectronApiContext } from '@/context/ElectronApiProvider';
44

55
export const useElectronApi = () => {
6-
const { setAppHeight, closeApp, minimizeApp } =
6+
const { setAppHeight, closeApp, minimizeApp, showNotification } =
77
useContext(ElectronApiContext);
88

99
return {
1010
setAppHeight,
1111
closeApp,
1212
minimizeApp,
13+
showNotification,
1314
};
1415
};

frontend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"next": "^14.2.3",
1212
"react": "^18",
1313
"react-dom": "^18",
14+
"react-canvas-confetti": "1.2.1",
1415
"sass": "^1.72.0",
1516
"styled-components": "^6.1.8",
1617
"usehooks-ts": "^2.14.0"

frontend/public/splash-robot-head.png

312 KB
Loading

frontend/public/twitter.svg

+3
Loading

frontend/styles/globals.scss

+8
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ button, input, select, textarea, .ant-input-suffix {
8585
margin-bottom: auto !important;
8686
}
8787

88+
.mt-8 {
89+
margin-top: 8px !important;
90+
}
91+
92+
.mt-12 {
93+
margin-top: 12px !important;
94+
}
95+
8896
.mx-auto {
8997
margin-left: auto !important;
9098
margin-right: auto !important;

frontend/yarn.lock

+18
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,11 @@
13491349
dependencies:
13501350
"@babel/types" "^7.20.7"
13511351

1352+
1353+
version "1.4.0"
1354+
resolved "https://registry.yarnpkg.com/@types/canvas-confetti/-/canvas-confetti-1.4.0.tgz#22127a1a9ed9d456e626d6e2b9a4d3b0a240e18b"
1355+
integrity sha512-Neq4mvVecrHmTdyo98EY5bnKCjkZGQ6Ma7VyOrxIcMHEZPmt4kfquccqfBMrpNrdryMHgk3oGQi7XtpZacltnw==
1356+
13521357
"@types/graceful-fs@^4.1.3":
13531358
version "4.1.9"
13541359
resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz"
@@ -2152,6 +2157,11 @@ caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587:
21522157
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz"
21532158
integrity sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==
21542159

2160+
2161+
version "1.4.0"
2162+
resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.4.0.tgz#840f6db4a566f8f32abe28c00dcd82acf39c92bd"
2163+
integrity sha512-S18o4Y9PqI/uabdlT/jI3MY7XBJjNxnfapFIkjkMwpz6qNxLFZOm2b22OMf4ZYDL9lpNWI+Ih4fEMVPwO1KHFQ==
2164+
21552165
chalk@^2.4.2:
21562166
version "2.4.2"
21572167
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
@@ -5209,6 +5219,14 @@ rc-virtual-list@^3.11.1, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
52095219
rc-resize-observer "^1.0.0"
52105220
rc-util "^5.36.0"
52115221

5222+
5223+
version "1.2.1"
5224+
resolved "https://registry.yarnpkg.com/react-canvas-confetti/-/react-canvas-confetti-1.2.1.tgz#22ac64cbc478cf57cb5f61c130322e359b35389d"
5225+
integrity sha512-onNjQNkhQjrB2JVCeRpJW7o8VlBNJvo3Eht9zlQlofNLVvvOd1+PzBLU5Ev8n5sFBkTbKJmIUVG0eZnkAl5cpg==
5226+
dependencies:
5227+
"@types/canvas-confetti" "1.4.0"
5228+
canvas-confetti "1.4.0"
5229+
52125230
react-dom@^18:
52135231
version "18.2.0"
52145232
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"

0 commit comments

Comments
 (0)