Skip to content

Commit

Permalink
Refactor BuiltByBit notifications and alerts system
Browse files Browse the repository at this point in the history
  • Loading branch information
YourMCGeek committed Mar 9, 2025
1 parent e4319ce commit 43ca23e
Show file tree
Hide file tree
Showing 10 changed files with 394 additions and 73 deletions.
6 changes: 3 additions & 3 deletions builtbybit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@
]
},
{
"name": "alerts",
"title": "Alerts",
"description": "Monitor Alerts from BuiltByBit",
"name": "get-notifications",
"title": "Get Notifications",
"description": "Fetch notifications from BuiltByBit",
"mode": "menu-bar",
"interval": "1m"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useAlerts } from "./hooks/useAlerts";
import axios from "axios";
import { showFailureToast } from "@raycast/utils";
import { formatRelativeDate } from "./utils/dateUtils";
import { ALERTS_CACHE_KEY, API_KEY } from "./utils/constants";
import { AlertType, ContentType, ContentTypeURLMap } from "./types/alert";

const MenuCommand: React.FC = () => {
Expand All @@ -25,6 +24,7 @@ const MenuCommand: React.FC = () => {

// Memoize mark all as read handler
const markAllAsRead = useCallback(() => {

axios
.patch("https://api.builtbybit.com/v1/alerts", {
headers: { Authorization: `Private ${API_KEY}` },
Expand Down
152 changes: 152 additions & 0 deletions builtbybit/src/get-notifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { useEffect, useState } from "react";
import { Alert, AlertType, ContentType, ContentTypeNames, ContentTypeURLMap } from "./types/alert";
import { AlertsService } from "./services/alertsService";
import { showFailureToast, useCachedPromise } from "@raycast/utils";
import { UserUtils } from "./utils/userUtils";
import { Color, Icon, MenuBarExtra, open, showToast, Toast } from "@raycast/api";
import { formatRelativeDate } from "./utils/dateUtils";

export default function Command() {
const [enrichedAlerts, setEnrichedAlerts] = useState<Alert[]>([]);

const {
data: alertResponse,
isLoading,
revalidate,
} = useCachedPromise(() => AlertsService.fetchAlerts(), [], {
keepPreviousData: true,
initialData: [],
});

useEffect(() => {
async function enrichAlerts() {
const alerts = Array.isArray(alertResponse) ? alertResponse : [];
if (alerts.length === 0) return;

// Filter out read alerts first
const unreadAlerts = alerts.filter((alert) => !alert.read);
const enriched = [...unreadAlerts];

// Process alerts in batches
const batchSize = 5;
for (let i = 0; i < enriched.length; i += batchSize) {
const batch = enriched.slice(i, i + batchSize);

await Promise.all(
batch.map(async (alert, index) => {
try {
if (alert.caused_member_id !== 0) {
const username = await UserUtils.idToUsername(alert.caused_member_id);
enriched[i + index] = { ...alert, username };
}
} catch (error) {
console.error(`Error fetching username for ID ${alert.caused_member_id}:`, error);
}
}),
);
}
setEnrichedAlerts(enriched);
}
enrichAlerts();
}, [alertResponse]);

const handleMarkAllAsRead = async () => {
try {
const success = await AlertsService.markAllAsRead();
if (success) {
await showToast(Toast.Style.Success, "Marked all notifications as read");
revalidate();
} else {
await showFailureToast("Failed to mark notifications as read", {
title: "Failed to mark notifications as read",
});
}
} catch (error) {
await showFailureToast(error, { title: "Error marking notifications as read", message: String(error) });
}
};

const getAlertMessage = (alert: Alert) => {
const username = alert.username || `User #${alert.caused_member_id}`;
const contentTypeName = ContentTypeNames[alert.content_type as ContentType] || "content";

switch (alert.alert_type) {
case AlertType.REACTION:
return `${username} reacted to your ${contentTypeName}`;
case AlertType.REPLY:
return `${username} replied to your ${contentTypeName}`;
case AlertType.TICKET_MOVED:
return `Your ticket has been moved`;
default:
return `New notification from ${username}`;
}
};

const getContentUrl = (alert: Alert) => {
const baseUrl = ContentTypeURLMap[alert.content_type as ContentType];
return `${baseUrl}/${alert.content_id}`;
};

const handleRefresh = async () => {
await UserUtils.clearCache();
revalidate();
};

const unreadCount = enrichedAlerts.filter((a) => !a.read).length;

return (
<MenuBarExtra
icon={
unreadCount > 0
? { source: "../assets/bbb-icon.png" }
: { source: "../assets/bbb-icon.png", tintColor: Color.SecondaryText }
}
title={unreadCount > 0 ? String(unreadCount) : "0"}
isLoading={isLoading}
>
<MenuBarExtra.Section title={unreadCount > 0 ? "Notifications" : "No Unread Notifications"}>
<MenuBarExtra.Item
title="View All Alerts"
shortcut={{ modifiers: ["cmd", "shift"], key: "o" }}
onAction={() => open("https://builtbybit.com/account/alerts")}
/>

{enrichedAlerts.map((alert, index) => (
<MenuBarExtra.Item
key={index}
title={getAlertMessage(alert)}
onAction={() => {
open(getContentUrl(alert));
alert.read = true;
revalidate();
}}
subtitle={formatRelativeDate(alert.alert_date)}
/>
))}
<MenuBarExtra.Item
title="Mark All as Read"
icon={Icon.CheckCircle}
onAction={handleMarkAllAsRead}
shortcut={{ modifiers: ["cmd"], key: "enter" }}
/>
</MenuBarExtra.Section>
<MenuBarExtra.Section>
<MenuBarExtra.Item
title="Refresh"
icon={Icon.ArrowClockwise}
shortcut={{ modifiers: ["cmd"], key: "r" }}
onAction={handleRefresh}
/>
<MenuBarExtra.Item
title="Clear User Cache"
icon={Icon.Trash}
onAction={async () => {
await UserUtils.clearCache();
await showToast(Toast.Style.Success, "User cache cleared");
}}
shortcut={{ modifiers: ["cmd", "shift"], key: "x" }}
/>
</MenuBarExtra.Section>
</MenuBarExtra>
);
}
1 change: 1 addition & 0 deletions builtbybit/src/get-resources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@raycast/api";
import { useFetch } from "@raycast/utils";


const preferences = getPreferenceValues<PreferenceValues>();

export default function Command(props: LaunchProps<{ arguments: Arguments.GetResources }>) {
Expand Down
7 changes: 4 additions & 3 deletions builtbybit/src/search-members.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Action, ActionPanel, Detail, getPreferenceValues, Icon, LaunchProps, PreferenceValues } from "@raycast/api";
import { useFetch } from "@raycast/utils";
import { API_BASE_URL } from "./utils/constants";

const preferences = getPreferenceValues<PreferenceValues>();

Expand All @@ -12,13 +13,13 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.Search

switch (method) {
case "username":
baseUrl = `https://api.builtbybit.com/v1/members/usernames/${search}`;
baseUrl = `${API_BASE_URL}/members/usernames/${search}`;
break;
case "userID":
baseUrl = `https://api.builtbybit.com/v1/members/${search}`;
baseUrl = `${API_BASE_URL}/members/${search}`;
break;
case "discordID":
baseUrl = `https://api.builtbybit.com/v1/members/discords/${search}`;
baseUrl = `${API_BASE_URL}/members/discords/${search}`;
break;
default:
throw new Error("Invalid search method");
Expand Down
29 changes: 29 additions & 0 deletions builtbybit/src/services/alertsService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { showFailureToast } from "@raycast/utils";
import { Alert } from "../types/alert";
import apiClient from "../utils/constants";

export class AlertsService {
public static async fetchAlerts(): Promise<Alert[]> {
try {
console.log("Fetching alerts at", new Date().toISOString());
const response = await apiClient.get("/alerts");
console.log(response.data);
return response.data?.data;
} catch (error) {
console.error("Error fetching alerts:", error);
await showFailureToast(error, { title: "Error fetching alerts." });
throw error;
}
}

public static async markAllAsRead(): Promise<boolean> {
try {
const response = await apiClient.post("/alerts", { read: true });
return response.status >= 200 && response.status < 300;
} catch (error) {
console.error("Error marking alerts as read:", error);
await showFailureToast(error, { title: "Error marking alerts as read." });
throw error;
}
}
}
16 changes: 16 additions & 0 deletions builtbybit/src/types/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,37 @@ export interface Alert {
alert_type: string;
alert_date: number;
username?: string;
read?: boolean;
}

export interface AlertResponse {
data: Alert[];
}

export enum AlertType {
REACTION = "reaction",
REPLY = "insert",
TICKET_MOVED = "nf_tickets_moved",
}

export enum ContentType {
CONVERSATION = "conversation_message",
TICKET = "nf_tickets_message",
THREAD = "post",
REPORT = "report_comment",
}

export const ContentTypeURLMap: { [key in ContentType]: string } = {
[ContentType.CONVERSATION]: "https://builtbybit.com/conversations/messages",
[ContentType.TICKET]: "https://builtbybit.com/tickets/messages",
[ContentType.THREAD]: "https://builtbybit.com/posts",
[ContentType.REPORT]: "https://builtbybit.com/reports/comment",

};

export const ContentTypeNames: { [key in ContentType]: string } = {
[ContentType.CONVERSATION]: "conversation",
[ContentType.TICKET]: "ticket",
[ContentType.THREAD]: "thread",
[ContentType.REPORT]: "report",
};
Loading

0 comments on commit 43ca23e

Please sign in to comment.