fix: lint + styling

This commit is contained in:
Calum H. (IMB11)
2026-06-02 20:30:08 +01:00
parent e7c7966f77
commit a4079c23e7
7 changed files with 146 additions and 28 deletions
+22 -4
View File
@@ -11,6 +11,7 @@ import {
import { import {
ArrowBigUpDashIcon, ArrowBigUpDashIcon,
ChangeSkinIcon, ChangeSkinIcon,
CheckIcon,
CompassIcon, CompassIcon,
DownloadIcon, DownloadIcon,
ExternalIcon, ExternalIcon,
@@ -40,6 +41,7 @@ import {
defineMessages, defineMessages,
I18nDebugPanel, I18nDebugPanel,
LoadingBar, LoadingBar,
ModrinthHostingLogo,
NewsArticleCard, NewsArticleCard,
NotificationPanel, NotificationPanel,
OverflowMenu, OverflowMenu,
@@ -84,6 +86,7 @@ import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue' import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue' import NavButton from '@/components/ui/NavButton.vue'
import ServerInvitePopupBody from '@/components/ui/notifications/ServerInvitePopupBody.vue'
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue' import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
@@ -756,7 +759,15 @@ command_listener(handleCommand)
notification_listener(handleLiveNotification) notification_listener(handleLiveNotification)
async function markLiveNotificationRead(notification) { 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) { async function respondToServerInvite(notification, action) {
@@ -765,12 +776,12 @@ async function respondToServerInvite(notification, action) {
throw new Error('Missing server ID for invite notification.') throw new Error('Missing server ID for invite notification.')
} }
await markLiveNotificationRead(notification)
await tauriApiClient.request(`/servers/${serverId}/invites/${action}`, { await tauriApiClient.request(`/servers/${serverId}/invites/${action}`, {
api: 'archon', api: 'archon',
version: 1, version: 1,
method: 'POST', method: 'POST',
}) })
await markLiveNotificationRead(notification)
return serverId return serverId
} }
@@ -804,21 +815,28 @@ async function handleLiveNotification(notification) {
const inviterId = notification.body.invited_by const inviterId = notification.body.invited_by
const invitedBy = const invitedBy =
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
const inviterName = invitedBy?.username ?? 'Someone'
addPopupNotification({ addPopupNotification({
title: 'Modrinth Hosting', 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', type: 'info',
buttons: [ buttons: [
{ {
label: 'Accept', label: 'Accept',
action: () => acceptServerInviteNotification(notification), action: () => acceptServerInviteNotification(notification),
icon: CheckIcon,
color: 'brand', color: 'brand',
}, },
{ {
label: 'Decline', label: 'Decline',
action: () => declineServerInviteNotification(notification), action: () => declineServerInviteNotification(notification),
icon: XIcon,
color: 'red', color: 'red',
}, },
], ],
@@ -0,0 +1,52 @@
<template>
<div class="flex flex-wrap items-center gap-x-1.5 gap-y-1 leading-snug text-primary">
<button
v-if="inviterName"
type="button"
class="inline-flex min-w-0 items-center border-0 bg-transparent p-0 font-semibold text-contrast hover:underline"
@click="openInviterProfile(inviterName)"
>
<Avatar
:src="inviterAvatarUrl"
:alt="inviterName"
circle
size="xxs"
no-shadow
class="mr-1.5 inline-flex"
/>
<span>{{ inviterName }}</span>
</button>
<span>
<span v-if="inviterName" class="whitespace-nowrap">has invited you to manage</span>
<span v-else class="whitespace-nowrap">You have been invited to manage</span>
<span class="font-semibold text-contrast ml-1">{{ serverName }}</span>
<span>.</span>
</span>
</div>
</template>
<script setup>
import { Avatar } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { config } from '@/config'
defineProps({
inviterName: {
type: String,
default: null,
},
inviterAvatarUrl: {
type: String,
default: null,
},
serverName: {
type: String,
required: true,
},
})
function openInviterProfile(username) {
openUrl(`${config.siteUrl}/user/${encodeURIComponent(username)}`)
}
</script>
+6
View File
@@ -42,6 +42,12 @@
{ {
"url": "https://api.purpurmc.org/*" "url": "https://api.purpurmc.org/*"
}, },
{
"url": "http://localhost:8000/*"
},
{
"url": "http://127.0.0.1:8000/*"
},
{ {
"url": "http://*.taila228c5.ts.net/*" "url": "http://*.taila228c5.ts.net/*"
}, },
+22 -1
View File
@@ -2,8 +2,9 @@ use crate::auth::AuthenticationError;
use crate::auth::validate::get_user_record_from_bearer_token; use crate::auth::validate::get_user_record_from_bearer_token;
use crate::database::PgPool; use crate::database::PgPool;
use crate::database::models::friend_item::DBFriend; use crate::database::models::friend_item::DBFriend;
use crate::database::models::notification_item::DBNotification;
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::models::notifications::Notification; use crate::models::notifications::{Notification, NotificationBody};
use crate::models::pats::Scopes; use crate::models::pats::Scopes;
use crate::models::users::User; use crate::models::users::User;
use crate::queue::session::AuthQueue; use crate::queue::session::AuthQueue;
@@ -129,6 +130,26 @@ pub async fn ws_init(
)?) )?)
.await; .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!(
&notification.body,
NotificationBody::ServerInvite { .. }
)
})
.map(Notification::from);
for notification in unread_server_invites {
let _ = session.text(serde_json::to_string(&notification)?).await;
}
let db = db.clone(); let db = db.clone();
let socket_id = db.next_socket_id.fetch_add(1, Ordering::Relaxed); let socket_id = db.next_socket_id.fetch_add(1, Ordering::Relaxed);
db.sockets db.sockets
@@ -18,28 +18,35 @@
> >
<div class="flex flex-col gap-2 w-full"> <div class="flex flex-col gap-2 w-full">
<div class="flex items-center justify-between gap-2.5"> <div class="flex items-center justify-between gap-2.5">
<div class="flex items-center gap-2"> <div class="flex min-w-0 flex-1 items-center gap-2">
<div <component
class="flex items-center" :is="item.titleLogo"
:class="{ v-if="item.titleLogo"
'text-red': item.type === 'error', class="h-7 w-auto min-w-0 max-w-full text-contrast"
'text-orange': item.type === 'warning', />
'text-green': item.type === 'download', <template v-else>
'text-contrast': item.type === 'success', <div
'text-blue': class="flex items-center"
!item.type || :class="{
!['error', 'warning', 'success', 'download'].includes(item.type), 'text-red': item.type === 'error',
}" 'text-orange': item.type === 'warning',
> 'text-green': item.type === 'download',
<IssuesIcon v-if="item.type === 'warning'" class="h-5 w-5" /> 'text-contrast': item.type === 'success',
<DownloadIcon v-else-if="item.type === 'download'" class="h-5 w-5" /> 'text-blue':
<CheckCircleIcon v-else-if="item.type === 'success'" class="h-5 w-5" /> !item.type ||
<XCircleIcon v-else-if="item.type === 'error'" class="h-5 w-5" /> !['error', 'warning', 'success', 'download'].includes(item.type),
<InfoIcon v-else class="h-5 w-5" /> }"
</div> >
<div class="text-contrast font-semibold m-0 grow"> <IssuesIcon v-if="item.type === 'warning'" class="h-5 w-5" />
{{ item.title }} <DownloadIcon v-else-if="item.type === 'download'" class="h-5 w-5" />
</div> <CheckCircleIcon v-else-if="item.type === 'success'" class="h-5 w-5" />
<XCircleIcon v-else-if="item.type === 'error'" class="h-5 w-5" />
<InfoIcon v-else class="h-5 w-5" />
</div>
<div class="text-contrast font-semibold m-0 grow">
{{ item.title }}
</div>
</template>
</div> </div>
<ButtonStyled size="small" type="transparent" circular> <ButtonStyled size="small" type="transparent" circular>
<button @click="dismiss(item.id)"> <button @click="dismiss(item.id)">
@@ -50,6 +57,11 @@
<span v-if="item.text" class="text-primary"> <span v-if="item.text" class="text-primary">
{{ item.text }} {{ item.text }}
</span> </span>
<component
:is="item.bodyComponent"
v-if="item.bodyComponent"
v-bind="item.bodyProps ?? {}"
/>
</div> </div>
<div v-if="item.progressItems?.length" class="flex flex-col gap-3"> <div v-if="item.progressItems?.length" class="flex flex-col gap-3">
<div <div
@@ -89,6 +101,7 @@
:color="btn.color || (idx === 0 ? 'brand' : undefined)" :color="btn.color || (idx === 0 ? 'brand' : undefined)"
> >
<button @click="handleButtonClick(item.id, btn)"> <button @click="handleButtonClick(item.id, btn)">
<component :is="btn.icon" v-if="btn.icon" />
{{ btn.label }} {{ btn.label }}
</button> </button>
</ButtonStyled> </ButtonStyled>
@@ -166,7 +179,7 @@ withDefaults(
top: calc(var(--top-bar-height, 3rem) + 1.5rem); top: calc(var(--top-bar-height, 3rem) + 1.5rem);
right: 1.5rem; right: 1.5rem;
z-index: 200; z-index: 200;
width: 400px; width: 520px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
@@ -5,6 +5,8 @@ export * from './icons'
export { default as InstallingBanner } from './InstallingBanner.vue' export { default as InstallingBanner } from './InstallingBanner.vue'
export * from './labels' export * from './labels'
export * from './marketing' 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 { default as SaveBanner } from './SaveBanner.vue'
export * from './server-header' export * from './server-header'
export { default as ServerListEmpty } from './server-list-empty/ServerListEmpty.vue' export { default as ServerListEmpty } from './server-list-empty/ServerListEmpty.vue'
@@ -1,8 +1,11 @@
import type { Component } from 'vue'
import { createContext } from '.' import { createContext } from '.'
export interface PopupNotificationButton { export interface PopupNotificationButton {
label: string label: string
action: () => void action: () => void
icon?: Component
color?: 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'standard' color?: 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'standard'
keepOpen?: boolean keepOpen?: boolean
} }
@@ -18,6 +21,9 @@ export interface PopupNotificationProgressItem {
export interface PopupNotification { export interface PopupNotification {
id: string | number id: string | number
title: string title: string
titleLogo?: Component
bodyComponent?: Component
bodyProps?: Record<string, unknown>
text?: string text?: string
type?: 'error' | 'warning' | 'success' | 'info' | 'download' type?: 'error' | 'warning' | 'success' | 'info' | 'download'
progress?: number progress?: number