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 @@
+
+
+
+
+ has invited you to manage
+ You have been invited to manage
+ {{ serverName }}
+ .
+
+
+
+
+
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 }}
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ 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