Skip to content

Commit 5f6042e

Browse files
committed
Merge branch 'main' into feat/rebuild-mute-unmute-rooms
2 parents 7f65c1f + c02bbd6 commit 5f6042e

File tree

7 files changed

+247
-21
lines changed

7 files changed

+247
-21
lines changed
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Increment Version
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version_increment:
7+
description: 'Version number to increment'
8+
required: true
9+
default: 'patch'
10+
type: 'choice'
11+
options:
12+
- patch
13+
- minor
14+
- major
15+
16+
jobs:
17+
increment-version:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v3
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Setup Node.js
26+
uses: actions/setup-node@v3
27+
with:
28+
node-version: '18'
29+
cache: 'npm'
30+
31+
- name: Configure Git
32+
run: |
33+
git config user.name "${{ github.actor }}"
34+
git config user.email "${{ github.actor }}@users.noreply.github.com"
35+
echo "Configured Git as user: ${{ github.actor }}"
36+
37+
- name: Increment version
38+
id: version
39+
run: |
40+
# Get current version
41+
CURRENT_VERSION=$(node -p "require('./package.json').version")
42+
echo "Current version: $CURRENT_VERSION"
43+
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
44+
45+
# Increment version based on input
46+
npm version ${{ github.event.inputs.version_increment }} --no-git-tag-version
47+
48+
# Get new version
49+
NEW_VERSION=$(node -p "require('./package.json').version")
50+
echo "New version: $NEW_VERSION"
51+
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
52+
53+
# Create branch name
54+
BRANCH_NAME="version-bump-$NEW_VERSION"
55+
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
56+
57+
- name: Create Pull Request
58+
uses: peter-evans/create-pull-request@v5
59+
with:
60+
token: ${{ secrets.GITHUB_TOKEN }}
61+
commit-message: 'chore: bump ${{ github.event.inputs.version_increment }} (`${{ steps.version.outputs.current_version }}` -> `${{ steps.version.outputs.new_version }}`)'
62+
title: 'chore: bump ${{ github.event.inputs.version_increment }} (`${{ steps.version.outputs.current_version }}` -> `${{ steps.version.outputs.new_version }}`)'
63+
body: |
64+
This PR bumps the version from `${{ steps.version.outputs.current_version }}` to `${{ steps.version.outputs.new_version }}`.
65+
66+
Automated version increment via GitHub Actions.
67+
branch: ${{ steps.version.outputs.branch_name }}
68+
base: main
69+
delete-branch: true

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ZERO",
3-
"version": "1.283.0",
3+
"version": "1.286.0",
44
"private": true,
55
"main": "./public/electron.js",
66
"engines": {

src/components/app-bar/index.tsx

+82-6
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,59 @@ interface State {
2626

2727
export class AppBar extends React.Component<Properties, State> {
2828
state = { isModalOpen: false };
29+
containerRef = React.createRef<HTMLDivElement>();
30+
mouseLeaveHandler: ((event: MouseEvent) => void) | null = null;
31+
32+
componentWillUnmount() {
33+
// Remove listener to prevent memory leaks
34+
this.removeMouseLeaveListener();
35+
}
2936

3037
openModal = () => this.setState({ isModalOpen: true });
3138
closeModal = () => this.setState({ isModalOpen: false });
3239

40+
/**
41+
* Removes the mouseleave listener from the container.
42+
* @dev mouse listener needs to be removed so we don't add multiple listeners.
43+
*/
44+
removeMouseLeaveListener = () => {
45+
if (this.containerRef.current && this.mouseLeaveHandler) {
46+
this.containerRef.current.removeEventListener('mouseleave', this.mouseLeaveHandler);
47+
this.mouseLeaveHandler = null;
48+
}
49+
};
50+
51+
/**
52+
* Unhovers the container and prevents another hover occurring until the mouse
53+
* leaves the container.
54+
*
55+
* This method:
56+
* 1. Adds the 'no-hover' class to prevent expansion
57+
* 2. Forces a reflow to ensure immediate width change
58+
* 3. Sets up a one-time mouseleave listener to restore hover functionality
59+
*/
60+
unhoverContainer = () => {
61+
const container = this.containerRef.current;
62+
if (!container) return;
63+
64+
// Add the no-hover class to prevent expansion
65+
container.classList.add('no-hover');
66+
67+
// Force a reflow to ensure the width change happens immediately (prevent animation glitches)
68+
container.getBoundingClientRect();
69+
70+
this.removeMouseLeaveListener();
71+
72+
this.mouseLeaveHandler = () => {
73+
if (this.containerRef.current) {
74+
this.containerRef.current.classList.remove('no-hover');
75+
this.removeMouseLeaveListener();
76+
}
77+
};
78+
79+
container.addEventListener('mouseleave', this.mouseLeaveHandler);
80+
};
81+
3382
renderNotificationIcon = () => {
3483
const { hasUnreadNotifications, hasUnreadHighlights } = this.props;
3584

@@ -53,26 +102,46 @@ export class AppBar extends React.Component<Properties, State> {
53102
return (
54103
<>
55104
<div {...cn('')}>
56-
<LegacyPanel {...cn('container')}>
57-
<AppLink Icon={IconHome} isActive={isActive('home')} label='Home' to='/home' />
105+
<LegacyPanel {...cn('container')} ref={this.containerRef}>
106+
<AppLink
107+
Icon={IconHome}
108+
isActive={isActive('home')}
109+
label='Home'
110+
to='/home'
111+
onLinkClick={this.unhoverContainer}
112+
/>
58113
<AppLink
59114
Icon={IconMessageSquare2}
60115
isActive={isActive('conversation')}
61116
label='Messenger'
62117
to='/conversation'
118+
onLinkClick={this.unhoverContainer}
63119
/>
64120
{featureFlags.enableFeedApp && (
65-
<AppLink Icon={IconSlashes} isActive={isActive('feed')} label='Channels' to='/feed' />
121+
<AppLink
122+
Icon={IconSlashes}
123+
isActive={isActive('feed')}
124+
label='Channels'
125+
to='/feed'
126+
onLinkClick={this.unhoverContainer}
127+
/>
66128
)}
67129
{featureFlags.enableNotificationsApp && (
68130
<AppLink
69131
Icon={this.renderNotificationIcon}
70132
isActive={isActive('notifications')}
71133
label='Notifications'
72134
to='/notifications'
135+
onLinkClick={this.unhoverContainer}
73136
/>
74137
)}
75-
<AppLink Icon={IconGlobe3} isActive={isActive('explorer')} label='Explorer' to='/explorer' />
138+
<AppLink
139+
Icon={IconGlobe3}
140+
isActive={isActive('explorer')}
141+
label='Explorer'
142+
to='/explorer'
143+
onLinkClick={this.unhoverContainer}
144+
/>
76145
<div {...cn('link')} title='More Apps'>
77146
<WorldPanelItem Icon={IconDotsGrid} label='More Apps' isActive={false} onClick={this.openModal} />
78147
<span>More Apps</span>
@@ -90,11 +159,18 @@ interface AppLinkProps {
90159
isActive: boolean;
91160
label: string;
92161
to: string;
162+
onLinkClick?: () => void;
93163
}
94164

95-
const AppLink = ({ Icon, isActive, to, label }: AppLinkProps) => {
165+
const AppLink = ({ Icon, isActive, to, label, onLinkClick }: AppLinkProps) => {
166+
const handleClick = () => {
167+
if (!isActive && onLinkClick) {
168+
onLinkClick();
169+
}
170+
};
171+
96172
return (
97-
<Link title={label} {...cn('link')} to={!isActive && to}>
173+
<Link title={label} {...cn('link')} to={!isActive ? to : undefined} onClick={handleClick}>
98174
<WorldPanelItem Icon={Icon} label={label} isActive={isActive} />
99175
<span data-active={isActive ? '' : null}>{label}</span>
100176
</Link>

src/components/app-bar/index.vitest.tsx

+68-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { vi } from 'vitest';
2-
import { render } from '@testing-library/react';
2+
import { render, fireEvent } from '@testing-library/react';
33
import { MemoryRouter } from 'react-router-dom';
44

55
import { AppBar, Properties } from '.';
66

7+
vi.mock('react-router-dom', () => ({
8+
Link: ({ children, onClick, to, ...props }: any) => (
9+
<a onClick={onClick} to={to} {...props}>
10+
{children}
11+
</a>
12+
),
13+
MemoryRouter: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
14+
}));
15+
716
vi.mock('@zero-tech/zui/icons', () => ({
817
IconHome: 'IconHome',
918
IconSlashes: 'IconSlashes',
@@ -13,6 +22,7 @@ vi.mock('@zero-tech/zui/icons', () => ({
1322
IconList: 'IconList',
1423
IconBell1: 'IconBell1',
1524
}));
25+
1626
vi.mock('./more-apps-modal', () => ({
1727
MoreAppsModal: () => <div data-testid='more-apps-modal' />,
1828
}));
@@ -45,15 +55,69 @@ describe(AppBar, () => {
4555
vi.clearAllMocks();
4656
});
4757

48-
describe('active app', () => {
49-
it('should make conversation icon active when active app is conversation', () => {
58+
describe('Active App State', () => {
59+
it('should set the Messenger icon as active when activeApp is "conversation"', () => {
5060
renderComponent({ activeApp: 'conversation' });
5161
expect(mockWorldPanelItem).toHaveBeenCalledWith(expect.objectContaining({ label: 'Messenger', isActive: true }));
5262
});
5363

54-
it('should not make conversation icon active when active app is anything else', () => {
64+
it('should not set the Messenger icon as active when activeApp is something else', () => {
5565
renderComponent({ activeApp: 'foo' });
5666
expect(mockWorldPanelItem).toHaveBeenCalledWith(expect.objectContaining({ label: 'Messenger', isActive: false }));
5767
});
5868
});
69+
70+
describe('Unhover Functionality', () => {
71+
it('should add the no-hover class when an AppLink is clicked', () => {
72+
const { getByText, getByTestId } = renderComponent({});
73+
74+
const link = getByText('Home');
75+
const panel = getByTestId('legacy-panel');
76+
77+
fireEvent.click(link);
78+
79+
expect(panel.classList.contains('no-hover')).toBe(true);
80+
});
81+
82+
it('should remove the no-hover class when the mouse leaves the container', () => {
83+
const { getByText, getByTestId } = renderComponent({});
84+
85+
const link = getByText('Home');
86+
const panel = getByTestId('legacy-panel');
87+
88+
fireEvent.click(link);
89+
expect(panel.classList.contains('no-hover')).toBe(true);
90+
91+
fireEvent.mouseLeave(panel);
92+
expect(panel.classList.contains('no-hover')).toBe(false);
93+
});
94+
95+
it('should maintain the no-hover class when the mouse moves within the container after clicking', () => {
96+
const { getByText, getByTestId } = renderComponent({});
97+
98+
const link = getByText('Home');
99+
const panel = getByTestId('legacy-panel');
100+
101+
fireEvent.click(link);
102+
expect(panel.classList.contains('no-hover')).toBe(true);
103+
104+
fireEvent.mouseMove(panel);
105+
expect(panel.classList.contains('no-hover')).toBe(true);
106+
});
107+
108+
it('should allow hovering again after the mouse leaves and re-enters the container', () => {
109+
const { getByText, getByTestId } = renderComponent({});
110+
111+
const link = getByText('Home');
112+
const panel = getByTestId('legacy-panel');
113+
114+
fireEvent.click(link);
115+
116+
fireEvent.mouseLeave(panel);
117+
expect(panel.classList.contains('no-hover')).toBe(false);
118+
119+
fireEvent.mouseEnter(panel);
120+
expect(panel.classList.contains('no-hover')).toBe(false);
121+
});
122+
});
59123
});

src/components/app-bar/styles.scss

+15-4
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,25 @@ $width: 46px;
3838
backdrop-filter: blur(30px);
3939
list-style: none;
4040
transition: width 0.15s ease-in-out;
41-
}
4241

43-
&:hover &__container {
44-
width: calc(200px);
42+
&:hover {
43+
width: calc(200px);
44+
}
45+
46+
&.no-hover {
47+
width: $width !important;
48+
transition: width 0.15s ease-in-out;
49+
50+
&:hover {
51+
width: $width !important;
52+
}
53+
}
4554
}
4655

4756
&__notification-icon-wrapper {
4857
position: relative;
58+
width: 22px;
59+
height: 22px;
4960
}
5061

5162
&__notification-icon {
@@ -90,7 +101,7 @@ $width: 46px;
90101

91102
span {
92103
position: absolute;
93-
left: 34px; // 30px icon + 4px padding
104+
left: 46px; // 30px icon + 16px padding
94105
top: 50%;
95106
transform: translateY(-50%);
96107

src/components/layout/panel/index.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode } from 'react';
1+
import { ReactNode, forwardRef, Ref } from 'react';
22

33
import cn from 'classnames';
44

@@ -10,9 +10,15 @@ export interface PanelProps {
1010
toggleSidekick?: () => void;
1111
}
1212

13-
export const LegacyPanel = ({ children, className }: PanelProps) => {
14-
return <div className={cn(styles.Legacy, className)}>{children}</div>;
15-
};
13+
export const LegacyPanel = forwardRef(({ children, className }: PanelProps, ref: Ref<HTMLDivElement>) => {
14+
return (
15+
<div ref={ref} className={cn(styles.Legacy, className)} data-testid='legacy-panel'>
16+
{children}
17+
</div>
18+
);
19+
});
20+
21+
LegacyPanel.displayName = 'LegacyPanel';
1622

1723
export const Panel = ({ children, className }: PanelProps) => {
1824
return <div className={cn(styles.Panel, className)}>{children}</div>;

0 commit comments

Comments
 (0)