diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index d4987c47ad..4e31d5a82c 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -11,6 +11,7 @@ import { import { ArrowBigUpDashIcon, ChangeSkinIcon, + CheckIcon, CompassIcon, DownloadIcon, ExternalIcon, @@ -40,6 +41,7 @@ import { defineMessages, I18nDebugPanel, LoadingBar, + ModrinthHostingLogo, NewsArticleCard, NotificationPanel, OverflowMenu, @@ -84,6 +86,7 @@ import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue' import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue' import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' import NavButton from '@/components/ui/NavButton.vue' +import ServerInvitePopupBody from '@/components/ui/notifications/ServerInvitePopupBody.vue' import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' @@ -756,7 +759,15 @@ command_listener(handleCommand) notification_listener(handleLiveNotification) async function markLiveNotificationRead(notification) { - await tauriApiClient.labrinth.notifications_v2.markAsRead(notification.id) + try { + await tauriApiClient.labrinth.notifications_v2.markAsRead(notification.id) + } catch (error) { + if (error instanceof ModrinthApiError && error.statusCode === 404) { + console.warn(`notification ${notification.id} could not be marked as read`, error) + return + } + throw error + } } async function respondToServerInvite(notification, action) { @@ -765,12 +776,12 @@ async function respondToServerInvite(notification, action) { throw new Error('Missing server ID for invite notification.') } - await markLiveNotificationRead(notification) await tauriApiClient.request(`/servers/${serverId}/invites/${action}`, { api: 'archon', version: 1, method: 'POST', }) + await markLiveNotificationRead(notification) return serverId } @@ -804,21 +815,28 @@ async function handleLiveNotification(notification) { const inviterId = notification.body.invited_by const invitedBy = typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null - const inviterName = invitedBy?.username ?? 'Someone' addPopupNotification({ title: 'Modrinth Hosting', - text: `${inviterName} has invited you to join ${serverName}.`, + titleLogo: ModrinthHostingLogo, + bodyComponent: ServerInvitePopupBody, + bodyProps: { + inviterName: invitedBy?.username ?? null, + inviterAvatarUrl: invitedBy?.avatar_url ?? null, + serverName, + }, type: 'info', buttons: [ { label: 'Accept', action: () => acceptServerInviteNotification(notification), + icon: CheckIcon, color: 'brand', }, { label: 'Decline', action: () => declineServerInviteNotification(notification), + icon: XIcon, color: 'red', }, ], diff --git a/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue b/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue new file mode 100644 index 0000000000..2899a0bdb7 --- /dev/null +++ b/apps/app-frontend/src/components/ui/notifications/ServerInvitePopupBody.vue @@ -0,0 +1,52 @@ + + + diff --git a/apps/app/capabilities/plugins.json b/apps/app/capabilities/plugins.json index 88617f4057..9bf2ad0b31 100644 --- a/apps/app/capabilities/plugins.json +++ b/apps/app/capabilities/plugins.json @@ -42,6 +42,12 @@ { "url": "https://api.purpurmc.org/*" }, + { + "url": "http://localhost:8000/*" + }, + { + "url": "http://127.0.0.1:8000/*" + }, { "url": "http://*.taila228c5.ts.net/*" }, diff --git a/apps/labrinth/src/routes/internal/statuses.rs b/apps/labrinth/src/routes/internal/statuses.rs index 885c06d84d..9a589e5013 100644 --- a/apps/labrinth/src/routes/internal/statuses.rs +++ b/apps/labrinth/src/routes/internal/statuses.rs @@ -2,8 +2,9 @@ use crate::auth::AuthenticationError; use crate::auth::validate::get_user_record_from_bearer_token; use crate::database::PgPool; use crate::database::models::friend_item::DBFriend; +use crate::database::models::notification_item::DBNotification; use crate::database::redis::RedisPool; -use crate::models::notifications::Notification; +use crate::models::notifications::{Notification, NotificationBody}; use crate::models::pats::Scopes; use crate::models::users::User; use crate::queue::session::AuthQueue; @@ -129,6 +130,26 @@ pub async fn ws_init( )?) .await; + let unread_server_invites = DBNotification::get_many_user_exposed_on_site( + user_id.into(), + &**pool, + &redis, + ) + .await? + .into_iter() + .filter(|notification| { + !notification.read + && matches!( + ¬ification.body, + NotificationBody::ServerInvite { .. } + ) + }) + .map(Notification::from); + + for notification in unread_server_invites { + let _ = session.text(serde_json::to_string(¬ification)?).await; + } + let db = db.clone(); let socket_id = db.next_socket_id.fetch_add(1, Ordering::Relaxed); db.sockets diff --git a/packages/ui/src/components/nav/PopupNotificationPanel.vue b/packages/ui/src/components/nav/PopupNotificationPanel.vue index aeb9d74128..3b0b66038b 100644 --- a/packages/ui/src/components/nav/PopupNotificationPanel.vue +++ b/packages/ui/src/components/nav/PopupNotificationPanel.vue @@ -18,28 +18,35 @@ >
-
-
- - - - - -
-
- {{ item.title }} -
+
+ +
@@ -166,7 +179,7 @@ withDefaults( top: calc(var(--top-bar-height, 3rem) + 1.5rem); right: 1.5rem; z-index: 200; - width: 400px; + width: 520px; display: flex; flex-direction: column; gap: 0.75rem; diff --git a/packages/ui/src/components/servers/index.ts b/packages/ui/src/components/servers/index.ts index e14e47cd23..e8f4f5d45c 100644 --- a/packages/ui/src/components/servers/index.ts +++ b/packages/ui/src/components/servers/index.ts @@ -5,6 +5,8 @@ export * from './icons' export { default as InstallingBanner } from './InstallingBanner.vue' export * from './labels' export * from './marketing' +export { default as ModrinthHostingLogo } from './ModrinthServersIcon.vue' +export { default as ModrinthServersIcon } from './ModrinthServersIcon.vue' export { default as SaveBanner } from './SaveBanner.vue' export * from './server-header' export { default as ServerListEmpty } from './server-list-empty/ServerListEmpty.vue' diff --git a/packages/ui/src/providers/popup-notifications.ts b/packages/ui/src/providers/popup-notifications.ts index 5cd049c1e2..f9dff9bf54 100644 --- a/packages/ui/src/providers/popup-notifications.ts +++ b/packages/ui/src/providers/popup-notifications.ts @@ -1,8 +1,11 @@ +import type { Component } from 'vue' + import { createContext } from '.' export interface PopupNotificationButton { label: string action: () => void + icon?: Component color?: 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'standard' keepOpen?: boolean } @@ -18,6 +21,9 @@ export interface PopupNotificationProgressItem { export interface PopupNotification { id: string | number title: string + titleLogo?: Component + bodyComponent?: Component + bodyProps?: Record text?: string type?: 'error' | 'warning' | 'success' | 'info' | 'download' progress?: number