Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bc0f83c
feat: notifications core service draft
cyyynthia Nov 14, 2023
1ec0527
chore: cleanup batch job internal events
cyyynthia Nov 19, 2023
eefcec0
feat: core user notification dispatch
cyyynthia Nov 19, 2023
c4a183c
chore: lint
cyyynthia Nov 19, 2023
fcecce7
feat: finish user notification dispatch logic
cyyynthia Nov 20, 2023
bd8bfab
fix: make fetching translation to language maps work again
cyyynthia Nov 21, 2023
a850303
tests: test notification dispatching
cyyynthia Nov 24, 2023
3f10a64
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Nov 24, 2023
3b31359
feat: redo notifications the right way this time
cyyynthia Dec 3, 2023
397dd08
chore(deps): solve ij classpath issues with json-unit & assertj
cyyynthia Dec 3, 2023
9f1c7cd
feat: debounce key creation and initial string creations
cyyynthia Dec 3, 2023
c97c9e9
feat: notification preference configuration
cyyynthia Dec 3, 2023
85149be
feat: respect notification subscription settings
cyyynthia Dec 3, 2023
f235131
tests: fix json number matching
cyyynthia Dec 4, 2023
caa4ce4
fix: handle modified entity relations using rdbms-level cascade
cyyynthia Dec 4, 2023
2384df9
chore: lint
cyyynthia Dec 4, 2023
be30fc9
fix: hibernate proxy behavior
cyyynthia Dec 4, 2023
16de53b
tests: remove unused mockbean
cyyynthia Dec 4, 2023
19d7c6e
fix: hibernate lazy initialization problems
cyyynthia Dec 4, 2023
e612c0c
feat: implement notification http endpoints
cyyynthia Dec 13, 2023
4aebb94
fix: notification updates
cyyynthia Dec 13, 2023
621113a
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Dec 13, 2023
38e938a
fix: key creation test
cyyynthia Dec 13, 2023
9569892
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Jan 22, 2024
8823857
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Jan 24, 2024
1d54b2e
fix: migrate notifications to Spring Boot 3
cyyynthia Jan 26, 2024
49b8710
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Jan 26, 2024
1698670
chore: format
cyyynthia Jan 26, 2024
cda1335
fix(tests): handle bad transactions
cyyynthia Jan 26, 2024
b838b7f
fix: make language permission fetching more efficient
cyyynthia Jan 27, 2024
c845dda
Merge remote-tracking branch 'origin/cynthia/notifications-core' into…
cyyynthia Jan 27, 2024
b841451
chore: ktlint
cyyynthia Jan 27, 2024
c208be0
fix: bad aggregation result
cyyynthia Jan 27, 2024
0601b71
fix: bad permission mock
cyyynthia Jan 27, 2024
9cb51f4
chore(webapp): cleanup & update some deps
cyyynthia Feb 5, 2024
ac25e49
feat: add notification bell and root view
cyyynthia Feb 5, 2024
ca62a12
fix: workaround the fact npm is a bad pkg manager
cyyynthia Feb 5, 2024
a6e0362
feat: skeleton of the notifications view
cyyynthia Feb 14, 2024
f018b9b
Merge remote-tracking branch 'origin/cynthia/notifications-core' into…
cyyynthia Feb 14, 2024
cbb7633
Merge branch 'main' into cynthia/notifications-core
cyyynthia Apr 29, 2024
f1e0ee9
fix: use listagg instead of array_agg
cyyynthia Apr 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.tolgee.api.EeSubscriptionProvider
import io.tolgee.component.PreferredOrganizationFacade
import io.tolgee.hateoas.InitialDataModel
import io.tolgee.hateoas.ee.IEeSubscriptionModelAssembler
import io.tolgee.notifications.UserNotificationService
import io.tolgee.openApiDocs.OpenApiHideFromPublicDocs
import io.tolgee.security.authentication.AuthenticationFacade
import io.tolgee.service.security.UserPreferencesService
Expand All @@ -32,6 +33,7 @@ class InitialDataController(
private val eeSubscriptionModelAssembler: IEeSubscriptionModelAssembler,
private val eeSubscriptionProvider: EeSubscriptionProvider,
private val announcementController: AnnouncementController,
private val userNotificationService: UserNotificationService,
) : IController {
@GetMapping(value = [""])
@Operation(summary = "Get initial data", description = "Returns initial data required by the UI to load")
Expand All @@ -53,6 +55,7 @@ class InitialDataController(
data.preferredOrganization = preferredOrganizationFacade.getPreferred()
data.languageTag = userPreferencesService.find(userAccount.id)?.language
data.announcement = announcementController.getLatest()
data.unreadNotifications = userNotificationService.getUnreadNotificationsCount(userAccount.id)
}

return data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Copyright (C) 2023 Tolgee s.r.o. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.tolgee.api.v2.controllers.notifications

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.notifications.NotificationPreferencesService
import io.tolgee.notifications.dto.NotificationPreferencesDto
import io.tolgee.security.authentication.AuthenticationFacade
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(value = ["/v2/notifications/preferences"])
@Tag(name = "Notification preferences")
class NotificationPreferencesController(
private val authenticationFacade: AuthenticationFacade,
private val notificationPreferencesService: NotificationPreferencesService,
) {
@GetMapping("")
@Operation(summary = "Fetch the global preferences and all overrides of the current user")
fun getAllPreferences(): Map<String, NotificationPreferencesDto> {
return notificationPreferencesService.getAllPreferences(authenticationFacade.authenticatedUser.id)
}

@GetMapping("/global")
@Operation(summary = "Fetch the global preferences for the current user")
fun getGlobalPreferences(): NotificationPreferencesDto {
return notificationPreferencesService.getGlobalPreferences(authenticationFacade.authenticatedUser.id)
}

@PutMapping("/global")
@Operation(summary = "Update the global notification preferences of the current user")
fun updateGlobalPreferences(
@RequestBody @Validated preferencesDto: NotificationPreferencesDto,
): NotificationPreferencesDto {
val updated =
notificationPreferencesService.setPreferencesOfUser(
authenticationFacade.authenticatedUser.id,
preferencesDto,
)

return NotificationPreferencesDto.fromEntity(updated)
}

@GetMapping("/project/{id}")
@Operation(summary = "Fetch the notification preferences of the current user for a specific project")
fun getPerProjectPreferences(
@PathVariable("id") id: Long,
): NotificationPreferencesDto {
return notificationPreferencesService.getProjectPreferences(
authenticationFacade.authenticatedUser.id,
id,
)
}

@PutMapping("/project/{id}")
@Operation(summary = "Update the notification preferences of the current user for a specific project")
fun updatePerProjectPreferences(
@PathVariable("id") id: Long,
@RequestBody @Validated preferencesDto: NotificationPreferencesDto,
): NotificationPreferencesDto {
val updated =
notificationPreferencesService.setProjectPreferencesOfUser(
authenticationFacade.authenticatedUser.id,
id,
preferencesDto,
)

return NotificationPreferencesDto.fromEntity(updated)
}

@DeleteMapping("/project/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete the notification preferences of the current user for a specific project")
fun deletePerProjectPreferences(
@PathVariable("id") id: Long,
) {
notificationPreferencesService.deleteProjectPreferencesOfUser(
authenticationFacade.authenticatedUser.id,
id,
)
}

@PostMapping("/project/{id}/subscribe")
@Operation(summary = "Subscribe to notifications for a given project")
fun subscribeToProject(
@PathVariable("id") id: Long,
): ResponseEntity<String> {
return ResponseEntity(
"Coming soon! Please see https://github.com/tolgee/tolgee-platform/issues/1360 for progress on this. :D",
HttpHeaders().also {
@Suppress("UastIncorrectHttpHeaderInspection")
it.add(
"x-hey-curious-reader",
"oh hey there, didn't expect you here... " +
"if you're here, might as well join us! https://tolgee.io/career",
)
},
HttpStatus.NOT_IMPLEMENTED,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Copyright (C) 2023 Tolgee s.r.o. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.tolgee.api.v2.controllers.notifications

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.hateoas.notifications.UserNotificationModel
import io.tolgee.hateoas.notifications.UserNotificationModelAssembler
import io.tolgee.notifications.NotificationStatus
import io.tolgee.notifications.UserNotificationService
import io.tolgee.security.authentication.AuthenticationFacade
import org.springdoc.core.annotations.ParameterObject
import org.springframework.data.domain.Pageable
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(value = ["/v2/notifications"])
@Tag(name = "Notifications")
class NotificationsController(
private val authenticationFacade: AuthenticationFacade,
private val userNotificationService: UserNotificationService,
private val userNotificationModelAssembler: UserNotificationModelAssembler,
) {
@GetMapping("")
@Operation(summary = "Fetch the current user's notifications")
fun getNotifications(
@RequestParam("status", defaultValue = "UNREAD,READ") status: Set<NotificationStatus>,
@ParameterObject pageable: Pageable,
): List<UserNotificationModel> {
val notifications =
userNotificationService.findNotificationsOfUserFilteredPaged(
authenticationFacade.authenticatedUser.id,
status,
pageable,
)

return notifications.map { userNotificationModelAssembler.toModel(it) }
}

@PostMapping("/mark-as-read")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Marks a given set of notifications as read.")
fun markNotificationsAsRead(
@RequestBody notifications: List<Long>,
) {
userNotificationService.markAsRead(
authenticationFacade.authenticatedUser.id,
notifications,
)
}

@PostMapping("/mark-as-read/all")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Marks all notifications as read.")
fun markAllNotificationsAsRead() {
userNotificationService.markAllAsRead(authenticationFacade.authenticatedUser.id)
}

@PostMapping("/mark-as-unread")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Marks a given set of notifications as unread.")
fun markNotificationsAsUnread(
@RequestBody notifications: List<Long>,
) {
userNotificationService.markAsUnread(
authenticationFacade.authenticatedUser.id,
notifications,
)
}

@PostMapping("/mark-as-done")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Marks a given set of notifications as done.")
fun markNotificationsAsDone(
@RequestBody notifications: List<Long>,
) {
userNotificationService.markAsDone(
authenticationFacade.authenticatedUser.id,
notifications,
)
}

@PostMapping("/mark-as-done/all")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Marks all notifications as done.")
fun markAllNotificationsAsDone() {
userNotificationService.markAllAsDone(authenticationFacade.authenticatedUser.id)
}

@PostMapping("/unmark-as-done")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Un-marks a given set of notifications as done.")
fun unmarkNotificationsAsDone(
@RequestBody notifications: Collection<Long>,
) {
userNotificationService.unmarkAsDone(
authenticationFacade.authenticatedUser.id,
notifications,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import io.tolgee.hateoas.ee.eeSubscription.EeSubscriptionModel
import io.tolgee.hateoas.organization.PrivateOrganizationModel
import io.tolgee.hateoas.userAccount.PrivateUserAccountModel

class InitialDataModel(
@Suppress("unused")
data class InitialDataModel(
val serverConfiguration: PublicConfigurationDTO,
var userInfo: PrivateUserAccountModel? = null,
var preferredOrganization: PrivateOrganizationModel? = null,
var languageTag: String? = null,
val eeSubscription: EeSubscriptionModel? = null,
var announcement: AnnouncementDto? = null,
var unreadNotifications: Int? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright (C) 2023 Tolgee s.r.o. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.tolgee.hateoas.notifications

import io.tolgee.hateoas.batch.BatchJobModel
import io.tolgee.hateoas.project.SimpleProjectModel
import io.tolgee.model.views.activity.SimpleModifiedEntityView
import io.tolgee.notifications.NotificationType
import org.springframework.hateoas.RepresentationModel
import java.io.Serializable
import java.util.*

@Suppress("unused")
class UserNotificationModel(
val id: Long,
val type: NotificationType,
val project: SimpleProjectModel?,
val batchJob: BatchJobModel?,
val modifiedEntities: List<SimpleModifiedEntityView>?,
val unread: Boolean,
val markedDoneAt: Date?,
val lastUpdated: Date,
) : RepresentationModel<UserNotificationModel>(), Serializable
Loading