diff --git a/client/src/api/notifications.preferences.ts b/client/src/api/notifications.preferences.ts index 59b910d8c346..8509d7cd2691 100644 --- a/client/src/api/notifications.preferences.ts +++ b/client/src/api/notifications.preferences.ts @@ -1,11 +1,18 @@ import { type components, fetcher } from "@/api/schema"; -export type UserNotificationPreferences = components["schemas"]["UserNotificationPreferences"]; +type UserNotificationPreferences = components["schemas"]["UserNotificationPreferences"]; + +export interface UserNotificationPreferencesExtended extends UserNotificationPreferences { + supportedChannels: string[]; +} const getNotificationsPreferences = fetcher.path("/api/notifications/preferences").method("get").create(); -export async function getNotificationsPreferencesFromServer() { - const { data } = await getNotificationsPreferences({}); - return data; +export async function getNotificationsPreferencesFromServer(): Promise { + const { data, headers } = await getNotificationsPreferences({}); + return { + ...data, + supportedChannels: headers.get("supported-channels")?.split(",") ?? [], + }; } type UpdateUserNotificationPreferencesRequest = components["schemas"]["UpdateUserNotificationPreferencesRequest"]; diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index dbc4567a868c..8786ebbb1e4e 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -1255,6 +1255,9 @@ export interface paths { /** * Returns the current user's preferences for notifications. * @description Anonymous users cannot have notification preferences. They will receive only broadcasted notifications. + * + * - The settings will contain all possible channels, but the client should only show the ones that are really supported by the server. + * The supported channels are returned in the `supported-channels` header. */ get: operations["get_notification_preferences_api_notifications_preferences_get"]; /** @@ -20160,6 +20163,9 @@ export interface operations { /** * Returns the current user's preferences for notifications. * @description Anonymous users cannot have notification preferences. They will receive only broadcasted notifications. + * + * - The settings will contain all possible channels, but the client should only show the ones that are really supported by the server. + * The supported channels are returned in the `supported-channels` header. */ parameters?: { /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ diff --git a/client/src/components/User/Notifications/NotificationsPreferences.vue b/client/src/components/User/Notifications/NotificationsPreferences.vue index 5d6f0c3ca017..48487103bcbd 100644 --- a/client/src/components/User/Notifications/NotificationsPreferences.vue +++ b/client/src/components/User/Notifications/NotificationsPreferences.vue @@ -8,7 +8,7 @@ import { computed, ref, watch } from "vue"; import { getNotificationsPreferencesFromServer, updateNotificationsPreferencesOnServer, - UserNotificationPreferences, + UserNotificationPreferencesExtended, } from "@/api/notifications.preferences"; import { useConfig } from "@/composables/config"; import { Toast } from "@/composables/toast"; @@ -39,14 +39,15 @@ const { config } = useConfig(true); const loading = ref(false); const errorMessage = ref(null); const pushNotificationsGranted = ref(pushNotificationsEnabled()); -const notificationsPreferences = ref({}); +const notificationsPreferences = ref({}); +const supportedChannels = ref([]); const categories = computed(() => Object.keys(notificationsPreferences.value)); const showPreferences = computed(() => { return !loading.value && config.value.enable_notification_system && notificationsPreferences.value; }); -const categoryDescriptionMap = { +const categoryDescriptionMap: Record = { message: "You will receive notifications when someone sends you a message.", new_shared_item: "You will receive notifications when someone shares an item with you.", }; @@ -55,6 +56,7 @@ async function getNotificationsPreferences() { loading.value = true; await getNotificationsPreferencesFromServer() .then((data) => { + supportedChannels.value = data.supportedChannels; notificationsPreferences.value = data.preferences; }) .catch((error: any) => { @@ -148,10 +150,7 @@ watch( switch /> -
+
List[User]: + pass + + +class NotificationChannelPlugin(Protocol): + config: GalaxyAppConfiguration + + def __init__(self, config: GalaxyAppConfiguration): + self.config = config + + def send(self, notification: Notification, user: User): + raise NotImplementedError + + class NotificationManager: """Manager class to interact with the database models related with Notifications.""" @@ -113,11 +128,7 @@ def __init__(self, sa_session: galaxy_scoped_session, config: GalaxyAppConfigura Notification.expiration_time, Notification.content, ] - # Register the supported notification channels here - self.channel_plugins: Dict[str, NotificationChannelPlugin] = { - "push": NoOpNotificationChannelPlugin(self.config), # Push notifications are handled by the client - "email": EmailNotificationChannelPlugin(self.config), - } + self.channel_plugins = self._register_supported_channels() @property def notifications_enabled(self): @@ -407,14 +418,30 @@ def update_user_notification_preferences( self, user: User, request: UpdateUserNotificationPreferencesRequest ) -> UserNotificationPreferences: """Updates the user's notification preferences with the requested changes.""" - notification_preferences = self.get_user_notification_preferences(user) - notification_preferences.update(request.preferences) - user.preferences[NOTIFICATION_PREFERENCES_SECTION_NAME] = ( - notification_preferences.model_dump_json() - ) # type:ignore[index] + preferences = self.get_user_notification_preferences(user) + preferences.update(request.preferences) + user.preferences[NOTIFICATION_PREFERENCES_SECTION_NAME] = preferences.model_dump_json() # type:ignore[index] with transaction(self.sa_session): self.sa_session.commit() - return notification_preferences + return preferences + + def _register_supported_channels(self) -> Dict[str, NotificationChannelPlugin]: + """Registers the supported notification channels in this server.""" + supported_channels: Dict[str, NotificationChannelPlugin] = { + # Push notifications are handled client-side so no real plugin is needed + "push": NoOpNotificationChannelPlugin(self.config), + } + + if self.can_send_notifications_async: + # Most additional channels require asynchronous processing and will be + # handled by Celery tasks. Add their plugins here. + supported_channels["email"] = EmailNotificationChannelPlugin(self.config) + + return supported_channels + + def get_supported_channels(self) -> Set[str]: + """Returns the set of supported notification channels in this server.""" + return set(self.channel_plugins.keys()) def cleanup_expired_notifications(self) -> CleanupResultSummary: """ @@ -492,9 +519,8 @@ def _broadcasted_notifications_query(self, since: Optional[datetime] = None, act return stmt -class NotificationRecipientResolverStrategy(Protocol): - def resolve_users(self, recipients: NotificationRecipients) -> List[User]: - pass +# -------------------------------------- +# Notification Recipients Resolver Implementations class NotificationRecipientResolver: @@ -603,17 +629,7 @@ def resolve_users(self, recipients: NotificationRecipients) -> List[User]: # -------------------------------------- -# Notification Channel Plugins - - -class NotificationChannelPlugin(Protocol): - config: GalaxyAppConfiguration - - def __init__(self, config: GalaxyAppConfiguration): - self.config = config - - def send(self, notification: Notification, user: User): - raise NotImplementedError +# Notification Channel Plugins Implementations class NoOpNotificationChannelPlugin(NotificationChannelPlugin): diff --git a/lib/galaxy/schema/notifications.py b/lib/galaxy/schema/notifications.py index bbf19a9bc39d..f78bc0954480 100644 --- a/lib/galaxy/schema/notifications.py +++ b/lib/galaxy/schema/notifications.py @@ -421,7 +421,10 @@ class NotificationChannelSettings(Model): email: bool = Field( default=True, title="Email", - description="Whether the user wants to receive email notifications for this category.", + description=( + "Whether the user wants to receive email notifications for this category. " + "This setting will be ignored unless the server supports asynchronous tasks." + ), ) # TODO: Add more channels here and implement the corresponding plugin in lib/galaxy/managers/notification.py # matrix: bool # Possible future Matrix.org integration? diff --git a/lib/galaxy/webapps/galaxy/api/notifications.py b/lib/galaxy/webapps/galaxy/api/notifications.py index dd23fa050311..f1bfbcab88a3 100644 --- a/lib/galaxy/webapps/galaxy/api/notifications.py +++ b/lib/galaxy/webapps/galaxy/api/notifications.py @@ -70,10 +70,20 @@ def get_notifications_status( ) def get_notification_preferences( self, + response: Response, trans: ProvidesUserContext = DependsOnTrans, ) -> UserNotificationPreferences: - """Anonymous users cannot have notification preferences. They will receive only broadcasted notifications.""" - return self.service.get_user_notification_preferences(trans) + """Anonymous users cannot have notification preferences. They will receive only broadcasted notifications. + + - The settings will contain all possible channels, but the client should only show the ones that are really supported by the server. + The supported channels are returned in the `supported-channels` header. + """ + result = self.service.get_user_notification_preferences(trans) + # Inform the client which channels are really supported by the server since the settings will contain all possible channels. + response.headers["supported-channels"] = str.join( + ",", self.service.notification_manager.get_supported_channels() + ) + return result @router.put( "/api/notifications/preferences",