diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/reaction/RemoveReaction.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/reaction/RemoveReaction.kt new file mode 100644 index 00000000..6a7dd7ff --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/reaction/RemoveReaction.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.core.application.reaction + +data class RemoveReaction( + val postId: Long, + val customEmojiId: Long?, + val unicodeEmoji: String +) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/reaction/UserRemoveReactionApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/reaction/UserRemoveReactionApplicationService.kt new file mode 100644 index 00000000..b53f21f8 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/reaction/UserRemoveReactionApplicationService.kt @@ -0,0 +1,50 @@ +package dev.usbharu.hideout.core.application.reaction + +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService +import dev.usbharu.hideout.core.application.shared.Transaction +import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository +import dev.usbharu.hideout.core.domain.model.post.PostId +import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository +import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser +import dev.usbharu.hideout.core.domain.service.emoji.UnicodeEmojiService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class UserRemoveReactionApplicationService( + transaction: Transaction, + private val customEmojiRepository: CustomEmojiRepository, + private val reactionRepository: ReactionRepository, + private val unicodeEmojiService: UnicodeEmojiService +) : + LocalUserAbstractApplicationService( + transaction, logger + ) { + override suspend fun internalExecute(command: RemoveReaction, principal: LocalUser) { + val postId = PostId(command.postId) + + val customEmoji = command.customEmojiId?.let { customEmojiRepository.findById(it) } + + val unicodeEmoji = if (unicodeEmojiService.isUnicodeEmoji(command.unicodeEmoji)) { + command.unicodeEmoji + } else { + "❤" + } + val reaction = + reactionRepository.findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji( + postId, + principal.actorId, + customEmoji?.id, + unicodeEmoji + ) + ?: throw IllegalArgumentException("Reaction $postId ${principal.actorId} ${customEmoji?.id} $unicodeEmoji not found.") + + reaction.delete() + + reactionRepository.delete(reaction) + } + + companion object { + private val logger = LoggerFactory.getLogger(UserRemoveReactionApplicationService::class.java) + } +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/reaction/Reaction.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/reaction/Reaction.kt index ea3eac76..93b43000 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/reaction/Reaction.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/reaction/Reaction.kt @@ -32,6 +32,10 @@ class Reaction( return id.hashCode() } + fun delete() { + addDomainEvent(ReactionEventFactory(this).createEvent(ReactionEvent.DELETE)) + } + companion object { fun create( id: ReactionId, diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/reaction/ReactionRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/reaction/ReactionRepository.kt index f2ed4779..9aa1f6aa 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/reaction/ReactionRepository.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/reaction/ReactionRepository.kt @@ -15,5 +15,12 @@ interface ReactionRepository { unicodeEmoji: String ): Boolean + suspend fun findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji( + postId: PostId, + actorId: ActorId, + customEmojiId: CustomEmojiId?, + unicodeEmoji: String + ): Reaction? + suspend fun delete(reaction: Reaction) } \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt index 9408d01a..37a476b1 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt @@ -168,7 +168,7 @@ class ExposedPostRepository( Posts.id eq id.id } .let(postQueryMapper::map) - .first() + .firstOrNull() } override suspend fun findAllById(ids: List): List { diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedReactionRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedReactionRepository.kt index f3ac7b96..f3fc6886 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedReactionRepository.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedReactionRepository.kt @@ -82,6 +82,21 @@ class ExposedReactionRepository(override val domainEventPublisher: DomainEventPu } } + override suspend fun findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji( + postId: PostId, + actorId: ActorId, + customEmojiId: CustomEmojiId?, + unicodeEmoji: String + ): Reaction? { + return query { + + Reactions.selectAll().where { + Reactions.postId.eq(postId.id).and(Reactions.actorId eq actorId.id) + .and((Reactions.customEmojiId eq customEmojiId?.emojiId or (Reactions.unicodeEmoji eq unicodeEmoji))) + }.limit(1).singleOrNull()?.toReaction() + } + } + companion object { private val logger = LoggerFactory.getLogger(ExposedReactionRepository::class.java) } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PostsController.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PostsController.kt index 0f6558c6..26e31f3c 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PostsController.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PostsController.kt @@ -5,7 +5,9 @@ import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplication import dev.usbharu.hideout.core.application.post.GetPostDetail import dev.usbharu.hideout.core.application.post.GetPostDetailApplicationService import dev.usbharu.hideout.core.application.reaction.CreateReaction +import dev.usbharu.hideout.core.application.reaction.RemoveReaction import dev.usbharu.hideout.core.application.reaction.UserCreateReactionApplicationService +import dev.usbharu.hideout.core.application.reaction.UserRemoveReactionApplicationService import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder import org.springframework.security.access.AccessDeniedException import org.springframework.stereotype.Controller @@ -19,7 +21,8 @@ class PostsController( private val getPostDetailApplicationService: GetPostDetailApplicationService, private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder, private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService, - private val userCreateReactionApplicationService: UserCreateReactionApplicationService + private val userCreateReactionApplicationService: UserCreateReactionApplicationService, + private val userRemoveReactionApplicationService: UserRemoveReactionApplicationService ) { @GetMapping("/users/{name}/posts/{id}") suspend fun postById(@PathVariable id: Long, model: Model): String { @@ -48,4 +51,17 @@ class PostsController( ) return "redirect:/users/$name/posts/$id" } + + @PostMapping("/users/{name}/posts/{id}/unfavourite") + suspend fun unfavourite(@PathVariable id: Long, @PathVariable name: String): String { + val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal() + userRemoveReactionApplicationService.execute( + RemoveReaction( + id, + null, + "❤" + ), principal + ) + return "redirect:/users/$name/posts/$id" + } } diff --git a/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql b/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql index 44bfbcf5..f4c62702 100644 --- a/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql +++ b/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql @@ -329,5 +329,6 @@ create table if not exists reactions unicode_emoji varchar(100) not null, created_at timestamp not null, constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade, - constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade + constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade, + unique (post_id, actor_id, created_at, unicode_emoji) ); \ No newline at end of file diff --git a/hideout-core/src/main/resources/templates/fragments-post.html b/hideout-core/src/main/resources/templates/fragments-post.html index 671cec83..d16f4861 100644 --- a/hideout-core/src/main/resources/templates/fragments-post.html +++ b/hideout-core/src/main/resources/templates/fragments-post.html @@ -44,14 +44,27 @@
-
- Reply - - - - -
+ +
+ Reply + + + + +
+
+ +
+ Reply + + + + +
+
+