feat: add notifs onto friends ws temporarily (#6290)

* feat: add notifs onto friends ws temporarily

* fix: lint + styling

* fix: regressions
This commit is contained in:
Calum H.
2026-06-02 20:47:37 +01:00
committed by GitHub
parent 940a796ba5
commit 3c051f5b1d
14 changed files with 369 additions and 45 deletions
@@ -1648,6 +1648,8 @@ export namespace Labrinth {
message_id?: string
invited_by?: string
organization_id?: string
server_id?: string
server_name?: string
team_id?: string
role?: string
old_status?: string
+15
View File
@@ -8,6 +8,7 @@ use crate::event::{
LoadingPayload, ProcessPayload, ProfilePayload, WarningPayload,
};
use futures::prelude::*;
use serde_json::Value;
#[cfg(feature = "tauri")]
use tauri::{Emitter, Manager};
use uuid::Uuid;
@@ -303,6 +304,20 @@ pub async fn emit_friend(payload: FriendPayload) -> crate::Result<()> {
Ok(())
}
#[allow(unused_variables)]
pub async fn emit_notification(payload: Value) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("notification", payload)
.map_err(EventError::from)?;
}
Ok(())
}
// loading_join! macro
// loading_join!(key: Option<&LoadingBarId>, total: f64, message: Option<&str>; task1, task2, task3...)
// This will submit a loading event with the given message for each task as they complete
+39 -7
View File
@@ -1,7 +1,7 @@
use crate::ErrorKind;
use crate::data::ModrinthCredentials;
use crate::event::FriendPayload;
use crate::event::emit::emit_friend;
use crate::event::emit::{emit_friend, emit_notification};
use crate::state::tunnel::InternalTunnelSocket;
use crate::state::{ProcessManager, Profile, TunnelSocket};
use crate::util::fetch::{FetchSemaphore, fetch_advanced, fetch_json};
@@ -22,6 +22,7 @@ use futures::{SinkExt, StreamExt};
use reqwest::Method;
use reqwest::header::HeaderValue;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::net::SocketAddr;
use std::ops::Deref;
use std::sync::Arc;
@@ -120,16 +121,34 @@ impl FriendsSocket {
Ok(msg) => {
let server_message = match msg {
Message::Text(text) => {
ServerToClientMessage::deserialize(
match ServerToClientMessage::deserialize(
Either::Left(&text),
)
.ok()
) {
Ok(message) => Some(message),
Err(_) => {
if let Ok(notification) =
serde_json::from_str::<Value>(&text)
{
let _ = Self::handle_notification(notification).await;
}
None
}
}
}
Message::Binary(bytes) => {
ServerToClientMessage::deserialize(
match ServerToClientMessage::deserialize(
Either::Right(&bytes),
)
.ok()
) {
Ok(message) => Some(message),
Err(_) => {
if let Ok(notification) =
serde_json::from_slice::<Value>(&bytes)
{
let _ = Self::handle_notification(notification).await;
}
None
}
}
}
Message::Ping(bytes) => {
if let Some(write) = write_handle
@@ -224,6 +243,19 @@ impl FriendsSocket {
Ok(())
}
async fn handle_notification(notification: Value) -> crate::Result<()> {
if notification
.get("body")
.and_then(|body| body.get("type"))
.and_then(Value::as_str)
.is_some()
{
emit_notification(notification).await?;
}
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn socket_loop() -> crate::Result<()> {
let state = crate::State::get().await?;
@@ -18,28 +18,35 @@
>
<div class="flex flex-col gap-2 w-full">
<div class="flex items-center justify-between gap-2.5">
<div class="flex items-center gap-2">
<div
class="flex items-center"
:class="{
'text-red': item.type === 'error',
'text-orange': item.type === 'warning',
'text-green': item.type === 'download',
'text-contrast': item.type === 'success',
'text-blue':
!item.type ||
!['error', 'warning', 'success', 'download'].includes(item.type),
}"
>
<IssuesIcon v-if="item.type === 'warning'" class="h-5 w-5" />
<DownloadIcon v-else-if="item.type === 'download'" class="h-5 w-5" />
<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>
<div class="flex min-w-0 flex-1 items-center gap-2">
<component
:is="item.titleLogo"
v-if="item.titleLogo"
class="h-7 w-auto min-w-0 max-w-full text-contrast"
/>
<template v-else>
<div
class="flex items-center"
:class="{
'text-red': item.type === 'error',
'text-orange': item.type === 'warning',
'text-green': item.type === 'download',
'text-contrast': item.type === 'success',
'text-blue':
!item.type ||
!['error', 'warning', 'success', 'download'].includes(item.type),
}"
>
<IssuesIcon v-if="item.type === 'warning'" class="h-5 w-5" />
<DownloadIcon v-else-if="item.type === 'download'" class="h-5 w-5" />
<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>
<ButtonStyled size="small" type="transparent" circular>
<button @click="dismiss(item.id)">
@@ -50,6 +57,11 @@
<span v-if="item.text" class="text-primary">
{{ item.text }}
</span>
<component
:is="item.bodyComponent"
v-if="item.bodyComponent"
v-bind="item.bodyProps ?? {}"
/>
</div>
<div v-if="item.progressItems?.length" class="flex flex-col gap-3">
<div
@@ -89,6 +101,7 @@
:color="btn.color || (idx === 0 ? 'brand' : undefined)"
>
<button @click="handleButtonClick(item.id, btn)">
<component :is="btn.icon" v-if="btn.icon" />
{{ btn.label }}
</button>
</ButtonStyled>
@@ -166,7 +179,8 @@ withDefaults(
top: calc(var(--top-bar-height, 3rem) + 1.5rem);
right: 1.5rem;
z-index: 200;
width: 400px;
width: 520px;
max-width: calc(100vw - 3rem);
display: flex;
flex-direction: column;
gap: 0.75rem;
@@ -179,6 +193,7 @@ withDefaults(
@media screen and (max-width: 500px) {
.popup-notification-group {
width: calc(100% - 1.5rem);
max-width: none;
right: 0.75rem;
}
}
@@ -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'
@@ -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<string, unknown>
text?: string
type?: 'error' | 'warning' | 'success' | 'info' | 'download'
progress?: number