Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 90 additions & 84 deletions src/browser/components/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function AppShell() {
tab.repo &&
tab.number !== undefined
) {
markTabUpdated(tab.id);
clearTabUpdated(tab.id);
setLastViewed(`${tab.owner}/${tab.repo}#${tab.number}`);
}
if (tab.type === "home") {
Expand All @@ -114,7 +114,7 @@ export function AppShell() {
navigate(`/${tab.owner}/${tab.repo}/pull/${tab.number}`);
}
},
[navigate, markTabUpdated]
[navigate, clearTabUpdated]
);

// Close a tab and navigate to the next active tab if needed
Expand Down Expand Up @@ -181,96 +181,100 @@ export function AppShell() {
};

const checkPRs = async () => {
const prTabs = tabs.filter(
(t): t is Tab & { owner: string; repo: string; number: number } =>
t.type === "pr-review" &&
t.owner !== undefined &&
t.repo !== undefined &&
t.number !== undefined
);

const involvedPRs = await githubStore.fetchInvolvedPRs();

const prSet = new Map<
string,
{ owner: string; repo: string; number: number }
>();
const addPr = (owner: string, repo: string, number: number) => {
prSet.set(`${owner}/${repo}/${number}`, { owner, repo, number });
};
for (const tab of prTabs) addPr(tab.owner, tab.repo, tab.number);
for (const pr of involvedPRs) {
const match = pr.repository_url?.match(/repos\/([^/]+)\/([^/]+)/);
if (match && pr.number) addPr(match[1], match[2], pr.number);
}
if (prSet.size === 0) return;

const enrichmentMap = await githubStore.getPREnrichment([
...prSet.values(),
]);

const tabPrIds = new Set(
prTabs.map((t) => `${t.owner}/${t.repo}#${t.number}`)
);
const notifiedThisCycle = new Set<string>();

for (const tab of prTabs) {
const key = `${tab.owner}/${tab.repo}/${tab.number}`;
const prId = `${tab.owner}/${tab.repo}#${tab.number}`;
const enrichment = enrichmentMap.get(key);
if (!enrichment) continue;
const viewerLastViewedAt = getLastViewed(prId);
const baseline = getBaseline(viewerLastViewedAt, enrichment);
if (baseline && enrichment.updatedAt > baseline) {
markTabUpdated(tab.id);
try {
const prTabs = tabs.filter(
(t): t is Tab & { owner: string; repo: string; number: number } =>
t.type === "pr-review" &&
t.owner !== undefined &&
t.repo !== undefined &&
t.number !== undefined
);

const involvedPRs = await githubStore.fetchInvolvedPRs();

const prSet = new Map<
string,
{ owner: string; repo: string; number: number }
>();
const addPr = (owner: string, repo: string, number: number) => {
prSet.set(`${owner}/${repo}/${number}`, { owner, repo, number });
};
for (const tab of prTabs) addPr(tab.owner, tab.repo, tab.number);
for (const pr of involvedPRs) {
const match = pr.repository_url?.match(/repos\/([^/]+)\/([^/]+)/);
if (match && pr.number) addPr(match[1], match[2], pr.number);
}
if (prSet.size === 0) return;

const enrichmentMap = await githubStore.getPREnrichment([
...prSet.values(),
]);

const tabPrIds = new Set(
prTabs.map((t) => `${t.owner}/${t.repo}#${t.number}`)
);
const notifiedThisCycle = new Set<string>();

for (const tab of prTabs) {
const key = `${tab.owner}/${tab.repo}/${tab.number}`;
const prId = `${tab.owner}/${tab.repo}#${tab.number}`;
const enrichment = enrichmentMap.get(key);
if (!enrichment) continue;
const viewerLastViewedAt = getLastViewed(prId);
const baseline = getBaseline(viewerLastViewedAt, enrichment);
if (baseline && enrichment.updatedAt > baseline) {
markTabUpdated(tab.id);
if (
notifsEnabled() &&
!notifiedThisCycle.has(prId) &&
enrichment.updatedAt > (getNotifiedAt(prId) ?? "")
) {
notifiedThisCycle.add(prId);
const prUrl = `/${tab.owner}/${tab.repo}/pull/${tab.number}`;
sendNotification(
`New activity on ${tab.owner}/${tab.repo} PR #${tab.number}`,
tab.prTitle || `#${tab.number}`,
prUrl,
`https://avatars.githubusercontent.com/${tab.owner}`
);
setNotifiedAt(prId, enrichment.updatedAt);
}
}
}

for (const pr of involvedPRs) {
const match = pr.repository_url?.match(/repos\/([^/]+)\/([^/]+)/);
if (!match || !pr.number) continue;
const owner = match[1];
const repo = match[2];
const number = pr.number;
const prId = `${owner}/${repo}#${number}`;
const prKey = `${owner}/${repo}/${number}`;
if (tabPrIds.has(prId)) continue;

const enrichment = enrichmentMap.get(prKey);
if (!enrichment) continue;
const viewerLastViewedAt = getLastViewed(prId);
const baseline = getBaseline(viewerLastViewedAt, enrichment);
if (
notifsEnabled() &&
!notifiedThisCycle.has(prId) &&
enrichment.updatedAt > (getNotifiedAt(prId) ?? "")
enrichment.updatedAt > (getNotifiedAt(prId) ?? "") &&
(!baseline || enrichment.updatedAt > baseline)
) {
notifiedThisCycle.add(prId);
const prUrl = `/${tab.owner}/${tab.repo}/pull/${tab.number}`;
const prUrl = `/${owner}/${repo}/pull/${number}`;
sendNotification(
`New activity on ${tab.owner}/${tab.repo} PR #${tab.number}`,
tab.prTitle || `#${tab.number}`,
`New activity on ${owner}/${repo} PR #${number}`,
pr.title,
prUrl,
`https://avatars.githubusercontent.com/${tab.owner}`
`https://avatars.githubusercontent.com/${owner}`
);
setNotifiedAt(prId, enrichment.updatedAt);
}
}
}

for (const pr of involvedPRs) {
const match = pr.repository_url?.match(/repos\/([^/]+)\/([^/]+)/);
if (!match || !pr.number) continue;
const owner = match[1];
const repo = match[2];
const number = pr.number;
const prId = `${owner}/${repo}#${number}`;
const prKey = `${owner}/${repo}/${number}`;
if (tabPrIds.has(prId)) continue;

const enrichment = enrichmentMap.get(prKey);
if (!enrichment) continue;
const viewerLastViewedAt = getLastViewed(prId);
const baseline = getBaseline(viewerLastViewedAt, enrichment);
if (
notifsEnabled() &&
!notifiedThisCycle.has(prId) &&
enrichment.updatedAt > (getNotifiedAt(prId) ?? "") &&
(!baseline || enrichment.updatedAt > baseline)
) {
notifiedThisCycle.add(prId);
const prUrl = `/${owner}/${repo}/pull/${number}`;
sendNotification(
`New activity on ${owner}/${repo} PR #${number}`,
pr.title,
prUrl,
`https://avatars.githubusercontent.com/${owner}`
);
setNotifiedAt(prId, enrichment.updatedAt);
}
} catch {
// Ignore transient network errors during polling
}
};

Expand Down Expand Up @@ -528,13 +532,15 @@ function TabStatusIndicator({ status }: { status?: TabStatus }) {
);
}

// Updated while in background gets an orange dot
// Updated while in background shows a refresh arrow
if (status.updated) {
return (
<span
className="w-2 h-2 rounded-full shrink-0 bg-orange-500"
className="shrink-0 text-orange-500 text-[10px] leading-none"
title="New activity"
/>
>
</span>
);
}

Expand Down
1 change: 1 addition & 0 deletions src/browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
default-src 'self';
connect-src 'self' https://api.github.com;
img-src 'self' https://github.com https://avatars.githubusercontent.com https://private-user-images.githubusercontent.com https://camo.githubusercontent.com data:;
media-src 'self' https://private-user-images.githubusercontent.com;
style-src 'self' 'unsafe-inline';
script-src 'self';
font-src 'self' data:;
Expand Down
Loading