Skip to content

Commit 7db6405

Browse files
committed
Add repository mute and snooze controls
- Add mute toggles for each repository in settings - Add snooze buttons in popup to temporarily hide repos - Configurable snooze duration (1-24 hours) - Group activities by repository instead of by time - Filter muted/snoozed repos from notifications and badge count - Fix toolbar icon visibility in dark mode
1 parent 5d0e576 commit 7db6405

6 files changed

Lines changed: 569 additions & 48 deletions

File tree

background.js

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,25 +49,42 @@ if (typeof chrome !== 'undefined' && chrome.notifications) {
4949

5050
async function checkGitHubActivity() {
5151
try {
52-
const { githubToken, watchedRepos, lastCheck, filters, notifications } = await chrome.storage.sync.get([
52+
const { githubToken, watchedRepos, lastCheck, filters, notifications, mutedRepos, snoozedRepos } = await chrome.storage.sync.get([
5353
'githubToken',
5454
'watchedRepos',
5555
'lastCheck',
5656
'filters',
57-
'notifications'
57+
'notifications',
58+
'mutedRepos',
59+
'snoozedRepos'
5860
]);
5961

6062
if (!githubToken || !watchedRepos || watchedRepos.length === 0) {
6163
return;
6264
}
6365

66+
// Clean up expired snoozes
67+
const activeSnoozedRepos = await cleanExpiredSnoozes(snoozedRepos || []);
68+
69+
// Get list of repos to exclude (muted + snoozed)
70+
const excludedRepos = new Set([
71+
...(mutedRepos || []),
72+
...activeSnoozedRepos.map(s => s.repo)
73+
]);
74+
6475
const enabledFilters = filters || { prs: true, issues: true, releases: true };
6576
const lastCheckDate = lastCheck ? new Date(lastCheck) : new Date(Date.now() - 24 * 60 * 60 * 1000);
6677
const newActivities = [];
6778

6879
for (const repo of watchedRepos) {
6980
// Handle both string format (legacy) and object format (new)
7081
const repoName = typeof repo === 'string' ? repo : repo.fullName;
82+
83+
// Skip muted and snoozed repos
84+
if (excludedRepos.has(repoName)) {
85+
continue;
86+
}
87+
7188
const activities = await fetchRepoActivity(repoName, githubToken, lastCheckDate, enabledFilters);
7289
newActivities.push(...activities);
7390
}
@@ -201,14 +218,40 @@ async function fetchRepoActivity(repo, token, since, filters) {
201218
return activities;
202219
}
203220

221+
async function cleanExpiredSnoozes(snoozedRepos) {
222+
const now = Date.now();
223+
const activeSnoozes = snoozedRepos.filter(s => s.expiresAt > now);
224+
225+
// Update storage if any snoozes expired
226+
if (activeSnoozes.length !== snoozedRepos.length) {
227+
await chrome.storage.sync.set({ snoozedRepos: activeSnoozes });
228+
}
229+
230+
return activeSnoozes;
231+
}
232+
204233
async function storeActivities(newActivities) {
205234
const { activities = [] } = await chrome.storage.local.get(['activities']);
235+
const { mutedRepos = [], snoozedRepos = [] } = await chrome.storage.sync.get(['mutedRepos', 'snoozedRepos']);
236+
237+
// Clean up expired snoozes
238+
const activeSnoozedRepos = await cleanExpiredSnoozes(snoozedRepos);
239+
240+
// Get list of repos to exclude
241+
const excludedRepos = new Set([
242+
...mutedRepos,
243+
...activeSnoozedRepos.map(s => s.repo)
244+
]);
206245

207246
// Merge new activities, avoiding duplicates
208247
const existingIds = new Set(activities.map(a => a.id));
209248
const uniqueNew = newActivities.filter(a => !existingIds.has(a.id));
210249

211-
const updated = [...uniqueNew, ...activities].slice(0, 100);
250+
// Filter out activities from excluded repos
251+
const allActivities = [...uniqueNew, ...activities];
252+
const filtered = allActivities.filter(a => !excludedRepos.has(a.repo));
253+
254+
const updated = filtered.slice(0, 100);
212255
await chrome.storage.local.set({ activities: updated });
213256
}
214257

@@ -340,6 +383,7 @@ if (typeof module !== 'undefined' && module.exports) {
340383
fetchRepoActivity,
341384
storeActivities,
342385
updateBadge,
343-
showNotifications
386+
showNotifications,
387+
cleanExpiredSnoozes
344388
};
345389
}

options/options.html

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,68 @@
451451
color: var(--text-secondary);
452452
background: transparent;
453453
}
454+
455+
.toggle-switch {
456+
position: relative;
457+
display: inline-block;
458+
width: 44px;
459+
height: 24px;
460+
flex-shrink: 0;
461+
}
462+
463+
.toggle-switch input {
464+
opacity: 0;
465+
width: 0;
466+
height: 0;
467+
}
468+
469+
.toggle-slider {
470+
position: absolute;
471+
cursor: pointer;
472+
top: 0;
473+
left: 0;
474+
right: 0;
475+
bottom: 0;
476+
background-color: #ccc;
477+
transition: .3s;
478+
border-radius: 24px;
479+
}
480+
481+
.toggle-slider:before {
482+
position: absolute;
483+
content: "";
484+
height: 18px;
485+
width: 18px;
486+
left: 3px;
487+
bottom: 3px;
488+
background-color: white;
489+
transition: .3s;
490+
border-radius: 50%;
491+
}
492+
493+
input:checked + .toggle-slider {
494+
background-color: var(--link-color);
495+
}
496+
497+
input:checked + .toggle-slider:before {
498+
transform: translateX(20px);
499+
}
500+
501+
.repo-actions {
502+
display: flex;
503+
align-items: center;
504+
gap: 12px;
505+
flex-shrink: 0;
506+
}
507+
508+
.mute-toggle-label {
509+
display: flex;
510+
align-items: center;
511+
gap: 8px;
512+
font-size: 13px;
513+
color: var(--text-secondary);
514+
white-space: nowrap;
515+
}
454516
</style>
455517
</head>
456518
<body>
@@ -584,6 +646,17 @@ <h2>Check Interval</h2>
584646
<option value="60">Every hour</option>
585647
</select>
586648
</div>
649+
<div class="form-row">
650+
<label for="snoozeHours">Snooze duration (hours)</label>
651+
<select id="snoozeHours">
652+
<option value="1" selected>1 hour</option>
653+
<option value="2">2 hours</option>
654+
<option value="4">4 hours</option>
655+
<option value="8">8 hours</option>
656+
<option value="24">24 hours</option>
657+
</select>
658+
<p class="help-text" style="margin-top: 8px;">How long to snooze repositories when you snooze them from the popup</p>
659+
</div>
587660
</div>
588661

589662
<button id="saveBtn" class="primary">Save Settings</button>

options/options.js

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const state = {
22
watchedRepos: [],
3+
mutedRepos: [],
34
currentPage: 1,
45
reposPerPage: 10,
56
searchQuery: ''
@@ -160,7 +161,9 @@ async function loadSettings() {
160161
const settings = await chrome.storage.sync.get([
161162
'githubToken',
162163
'watchedRepos',
164+
'mutedRepos',
163165
'checkInterval',
166+
'snoozeHours',
164167
'filters',
165168
'notifications',
166169
'theme'
@@ -174,6 +177,7 @@ async function loadSettings() {
174177
}
175178

176179
state.watchedRepos = settings.watchedRepos || [];
180+
state.mutedRepos = settings.mutedRepos || [];
177181

178182
// Migrate old string format to new object format
179183
if (settings.githubToken && state.watchedRepos.some(r => typeof r === 'string')) {
@@ -186,6 +190,10 @@ async function loadSettings() {
186190
document.getElementById('checkInterval').value = settings.checkInterval;
187191
}
188192

193+
if (settings.snoozeHours) {
194+
document.getElementById('snoozeHours').value = settings.snoozeHours;
195+
}
196+
189197
if (settings.filters) {
190198
document.getElementById('filterPrs').checked = settings.filters.prs !== false;
191199
document.getElementById('filterIssues').checked = settings.filters.issues !== false;
@@ -533,18 +541,29 @@ function renderRepoList() {
533541
list.innerHTML = reposToDisplay.map(repo => {
534542
// Handle both old string format and new object format
535543
if (typeof repo === 'string') {
544+
const isMuted = state.mutedRepos.includes(repo);
536545
return `
537546
<li class="repo-item">
538547
<div class="repo-content">
539548
<div class="repo-name">${repo}</div>
540549
<div class="repo-description">Legacy format - remove and re-add to see details</div>
541550
</div>
542-
<button class="danger" data-repo="${repo}">Remove</button>
551+
<div class="repo-actions">
552+
<label class="mute-toggle-label">
553+
<span>Muted</span>
554+
<label class="toggle-switch">
555+
<input type="checkbox" class="mute-toggle" data-repo="${repo}" ${isMuted ? 'checked' : ''}>
556+
<span class="toggle-slider"></span>
557+
</label>
558+
</label>
559+
<button class="danger" data-repo="${repo}">Remove</button>
560+
</div>
543561
</li>
544562
`;
545563
}
546564

547565
const { fullName, description, language, stars, updatedAt, latestRelease } = repo;
566+
const isMuted = state.mutedRepos.includes(fullName);
548567

549568
const starIcon = '<svg class="star-icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/></svg>';
550569

@@ -560,7 +579,16 @@ function renderRepoList() {
560579
<span class="meta-item">Updated ${formatDate(updatedAt)}</span>
561580
</div>
562581
</div>
563-
<button class="danger" data-repo="${fullName}">Remove</button>
582+
<div class="repo-actions">
583+
<label class="mute-toggle-label">
584+
<span>Muted</span>
585+
<label class="toggle-switch">
586+
<input type="checkbox" class="mute-toggle" data-repo="${fullName}" ${isMuted ? 'checked' : ''}>
587+
<span class="toggle-slider"></span>
588+
</label>
589+
</label>
590+
<button class="danger" data-repo="${fullName}">Remove</button>
591+
</div>
564592
</li>
565593
`;
566594
}).join('');
@@ -570,11 +598,32 @@ function renderRepoList() {
570598
removeRepo(btn.dataset.repo);
571599
});
572600
});
601+
602+
list.querySelectorAll('.mute-toggle').forEach(toggle => {
603+
toggle.addEventListener('change', (e) => {
604+
toggleMuteRepo(toggle.dataset.repo, e.target.checked);
605+
});
606+
});
607+
}
608+
609+
async function toggleMuteRepo(repoFullName, mute) {
610+
if (mute) {
611+
if (!state.mutedRepos.includes(repoFullName)) {
612+
state.mutedRepos.push(repoFullName);
613+
}
614+
} else {
615+
state.mutedRepos = state.mutedRepos.filter(r => r !== repoFullName);
616+
}
617+
618+
// Auto-save
619+
await chrome.storage.sync.set({ mutedRepos: state.mutedRepos });
620+
showRepoMessage(mute ? 'Repository muted' : 'Repository unmuted', 'success');
573621
}
574622

575623
async function saveSettings() {
576624
const token = document.getElementById('githubToken').value.trim();
577625
const interval = parseInt(document.getElementById('checkInterval').value);
626+
const snoozeHours = parseInt(document.getElementById('snoozeHours').value);
578627

579628
if (!token) {
580629
showMessage('GitHub token is required', 'error');
@@ -600,7 +649,9 @@ async function saveSettings() {
600649
await chrome.storage.sync.set({
601650
githubToken: token,
602651
watchedRepos: state.watchedRepos,
652+
mutedRepos: state.mutedRepos,
603653
checkInterval: interval,
654+
snoozeHours: snoozeHours,
604655
filters: filters,
605656
notifications: notifications,
606657
theme: theme
@@ -650,6 +701,7 @@ if (typeof module !== 'undefined' && module.exports) {
650701
fetchGitHubRepoFromNpm,
651702
validateRepo,
652703
removeRepo,
704+
toggleMuteRepo,
653705
getFilteredRepos,
654706
renderRepoList,
655707
migrateRepoFormat,

0 commit comments

Comments
 (0)