diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/ActorDetail.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/ActorDetail.kt
new file mode 100644
index 00000000..d0c552c0
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/ActorDetail.kt
@@ -0,0 +1,38 @@
+package dev.usbharu.hideout.core.application.actor
+
+import dev.usbharu.hideout.core.domain.model.actor.Actor
+import java.net.URI
+
+data class ActorDetail(
+ val id: Long,
+ val name: String,
+ val screenName: String,
+ val host: String,
+ val remoteUrl: String?,
+ val locked: Boolean,
+ val description: String,
+ val postsCount: Int,
+ val iconUrl: URI?,
+ val bannerURL: URI?,
+ val followingCount: Int?,
+ val followersCount: Int?,
+) {
+ companion object {
+ fun of(actor: Actor, iconUrl: URI?, bannerURL: URI?): ActorDetail {
+ return ActorDetail(
+ id = actor.id.id,
+ name = actor.name.name,
+ screenName = actor.screenName.screenName,
+ host = actor.url.host,
+ remoteUrl = actor.url.toString(),
+ locked = actor.locked,
+ description = actor.description.description,
+ postsCount = actor.postsCount.postsCount,
+ iconUrl = iconUrl,
+ bannerURL = bannerURL,
+ followingCount = actor.followingCount?.relationshipCount,
+ followersCount = actor.followersCount?.relationshipCount,
+ )
+ }
+ }
+}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetActorDetail.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetActorDetail.kt
new file mode 100644
index 00000000..8089a404
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetActorDetail.kt
@@ -0,0 +1,8 @@
+package dev.usbharu.hideout.core.application.actor
+
+import dev.usbharu.hideout.core.domain.model.support.acct.Acct
+
+data class GetActorDetail(
+ val actorName: Acct? = null,
+ val id: Long? = null
+)
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetActorDetailApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetActorDetailApplicationService.kt
new file mode 100644
index 00000000..722f16fe
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetActorDetailApplicationService.kt
@@ -0,0 +1,49 @@
+package dev.usbharu.hideout.core.application.actor
+
+import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
+import dev.usbharu.hideout.core.application.shared.Transaction
+import dev.usbharu.hideout.core.config.ApplicationConfig
+import dev.usbharu.hideout.core.domain.model.actor.ActorId
+import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
+import dev.usbharu.hideout.core.domain.model.media.MediaRepository
+import dev.usbharu.hideout.core.domain.model.support.principal.Principal
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+
+@Component
+class GetActorDetailApplicationService(
+ private val actorRepository: ActorRepository,
+ private val mediaRepository: MediaRepository,
+ private val applicationConfig: ApplicationConfig,
+ transaction: Transaction
+) :
+ AbstractApplicationService(
+ transaction,
+ logger
+ ) {
+ override suspend fun internalExecute(command: GetActorDetail, principal: Principal): ActorDetail {
+ val actor = if (command.id != null) {
+ actorRepository.findById(ActorId(command.id))
+ ?: throw IllegalArgumentException("Actor ${command.id} not found.")
+ } else if (command.actorName != null) {
+ val host = if (command.actorName.host.isEmpty()) {
+ applicationConfig.url.host
+ } else {
+ command.actorName.host
+ }
+ actorRepository.findByNameAndDomain(command.actorName.userpart, host)
+ ?: throw IllegalArgumentException("Actor ${command.actorName} not found.")
+ } else {
+ throw IllegalArgumentException("id and actorName are null.")
+ }
+
+ val iconUrl = actor.icon?.let { mediaRepository.findById(it)?.url }
+ val bannerUrl = actor.banner?.let { mediaRepository.findById(it)?.url }
+
+ return ActorDetail.of(actor, iconUrl, bannerUrl)
+ }
+
+ companion object {
+ private val logger = LoggerFactory.getLogger(GetActorDetailApplicationService::class.java)
+ }
+}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/instance/GetLocalInstanceApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/instance/GetLocalInstanceApplicationService.kt
index b96b7e06..cdca8b84 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/instance/GetLocalInstanceApplicationService.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/instance/GetLocalInstanceApplicationService.kt
@@ -19,10 +19,11 @@ class GetLocalInstanceApplicationService(
transaction,
logger
) {
- var cachedInstance: Instance? = null
+ private var cachedInstance: Instance? = null
override suspend fun internalExecute(command: Unit, principal: Principal): Instance {
if (cachedInstance != null) {
+ logger.trace("Use cache {}", cachedInstance)
@Suppress("UnsafeCallOnNullableType")
return cachedInstance!!
}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/GetUserTimeline.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/GetUserTimeline.kt
new file mode 100644
index 00000000..8ebc7d15
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/GetUserTimeline.kt
@@ -0,0 +1,5 @@
+package dev.usbharu.hideout.core.application.timeline
+
+import dev.usbharu.hideout.core.domain.model.support.page.Page
+
+data class GetUserTimeline(val id: Long, val page: Page)
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/GetUserTimelineApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/GetUserTimelineApplicationService.kt
new file mode 100644
index 00000000..0d85511c
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/timeline/GetUserTimelineApplicationService.kt
@@ -0,0 +1,53 @@
+package dev.usbharu.hideout.core.application.timeline
+
+import dev.usbharu.hideout.core.application.post.PostDetail
+import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
+import dev.usbharu.hideout.core.application.shared.Transaction
+import dev.usbharu.hideout.core.domain.model.actor.ActorId
+import dev.usbharu.hideout.core.domain.model.post.PostId
+import dev.usbharu.hideout.core.domain.model.post.PostRepository
+import dev.usbharu.hideout.core.domain.model.post.Visibility
+import dev.usbharu.hideout.core.domain.model.support.page.PaginationList
+import dev.usbharu.hideout.core.domain.model.support.principal.Principal
+import dev.usbharu.hideout.core.query.usertimeline.UserTimelineQueryService
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Service
+
+@Service
+class GetUserTimelineApplicationService(
+ private val userTimelineQueryService: UserTimelineQueryService,
+ private val postRepository: PostRepository,
+ transaction: Transaction
+) :
+ AbstractApplicationService>(transaction, logger) {
+ override suspend fun internalExecute(
+ command: GetUserTimeline,
+ principal: Principal
+ ): PaginationList {
+ val postList = postRepository.findByActorIdAndVisibilityInList(
+ ActorId(command.id),
+ listOf(Visibility.PUBLIC, Visibility.UNLISTED, Visibility.FOLLOWERS),
+ command.page
+ )
+
+ val postIdList =
+ postList.mapNotNull { it.repostId } + postList.mapNotNull { it.replyId } + postList.map { it.id }
+
+ val postDetailMap = userTimelineQueryService.findByIdAll(postIdList, principal).associateBy { it.id }
+
+ return PaginationList(
+ postList.mapNotNull {
+ postDetailMap[it.id.id]?.copy(
+ repost = postDetailMap[it.repostId?.id],
+ reply = postDetailMap[it.replyId?.id]
+ )
+ },
+ postList.next,
+ postList.prev
+ )
+ }
+
+ companion object {
+ private val logger = LoggerFactory.getLogger(GetUserTimelineApplicationService::class.java)
+ }
+}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/MessageSourceConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/MessageSourceConfig.kt
new file mode 100644
index 00000000..dcfecd4b
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/MessageSourceConfig.kt
@@ -0,0 +1,26 @@
+package dev.usbharu.hideout.core.config
+
+import org.springframework.boot.autoconfigure.context.MessageSourceProperties
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.context.MessageSource
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Profile
+import org.springframework.context.support.ReloadableResourceBundleMessageSource
+
+@Configuration
+@Profile("dev")
+class MessageSourceConfig {
+ @Bean
+ fun messageSource(messageSourceProperties: MessageSourceProperties): MessageSource {
+ val reloadableResourceBundleMessageSource = ReloadableResourceBundleMessageSource()
+ reloadableResourceBundleMessageSource.setBasename("classpath:" + messageSourceProperties.basename)
+ reloadableResourceBundleMessageSource.setCacheSeconds(0)
+ return reloadableResourceBundleMessageSource
+ }
+
+ @Bean
+ @Profile("dev")
+ @ConfigurationProperties(prefix = "spring.messages")
+ fun messageSourceProperties(): MessageSourceProperties = MessageSourceProperties()
+}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt
index 2eccad56..d0b071ca 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt
@@ -68,7 +68,7 @@ class SecurityConfig {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
http {
exceptionHandling {
- authenticationEntryPoint = LoginUrlAuthenticationEntryPoint("/login")
+ authenticationEntryPoint = LoginUrlAuthenticationEntryPoint("/auth/sign_in")
}
}
return http.build()
@@ -80,20 +80,29 @@ class SecurityConfig {
http {
authorizeHttpRequests {
authorize("/error", permitAll)
- authorize("/login", permitAll)
+ authorize("/auth/sign_in", permitAll)
authorize(GET, "/.well-known/**", permitAll)
authorize(GET, "/nodeinfo/2.0", permitAll)
authorize(GET, "/auth/sign_up", hasRole("ANONYMOUS"))
authorize(POST, "/auth/sign_up", permitAll)
authorize(GET, "/users/{username}/posts/{postId}", permitAll)
+ authorize(GET, "/users/{userid}", permitAll)
authorize(GET, "/files/*", permitAll)
authorize(POST, "/publish", authenticated)
authorize(GET, "/publish", authenticated)
+ authorize(GET, "/", permitAll)
authorize(anyRequest, authenticated)
}
formLogin {
+ loginPage = "/auth/sign_in"
+ loginProcessingUrl = "/login"
+ defaultSuccessUrl("/home", false)
+ }
+ logout {
+ logoutUrl = "/auth/sign_out"
+ logoutSuccessUrl = "/auth/sign_in"
}
}
return http.build()
@@ -131,6 +140,7 @@ class SecurityConfig {
}
@Bean
+ @Suppress("UnsafeCallOnNullableType")
fun loadJwkSource(jwkConfig: JwkConfig, applicationConfig: ApplicationConfig): JWKSource {
if (jwkConfig.keyId == null) {
logger.error("hideout.security.jwt.keyId is null.")
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SpringMvcConfig.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SpringMvcConfig.kt
index ee75a0ef..301762d4 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SpringMvcConfig.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SpringMvcConfig.kt
@@ -16,20 +16,29 @@
package dev.usbharu.hideout.core.config
+import dev.usbharu.hideout.core.infrastructure.springframework.SPAInterceptor
import dev.usbharu.hideout.generate.JsonOrFormModelMethodProcessor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.web.method.support.HandlerMethodArgumentResolver
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor
@Configuration
-class MvcConfigurer(private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor) : WebMvcConfigurer {
+class MvcConfigurer(
+ private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor,
+ private val spaInterceptor: SPAInterceptor
+) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList) {
resolvers.add(jsonOrFormModelMethodProcessor)
}
+
+ override fun addInterceptors(registry: InterceptorRegistry) {
+ registry.addInterceptor(spaInterceptor)
+ }
}
@Configuration
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/acct/Acct.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/acct/Acct.kt
index 8be92e3a..148e01ca 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/acct/Acct.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/support/acct/Acct.kt
@@ -5,4 +5,14 @@ data class Acct(
val host: String
) {
override fun toString(): String = "acct:$userpart@$host"
+
+ companion object {
+
+ fun of(acct: String): Acct {
+ return Acct(
+ acct.substringBeforeLast('@'),
+ acct.substringAfterLast('@', "")
+ )
+ }
+ }
}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailHashedPassword.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailHashedPassword.kt
index 8e5edf56..2e73d911 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailHashedPassword.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailHashedPassword.kt
@@ -18,7 +18,5 @@ package dev.usbharu.hideout.core.domain.model.userdetails
@JvmInline
value class UserDetailHashedPassword(val password: String) {
- override fun toString(): String {
- return "[MASKED]"
- }
+ override fun toString(): String = "[MASKED]"
}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailId.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailId.kt
index 0641cd6e..23faa150 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailId.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/userdetails/UserDetailId.kt
@@ -18,7 +18,5 @@ package dev.usbharu.hideout.core.domain.model.userdetails
@JvmInline
value class UserDetailId(val id: Long) {
- override fun toString(): String {
- return "UserDetailId(id=$id)"
- }
+ override fun toString(): String = "UserDetailId(id=$id)"
}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/ExposedUserTimelineQueryService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/ExposedUserTimelineQueryService.kt
new file mode 100644
index 00000000..82be6f96
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedquery/ExposedUserTimelineQueryService.kt
@@ -0,0 +1,110 @@
+package dev.usbharu.hideout.core.infrastructure.exposedquery
+
+import dev.usbharu.hideout.core.application.post.ActorDetail
+import dev.usbharu.hideout.core.application.post.MediaDetail
+import dev.usbharu.hideout.core.application.post.PostDetail
+import dev.usbharu.hideout.core.domain.model.post.PostId
+import dev.usbharu.hideout.core.domain.model.post.Visibility
+import dev.usbharu.hideout.core.domain.model.support.principal.Principal
+import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
+import dev.usbharu.hideout.core.query.usertimeline.UserTimelineQueryService
+import org.jetbrains.exposed.sql.*
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Repository
+import java.net.URI
+
+@Repository
+class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractRepository() {
+
+ override val logger: Logger
+ get() = Companion.logger
+
+ protected fun authorizedQuery(principal: Principal? = null): QueryAlias {
+ if (principal == null) {
+ return Posts
+ .selectAll()
+ .where {
+ Posts.visibility eq Visibility.PUBLIC.name or (Posts.visibility eq Visibility.UNLISTED.name)
+ }.alias("authorized_table")
+ }
+
+ val relationshipsAlias = Relationships.alias("inverse_relationships")
+
+ return Posts
+ .leftJoin(PostsVisibleActors)
+ .leftJoin(Relationships, onColumn = { Posts.actorId }, otherColumn = { actorId })
+ .leftJoin(
+ relationshipsAlias,
+ onColumn = { Posts.actorId },
+ otherColumn = { relationshipsAlias[Relationships.targetActorId] }
+ )
+ .select(Posts.columns)
+ .where {
+ Posts.visibility eq Visibility.PUBLIC.name or
+ (Posts.visibility eq Visibility.UNLISTED.name) or
+ (Posts.visibility eq Visibility.DIRECT.name and (PostsVisibleActors.actorId eq principal.actorId.id)) or
+ (Posts.visibility eq Visibility.FOLLOWERS.name and (Relationships.blocking eq false and (relationshipsAlias[Relationships.following] eq true))) or
+ (Posts.actorId eq principal.actorId.id)
+ }
+ .alias("authorized_table")
+ }
+
+ override suspend fun findByIdAll(idList: List, principal: Principal): List {
+ val authorizedQuery = authorizedQuery(principal)
+
+ val iconMedia = Media.alias("ICON_MEDIA")
+
+ return authorizedQuery
+ .leftJoin(PostsVisibleActors, { authorizedQuery[Posts.id] }, { PostsVisibleActors.postId })
+ .leftJoin(Actors, { authorizedQuery[Posts.actorId] }, { Actors.id })
+ .leftJoin(iconMedia, { Actors.icon }, { iconMedia[Media.id] })
+ .leftJoin(PostsMedia, { authorizedQuery[Posts.id] }, { PostsMedia.postId })
+ .leftJoin(Media, { PostsMedia.mediaId }, { Media.id })
+ .selectAll()
+ .where { authorizedQuery[Posts.id] inList idList.map { it.id } }
+ .groupBy { it[authorizedQuery[Posts.id]] }
+ .map { it.value }
+ .map {
+ toPostDetail(it.first(), authorizedQuery, iconMedia).copy(
+ mediaDetailList = it.mapNotNull { resultRow ->
+ resultRow.toMediaOrNull()?.let { it1 -> MediaDetail.of(it1) }
+ }
+ )
+ }
+ }
+
+ private fun toPostDetail(it: ResultRow, authorizedQuery: QueryAlias, iconMedia: Alias): PostDetail {
+ return PostDetail(
+ id = it[authorizedQuery[Posts.id]],
+ actor = ActorDetail(
+ actorId = it[authorizedQuery[Posts.actorId]],
+ instanceId = it[Actors.instance],
+ name = it[Actors.name],
+ domain = it[Actors.domain],
+ screenName = it[Actors.screenName],
+ url = URI.create(it[Actors.url]),
+ locked = it[Actors.locked],
+ icon = it.getOrNull(iconMedia[Media.url])?.let { URI.create(it) }
+ ),
+ overview = it[authorizedQuery[Posts.overview]],
+ text = it[authorizedQuery[Posts.text]],
+ content = it[authorizedQuery[Posts.content]],
+ createdAt = it[authorizedQuery[Posts.createdAt]],
+ visibility = Visibility.valueOf(it[authorizedQuery[Posts.visibility]]),
+ pureRepost = false,
+ url = URI.create(it[authorizedQuery[Posts.url]]),
+ apId = URI.create(it[authorizedQuery[Posts.apId]]),
+ repost = null,
+ reply = null,
+ sensitive = it[authorizedQuery[Posts.sensitive]],
+ deleted = it[authorizedQuery[Posts.deleted]],
+ mediaDetailList = emptyList(),
+ moveTo = null
+ )
+ }
+
+ companion object {
+ private val logger = LoggerFactory.getLogger(ExposedUserTimelineQueryService::class.java)
+ }
+}
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 8dc2184a..9408d01a 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
@@ -211,17 +211,38 @@ class ExposedPostRepository(
visibilityList: List,
of: Page?
): PaginationList {
+ val postList = query {
+ val query = Posts
+ .selectAll()
+ .where {
+ Posts.actorId eq actorId.id and (visibility inList visibilityList.map { it.name })
+ }
+
+ if (of?.minId != null) {
+ query.orderBy(Posts.createdAt, SortOrder.ASC)
+ of.minId?.let { query.andWhere { Posts.id greater it } }
+ of.maxId?.let { query.andWhere { Posts.id less it } }
+ } else {
+ query.orderBy(Posts.createdAt, SortOrder.DESC)
+ of?.sinceId?.let { query.andWhere { Posts.id greater it } }
+ of?.maxId?.let { query.andWhere { Posts.id less it } }
+ }
+
+ of?.limit?.let { query.limit(it) }
+
+ query.let(postQueryMapper::map)
+ }
+
+ val posts = if (of?.minId != null) {
+ postList.reversed()
+ } else {
+ postList
+ }
+
return PaginationList(
- query {
- Posts
- .selectAll()
- .where {
- Posts.actorId eq actorId.id and (visibility inList visibilityList.map { it.name })
- }
- .let(postQueryMapper::map)
- },
- null,
- null
+ posts,
+ posts.lastOrNull()?.id,
+ posts.firstOrNull()?.id
)
}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt
index d5bd5f2a..0f145fbe 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/MediaRepositoryImpl.kt
@@ -108,6 +108,23 @@ fun ResultRow.toMedia(): EntityMedia {
)
}
+fun ResultRow.toMediaOrNull(): EntityMedia? {
+ val fileType = FileType.valueOf(this.getOrNull(Media.type) ?: return null)
+ val mimeType = this.getOrNull(Media.mimeType) ?: return null
+ return EntityMedia(
+ id = MediaId(this.getOrNull(Media.id) ?: return null),
+ name = MediaName(this.getOrNull(Media.name) ?: return null),
+ url = URI.create(this.getOrNull(Media.url) ?: return null),
+ remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) },
+ thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) },
+ type = FileType.valueOf(this[Media.type]),
+ blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) },
+ mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType),
+ description = this[Media.description]?.let { MediaDescription(it) },
+ actorId = ActorId(this[Media.actorId])
+ )
+}
+
object Media : Table("media") {
val id = long("id")
val name = varchar("name", 255)
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/SPAInterceptor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/SPAInterceptor.kt
new file mode 100644
index 00000000..b3a6eeb0
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/SPAInterceptor.kt
@@ -0,0 +1,43 @@
+package dev.usbharu.hideout.core.infrastructure.springframework
+
+import dev.usbharu.hideout.core.interfaces.web.common.OGP
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.springframework.stereotype.Component
+import org.springframework.web.servlet.HandlerInterceptor
+import org.springframework.web.servlet.ModelAndView
+
+@Component
+class SPAInterceptor : HandlerInterceptor {
+
+ override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
+ if (request.getParameter("s") == "f") {
+ request.session.setAttribute("s", "f")
+ } else if (request.getParameter("s") == "t") {
+ request.session.setAttribute("s", "t")
+ }
+ return true
+ }
+
+ override fun postHandle(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ handler: Any,
+ modelAndView: ModelAndView?
+ ) {
+ if (modelAndView?.viewName == "error") {
+ return
+ }
+
+ if (request.session.getAttribute("s") == "f") {
+ return
+ }
+
+ val ogp = modelAndView?.modelMap?.get("ogp") as? OGP
+
+ modelAndView?.clear()
+ modelAndView?.addObject("nsUrl", request.requestURI + "?s=f" + request.queryString?.let { "&$it" }.orEmpty())
+ modelAndView?.addObject("ogp", ogp)
+ modelAndView?.viewName = "index"
+ }
+}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/IndexController.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/IndexController.kt
new file mode 100644
index 00000000..6f181ce4
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/IndexController.kt
@@ -0,0 +1,28 @@
+package dev.usbharu.hideout.core.interfaces.web
+
+import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
+import dev.usbharu.hideout.core.config.ApplicationConfig
+import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
+import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
+import org.springframework.stereotype.Controller
+import org.springframework.ui.Model
+import org.springframework.web.bind.annotation.GetMapping
+
+@Controller
+class IndexController(
+ private val applicationConfig: ApplicationConfig,
+ private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder,
+ private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService
+) {
+ @GetMapping("/")
+ suspend fun index(model: Model): String {
+ if (springSecurityFormLoginPrincipalContextHolder.getPrincipal().userDetailId != null) {
+ return "redirect:/home"
+ }
+
+ val instance = getLocalInstanceApplicationService.execute(Unit, Anonymous)
+ model.addAttribute("instance", instance)
+ model.addAttribute("applicationConfig", applicationConfig)
+ return "top"
+ }
+}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/AuthController.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/auth/AuthController.kt
similarity index 68%
rename from hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/AuthController.kt
rename to hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/auth/AuthController.kt
index b19f3c4a..ccda2fab 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/AuthController.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/auth/AuthController.kt
@@ -14,13 +14,16 @@
* limitations under the License.
*/
-package dev.usbharu.hideout.core.interfaces.api.auth
+package dev.usbharu.hideout.core.interfaces.web.auth
import dev.usbharu.hideout.core.application.actor.RegisterLocalActor
import dev.usbharu.hideout.core.application.actor.RegisterLocalActorApplicationService
+import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
+import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import jakarta.servlet.http.HttpServletRequest
import org.springframework.stereotype.Controller
+import org.springframework.ui.Model
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
@@ -28,11 +31,16 @@ import org.springframework.web.bind.annotation.PostMapping
@Controller
class AuthController(
+ private val applicationConfig: ApplicationConfig,
private val registerLocalActorApplicationService: RegisterLocalActorApplicationService,
+ private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService,
) {
@GetMapping("/auth/sign_up")
@Suppress("FunctionOnlyReturningConstant")
- fun signUp(): String = "sign_up"
+ suspend fun signUp(model: Model): String {
+ model.addAttribute("instance", getLocalInstanceApplicationService.execute(Unit, Anonymous))
+ return "sign_up"
+ }
@PostMapping("/auth/sign_up")
suspend fun signUp(@Validated @ModelAttribute signUpForm: SignUpForm, request: HttpServletRequest): String {
@@ -44,4 +52,14 @@ class AuthController(
request.login(signUpForm.username, signUpForm.password)
return "redirect:$uri"
}
+
+ @GetMapping("/auth/sign_in")
+ suspend fun signIn(model: Model): String {
+ model.addAttribute("applicationConfig", applicationConfig)
+ return "sign_in"
+ }
+
+ @GetMapping("/auth/sign_out")
+ @Suppress("FunctionOnlyReturningConstant")
+ fun signOut(): String = "sign_out"
}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/SignUpForm.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/auth/SignUpForm.kt
similarity index 68%
rename from hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/SignUpForm.kt
rename to hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/auth/SignUpForm.kt
index d70eb9c2..320187d8 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/api/auth/SignUpForm.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/auth/SignUpForm.kt
@@ -1,4 +1,4 @@
-package dev.usbharu.hideout.core.interfaces.api.auth
+package dev.usbharu.hideout.core.interfaces.web.auth
data class SignUpForm(
val username: String,
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/common/OGP.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/common/OGP.kt
new file mode 100644
index 00000000..e3206294
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/common/OGP.kt
@@ -0,0 +1,8 @@
+package dev.usbharu.hideout.core.interfaces.web.common
+
+data class OGP(
+ val title: String,
+ val url: String,
+ val description: String,
+ val image: String?
+)
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PublishController.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PublishController.kt
index 86eb8db7..a3313ba2 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PublishController.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PublishController.kt
@@ -13,6 +13,7 @@ import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestParam
@Controller
class PublishController(
@@ -22,7 +23,7 @@ class PublishController(
private val userRegisterLocalPostApplicationService: RegisterLocalPostApplicationService
) {
@GetMapping("/publish")
- suspend fun publish(model: Model): String {
+ suspend fun publish(model: Model, @RequestParam("reply_to") replyTo: Long?, @RequestParam repost: Long?): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
if (principal.userDetailId == null) {
@@ -35,7 +36,7 @@ class PublishController(
val userDetail = getUserDetailApplicationService.execute(GetUserDetail(principal.userDetailId!!.id), principal)
model.addAttribute("instance", instance)
model.addAttribute("user", userDetail)
- model.addAttribute("form", PublishPost())
+ model.addAttribute("form", PublishPost(reply_to = replyTo, repost = repost))
return "post-postForm"
}
@@ -50,8 +51,8 @@ class PublishController(
content = publishPost.status.orEmpty(),
overview = publishPost.overview,
visibility = Visibility.valueOf(publishPost.visibility.uppercase()),
- repostId = null,
- replyId = null,
+ repostId = publishPost.repost,
+ replyId = publishPost.reply_to,
sensitive = false,
mediaIds = emptyList()
)
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PublishPost.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PublishPost.kt
index c732d2eb..5da5aa30 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PublishPost.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/posts/PublishPost.kt
@@ -1,3 +1,10 @@
package dev.usbharu.hideout.core.interfaces.web.posts
-data class PublishPost(var status: String? = null, var overview: String? = null, var visibility: String = "PUBLIC")
+@Suppress("ConstructorParameterNaming")
+data class PublishPost(
+ var status: String? = null,
+ var overview: String? = null,
+ var visibility: String = "PUBLIC",
+ var reply_to: Long? = null,
+ var repost: Long? = null
+)
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/timeline/TimelineController.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/timeline/TimelineController.kt
index 52e83f5d..c80b999b 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/timeline/TimelineController.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/timeline/TimelineController.kt
@@ -22,9 +22,9 @@ class TimelineController(
@GetMapping("/home")
suspend fun homeTimeline(
model: Model,
- @RequestParam sinceId: String?,
- @RequestParam maxId: String?,
- @RequestParam minId: String?
+ @RequestParam("since_id") sinceId: String?,
+ @RequestParam("max_id") maxId: String?,
+ @RequestParam("min_id") minId: String?
): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
val userDetail = transaction.transaction {
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/user/UserController.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/user/UserController.kt
new file mode 100644
index 00000000..c2691652
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/interfaces/web/user/UserController.kt
@@ -0,0 +1,53 @@
+package dev.usbharu.hideout.core.interfaces.web.user
+
+import dev.usbharu.hideout.core.application.actor.GetActorDetail
+import dev.usbharu.hideout.core.application.actor.GetActorDetailApplicationService
+import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
+import dev.usbharu.hideout.core.application.timeline.GetUserTimeline
+import dev.usbharu.hideout.core.application.timeline.GetUserTimelineApplicationService
+import dev.usbharu.hideout.core.domain.model.support.acct.Acct
+import dev.usbharu.hideout.core.domain.model.support.page.Page
+import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
+import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
+import org.springframework.stereotype.Controller
+import org.springframework.ui.Model
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestParam
+
+@Controller
+class UserController(
+ private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService,
+ private val getUserDetailApplicationService: GetActorDetailApplicationService,
+ private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder,
+ private val getUserTimelineApplicationService: GetUserTimelineApplicationService
+) {
+ @GetMapping("/users/{name}")
+ suspend fun userById(
+ @PathVariable name: String,
+ @RequestParam("min_id") minId: Long?,
+ @RequestParam("max_id") maxId: Long?,
+ @RequestParam("since_id") sinceId: Long?,
+ model: Model
+ ): String {
+ val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
+
+ model.addAttribute("instance", getLocalInstanceApplicationService.execute(Unit, Anonymous))
+ val actorDetail = getUserDetailApplicationService.execute(GetActorDetail(Acct.of(name)), principal)
+ model.addAttribute(
+ "user",
+ actorDetail
+ )
+ model.addAttribute(
+ "userTimeline",
+ getUserTimelineApplicationService.execute(
+ GetUserTimeline(
+ actorDetail.id,
+ Page.of(maxId, sinceId, minId, 20)
+ ),
+ principal
+ )
+ )
+ return "userById"
+ }
+}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/usertimeline/UserTimelineQueryService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/usertimeline/UserTimelineQueryService.kt
new file mode 100644
index 00000000..2981cfa7
--- /dev/null
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/query/usertimeline/UserTimelineQueryService.kt
@@ -0,0 +1,12 @@
+package dev.usbharu.hideout.core.query.usertimeline
+
+import dev.usbharu.hideout.core.application.post.PostDetail
+import dev.usbharu.hideout.core.domain.model.post.PostId
+import dev.usbharu.hideout.core.domain.model.support.principal.Principal
+
+interface UserTimelineQueryService {
+ /**
+ * replyやrepost等はnullになります
+ */
+ suspend fun findByIdAll(idList: List, principal: Principal): List
+}
diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt
index 4af71576..8f9d0bfd 100644
--- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt
+++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt
@@ -45,11 +45,7 @@ object RsaUtil {
fun decodeRsaPrivateKey(encoded: String): RSAPrivateKey = decodeRsaPrivateKey(Base64Util.decode(encoded))
- fun encodeRsaPublicKey(publicKey: RSAPublicKey): String {
- return Base64Util.encode(publicKey.encoded)
- }
+ fun encodeRsaPublicKey(publicKey: RSAPublicKey): String = Base64Util.encode(publicKey.encoded)
- fun encodeRsaPrivateKey(privateKey: RSAPrivateKey): String {
- return Base64Util.encode(privateKey.encoded)
- }
+ fun encodeRsaPrivateKey(privateKey: RSAPrivateKey): String = Base64Util.encode(privateKey.encoded)
}
diff --git a/hideout-core/src/main/resources/application-dev.yml b/hideout-core/src/main/resources/application-dev.yml
index a6d4118b..2c19a35e 100644
--- a/hideout-core/src/main/resources/application-dev.yml
+++ b/hideout-core/src/main/resources/application-dev.yml
@@ -29,7 +29,8 @@ spring:
virtual:
enabled: true
messages:
- basename: messages.hideout-web-messages
+ basename: messages/hideout-web-messages
+ cache-duration: -1
thymeleaf:
cache: false
server:
diff --git a/hideout-core/src/main/resources/messages/hideout-web-messages.properties b/hideout-core/src/main/resources/messages/hideout-web-messages.properties
index c22c2992..d2c0de78 100644
--- a/hideout-core/src/main/resources/messages/hideout-web-messages.properties
+++ b/hideout-core/src/main/resources/messages/hideout-web-messages.properties
@@ -1,3 +1,7 @@
+auth-signIn.title=\u30ED\u30B0\u30A4\u30F3 - {0}
+auth-signUp.password=\u30D1\u30B9\u30EF\u30FC\u30C9
+auth-signUp.register=\u767B\u9332\u3059\u308B
+auth-signUp.username=\u30E6\u30FC\u30B6\u30FC\u540D
common.audio=\u30AA\u30FC\u30C7\u30A3\u30AA
common.audio-download-link=\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.empty=\u8868\u793A\u3059\u308B\u3082\u306E\u304C\u3042\u308A\u307E\u305B\u3093
@@ -16,6 +20,12 @@ post-form.new-posts=\u65B0\u3057\u3044\u6295\u7A3F!
post-form.new-posts-cw=CW
post-form.new-posts-cw-title=\u30B3\u30F3\u30C6\u30F3\u30C4\u306B\u95B2\u89A7\u6CE8\u610F\u3092\u3064\u3051\u308B
post-form.new-posts-form-label=\u4ECA\u306A\u306B\u3057\u3066\u308B?
+post-form.new-posts-replyTo=\u8FD4\u4FE1\u5148
post-form.new-posts-submit=\u6295\u7A3F\u3059\u308B
+post-form.new-posts.repost=\u30EA\u30DD\u30B9\u30C8
post.repost=\u30EA\u30DD\u30B9\u30C8
-post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8
\ No newline at end of file
+post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8
+user-by-id.followersCount={0} \u30D5\u30A9\u30ED\u30EF\u30FC
+user-by-id.followingCount={0} \u30D5\u30A9\u30ED\u30FC\u4E2D
+user-by-id.postsCount={0} \u6295\u7A3F
+user-by-id.title={0} \u3055\u3093 - {1}
\ No newline at end of file
diff --git a/hideout-core/src/main/resources/messages/hideout-web-messages_en_US.properties b/hideout-core/src/main/resources/messages/hideout-web-messages_en_US.properties
index f32efecd..e19ac2b5 100644
--- a/hideout-core/src/main/resources/messages/hideout-web-messages_en_US.properties
+++ b/hideout-core/src/main/resources/messages/hideout-web-messages_en_US.properties
@@ -1,3 +1,7 @@
+auth-signIn.title=Sign in - {0}
+auth-signUp.password=Password
+auth-signUp.register=Register Account
+auth-signUp.username=Username
common.audio=Audio
common.audio-download-link=Download the audio.
common.empty=Empty
@@ -15,6 +19,11 @@ post-form.new-posts=New Posts!
post-form.new-posts-cw=CW
post-form.new-posts-cw-title=Add content warning
post-form.new-posts-form-label=What's on your mind?
+post-form.new-posts-replyTo=Reply to
post-form.new-posts-submit=Submit!
+post-form.new-posts.repost=Repost
post.repost=Repost
-post.repost-by=Repost by {0}
\ No newline at end of file
+post.repost-by=Repost by {0}
+user-by-id.followersCount={0} Followers
+user-by-id.followingCount={0} Following
+user-by-id.postsCount={0} Posts
\ No newline at end of file
diff --git a/hideout-core/src/main/resources/messages/hideout-web-messages_ja_JP.properties b/hideout-core/src/main/resources/messages/hideout-web-messages_ja_JP.properties
index c22c2992..d2c0de78 100644
--- a/hideout-core/src/main/resources/messages/hideout-web-messages_ja_JP.properties
+++ b/hideout-core/src/main/resources/messages/hideout-web-messages_ja_JP.properties
@@ -1,3 +1,7 @@
+auth-signIn.title=\u30ED\u30B0\u30A4\u30F3 - {0}
+auth-signUp.password=\u30D1\u30B9\u30EF\u30FC\u30C9
+auth-signUp.register=\u767B\u9332\u3059\u308B
+auth-signUp.username=\u30E6\u30FC\u30B6\u30FC\u540D
common.audio=\u30AA\u30FC\u30C7\u30A3\u30AA
common.audio-download-link=\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.empty=\u8868\u793A\u3059\u308B\u3082\u306E\u304C\u3042\u308A\u307E\u305B\u3093
@@ -16,6 +20,12 @@ post-form.new-posts=\u65B0\u3057\u3044\u6295\u7A3F!
post-form.new-posts-cw=CW
post-form.new-posts-cw-title=\u30B3\u30F3\u30C6\u30F3\u30C4\u306B\u95B2\u89A7\u6CE8\u610F\u3092\u3064\u3051\u308B
post-form.new-posts-form-label=\u4ECA\u306A\u306B\u3057\u3066\u308B?
+post-form.new-posts-replyTo=\u8FD4\u4FE1\u5148
post-form.new-posts-submit=\u6295\u7A3F\u3059\u308B
+post-form.new-posts.repost=\u30EA\u30DD\u30B9\u30C8
post.repost=\u30EA\u30DD\u30B9\u30C8
-post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8
\ No newline at end of file
+post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8
+user-by-id.followersCount={0} \u30D5\u30A9\u30ED\u30EF\u30FC
+user-by-id.followingCount={0} \u30D5\u30A9\u30ED\u30FC\u4E2D
+user-by-id.postsCount={0} \u6295\u7A3F
+user-by-id.title={0} \u3055\u3093 - {1}
\ 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 5b9533b6..e55f55be 100644
--- a/hideout-core/src/main/resources/templates/fragments-post.html
+++ b/hideout-core/src/main/resources/templates/fragments-post.html
@@ -11,7 +11,8 @@
@@ -43,6 +44,7 @@
+
Reply
diff --git a/hideout-core/src/main/resources/templates/fragments-timeline.html b/hideout-core/src/main/resources/templates/fragments-timeline.html
index f08d6715..ef29250c 100644
--- a/hideout-core/src/main/resources/templates/fragments-timeline.html
+++ b/hideout-core/src/main/resources/templates/fragments-timeline.html
@@ -8,7 +8,7 @@
@@ -16,7 +16,7 @@
-
+
diff --git a/hideout-core/src/main/resources/templates/homeTimeline.html b/hideout-core/src/main/resources/templates/homeTimeline.html
index 60bc762b..c1f982ac 100644
--- a/hideout-core/src/main/resources/templates/homeTimeline.html
+++ b/hideout-core/src/main/resources/templates/homeTimeline.html
@@ -1,12 +1,10 @@
-
+