Skip to content

Commit 19f81b2

Browse files
committed
feat: search notifications
Signed-off-by: Adam Setch <[email protected]>
1 parent f6f60d2 commit 19f81b2

File tree

8 files changed

+76
-60
lines changed

8 files changed

+76
-60
lines changed

biome.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"useDefaultSwitchClause": "error",
5959
"noParameterAssign": "error",
6060
"useAsConstAssertion": "error",
61+
"useBlockStatements": "error",
6162
"useDefaultParameterLast": "error",
6263
"useEnumInitializers": "error",
6364
"useSelfClosingElements": "error",

src/main/first-run.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ export async function onFirstRunMaybe() {
1717
* Ask user if the app should be moved to the applications folder (masOS).
1818
*/
1919
async function promptMoveToApplicationsFolder() {
20-
if (!isMacOS()) return;
20+
if (!isMacOS()) {
21+
return;
22+
}
2123

2224
const isDevMode = !!process.defaultApp;
23-
if (isDevMode || app.isInApplicationsFolder()) return;
25+
if (isDevMode || app.isInApplicationsFolder()) {
26+
return;
27+
}
2428

2529
const { response } = await dialog.showMessageBox({
2630
type: 'question',

src/main/updater.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ const listeners: ListenerMap = {};
2222
jest.mock('electron-updater', () => ({
2323
autoUpdater: {
2424
on: jest.fn((event: string, cb: Listener) => {
25-
if (!listeners[event]) listeners[event] = [];
25+
if (!listeners[event]) {
26+
listeners[event] = [];
27+
}
2628
listeners[event].push(cb);
2729
return this;
2830
}),
@@ -59,16 +61,16 @@ describe('main/updater.ts', () => {
5961
public setNoUpdateAvailableMenuVisibility = jest.fn();
6062
public setUpdateAvailableMenuVisibility = jest.fn();
6163
public setUpdateReadyForInstallMenuVisibility = jest.fn();
62-
constructor(mb: Menubar) {
63-
super(mb);
64-
}
6564
}
65+
6666
let menuBuilder: TestMenuBuilder;
6767
let updater: AppUpdater;
6868

6969
beforeEach(() => {
7070
jest.clearAllMocks();
71-
for (const k of Object.keys(listeners)) delete listeners[k];
71+
for (const k of Object.keys(listeners)) {
72+
delete listeners[k];
73+
}
7274

7375
menubar = {
7476
app: {

src/main/updater.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ export default class AppUpdater {
140140
};
141141

142142
dialog.showMessageBox(dialogOpts).then((returnValue) => {
143-
if (returnValue.response === 0) autoUpdater.quitAndInstall();
143+
if (returnValue.response === 0) {
144+
autoUpdater.quitAndInstall();
145+
}
144146
});
145147
}
146148
}

src/renderer/components/filters/SearchFilter.tsx

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type FC, useContext, useEffect, useId, useState } from 'react';
1+
import { type FC, useContext, useEffect, useState } from 'react';
22

33
import {
44
CheckCircleFillIcon,
@@ -21,8 +21,6 @@ import { Title } from '../primitives/Title';
2121
import { RequiresDetailedNotificationWarning } from './RequiresDetailedNotificationsWarning';
2222
import { TokenSearchInput } from './TokenSearchInput';
2323

24-
type InputToken = { id: number; text: string };
25-
2624
export const SearchFilter: FC = () => {
2725
const { updateFilter, settings } = useContext(AppContext);
2826

@@ -37,58 +35,52 @@ export const SearchFilter: FC = () => {
3735
}
3836
}, [settings.filterIncludeSearchTokens, settings.filterExcludeSearchTokens]);
3937

40-
const mapValuesToTokens = (values: string[]): InputToken[] =>
41-
values.map((value, index) => ({ id: index, text: value }));
42-
43-
const [includeSearchTokens, setIncludeSearchTokens] = useState<InputToken[]>(
44-
mapValuesToTokens(settings.filterIncludeSearchTokens),
38+
const [includeSearchTokens, setIncludeSearchTokens] = useState<SearchToken[]>(
39+
settings.filterIncludeSearchTokens,
4540
);
4641

4742
const addIncludeSearchToken = (value: string) => {
48-
if (!value || includeSearchTokens.some((v) => v.text === value)) return;
49-
const nextId =
50-
includeSearchTokens.reduce((m, t) => Math.max(m, t.id), -1) + 1;
51-
setIncludeSearchTokens([
52-
...includeSearchTokens,
53-
{ id: nextId, text: value },
54-
]);
43+
if (!value || includeSearchTokens.includes(value as SearchToken)) {
44+
return;
45+
}
46+
47+
setIncludeSearchTokens([...includeSearchTokens, value as SearchToken]);
5548
updateFilter('filterIncludeSearchTokens', value as SearchToken, true);
5649
};
5750

58-
const removeIncludeSearchToken = (tokenId: string | number) => {
59-
const value = includeSearchTokens.find((v) => v.id === tokenId)?.text || '';
60-
if (value)
61-
updateFilter('filterIncludeSearchTokens', value as SearchToken, false);
62-
setIncludeSearchTokens(includeSearchTokens.filter((v) => v.id !== tokenId));
51+
const removeIncludeSearchToken = (token: SearchToken) => {
52+
if (!token) {
53+
return;
54+
}
55+
56+
updateFilter('filterIncludeSearchTokens', token, false);
57+
setIncludeSearchTokens(includeSearchTokens.filter((t) => t !== token));
6358
};
6459

65-
const [excludeSearchTokens, setExcludeSearchTokens] = useState<InputToken[]>(
66-
mapValuesToTokens(settings.filterExcludeSearchTokens),
60+
const [excludeSearchTokens, setExcludeSearchTokens] = useState<SearchToken[]>(
61+
settings.filterExcludeSearchTokens as SearchToken[],
6762
);
6863

6964
const addExcludeSearchToken = (value: string) => {
70-
if (!value || excludeSearchTokens.some((v) => v.text === value)) return;
71-
const nextId =
72-
excludeSearchTokens.reduce((m, t) => Math.max(m, t.id), -1) + 1;
73-
setExcludeSearchTokens([
74-
...excludeSearchTokens,
75-
{ id: nextId, text: value },
76-
]);
65+
if (!value || excludeSearchTokens.includes(value as SearchToken)) {
66+
return;
67+
}
68+
69+
setExcludeSearchTokens([...excludeSearchTokens, value as SearchToken]);
7770
updateFilter('filterExcludeSearchTokens', value as SearchToken, true);
7871
};
7972

80-
const removeExcludeSearchToken = (tokenId: string | number) => {
81-
const value = excludeSearchTokens.find((v) => v.id === tokenId)?.text || '';
82-
if (value)
83-
updateFilter('filterExcludeSearchTokens', value as SearchToken, false);
84-
setExcludeSearchTokens(excludeSearchTokens.filter((v) => v.id !== tokenId));
85-
};
73+
const removeExcludeSearchToken = (token: SearchToken) => {
74+
if (!token) {
75+
return;
76+
}
8677

87-
// Basic suggestions for prefixes
88-
const fieldsetId = useId();
78+
updateFilter('filterExcludeSearchTokens', token, false);
79+
setExcludeSearchTokens(excludeSearchTokens.filter((t) => t !== token));
80+
};
8981

9082
return (
91-
<fieldset id={fieldsetId}>
83+
<fieldset>
9284
<Title
9385
icon={SearchIcon}
9486
tooltip={

src/renderer/components/filters/TokenSearchInput.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,15 @@ import {
99
} from '../../utils/notifications/filters/search';
1010
import { SearchFilterSuggestions } from './SearchFilterSuggestions';
1111

12-
export interface TokenInputItem {
13-
id: number; // stable index-based id (unique within its list)
14-
text: string; // actual token string (e.g. "author:octocat")
15-
}
16-
1712
interface TokenSearchInputProps {
1813
label: string;
1914
icon: FC<{ className?: string }>;
2015
iconColorClass: string;
21-
tokens: TokenInputItem[];
16+
tokens: readonly SearchToken[]; // raw token strings
2217
showSuggestionsOnFocusIfEmpty: boolean;
2318
isDetailedNotificationsEnabled: boolean;
24-
onAdd: (token: string) => void;
25-
onRemove: (tokenId: string | number) => void;
19+
onAdd: (token: SearchToken) => void;
20+
onRemove: (token: SearchToken) => void;
2621
}
2722

2823
const tokenEvents = ['Enter', 'Tab', ' ', ','];
@@ -40,14 +35,17 @@ export const TokenSearchInput: FC<TokenSearchInputProps> = ({
4035
const [inputValue, setInputValue] = useState('');
4136
const [showSuggestions, setShowSuggestions] = useState(false);
4237

38+
// FIXME - remove this
39+
const tokenItems = tokens.map((text, id) => ({ id, text }));
40+
4341
function tryAddToken(
4442
event:
4543
| React.KeyboardEvent<HTMLInputElement>
4644
| React.FocusEvent<HTMLInputElement>,
4745
) {
4846
const raw = (event.target as HTMLInputElement).value;
4947
const value = normalizeSearchInputToToken(raw);
50-
if (value && !tokens.some((t) => t.text === value)) {
48+
if (value && !tokens.includes(value as SearchToken)) {
5149
onAdd(value as SearchToken);
5250
(event.target as HTMLInputElement).value = '';
5351
setInputValue('');
@@ -104,10 +102,18 @@ export const TokenSearchInput: FC<TokenSearchInputProps> = ({
104102
}
105103
}}
106104
onKeyDown={onKeyDown}
107-
onTokenRemove={onRemove}
105+
onTokenRemove={(id) => {
106+
const token = tokenItems.find((t) => t.id === id)?.text as
107+
| SearchToken
108+
| undefined;
109+
110+
if (token) {
111+
onRemove(token);
112+
}
113+
}}
108114
size="small"
109115
title={`${label} searches`}
110-
tokens={tokens}
116+
tokens={tokenItems}
111117
/>
112118
<SearchFilterSuggestions
113119
inputValue={inputValue}

src/renderer/components/notifications/AccountNotifications.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ export const AccountNotifications: FC<IAccountNotifications> = (
4242
notifications.reduce(
4343
(acc: { [key: string]: Notification[] }, notification) => {
4444
const key = notification.repository.full_name;
45-
if (!acc[key]) acc[key] = [];
45+
if (!acc[key]) {
46+
acc[key] = [];
47+
}
48+
4649
acc[key].push(notification);
4750
return acc;
4851
},

src/renderer/utils/zoom.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ const MULTIPLIER = 2;
77
* @returns zoomLevel -2 to 0.5
88
*/
99
export const zoomPercentageToLevel = (percentage: number): number => {
10-
if (typeof percentage === 'undefined') return 0;
10+
if (typeof percentage === 'undefined') {
11+
return 0;
12+
}
13+
1114
return ((percentage - RECOMMENDED) * MULTIPLIER) / 100;
1215
};
1316

@@ -17,6 +20,9 @@ export const zoomPercentageToLevel = (percentage: number): number => {
1720
* @returns percentage 0-150
1821
*/
1922
export const zoomLevelToPercentage = (zoom: number): number => {
20-
if (typeof zoom === 'undefined') return 100;
23+
if (typeof zoom === 'undefined') {
24+
return 100;
25+
}
26+
2127
return (zoom / MULTIPLIER) * 100 + RECOMMENDED;
2228
};

0 commit comments

Comments
 (0)