Add live preview for posts
This commit is contained in:
parent
74c297263e
commit
32596228fe
|
@ -88,8 +88,6 @@ kotlin {
|
||||||
|
|
||||||
implementation "de.westermann:KObserve-jvm:$observable_version"
|
implementation "de.westermann:KObserve-jvm:$observable_version"
|
||||||
|
|
||||||
//api 'org.jetbrains:markdown:0.1.28'
|
|
||||||
//implementation 'com.atlassian.commonmark:commonmark:0.12.1'
|
|
||||||
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
|
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
|
||||||
api 'io.github.microutils:kotlin-logging:1.6.23'
|
api 'io.github.microutils:kotlin-logging:1.6.23'
|
||||||
api 'ch.qos.logback:logback-classic:1.2.3'
|
api 'ch.qos.logback:logback-classic:1.2.3'
|
||||||
|
|
|
@ -4,6 +4,7 @@ import de.kif.frontend.views.calendar.initCalendar
|
||||||
import de.kif.frontend.views.table.initTableLayout
|
import de.kif.frontend.views.table.initTableLayout
|
||||||
import de.kif.frontend.views.initWorkGroupConstraints
|
import de.kif.frontend.views.initWorkGroupConstraints
|
||||||
import de.kif.frontend.views.overview.initOverviewMain
|
import de.kif.frontend.views.overview.initOverviewMain
|
||||||
|
import de.kif.frontend.views.overview.initPostEdit
|
||||||
import de.kif.frontend.views.overview.initPosts
|
import de.kif.frontend.views.overview.initPosts
|
||||||
import de.westermann.kwebview.components.init
|
import de.westermann.kwebview.components.init
|
||||||
import kotlin.browser.document
|
import kotlin.browser.document
|
||||||
|
@ -26,4 +27,7 @@ fun main() = init {
|
||||||
if (document.getElementsByClassName("post").length > 0) {
|
if (document.getElementsByClassName("post").length > 0) {
|
||||||
initPosts()
|
initPosts()
|
||||||
}
|
}
|
||||||
|
if (document.getElementsByClassName("post-edit-right").length > 0) {
|
||||||
|
initPostEdit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,10 @@ object PostRepository : Repository<Post> {
|
||||||
return repositoryRawGet("/api/p/$url")
|
return repositoryRawGet("/api/p/$url")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun render(data: String): String {
|
||||||
|
return repositoryPost("/api/render", data)
|
||||||
|
}
|
||||||
|
|
||||||
val handler = object : MessageHandler(RepositoryType.POST) {
|
val handler = object : MessageHandler(RepositoryType.POST) {
|
||||||
|
|
||||||
override fun onCreate(id: Long) = onCreate.emit(id)
|
override fun onCreate(id: Long) = onCreate.emit(id)
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package de.kif.frontend.views.overview
|
package de.kif.frontend.views.overview
|
||||||
|
|
||||||
import de.kif.frontend.iterator
|
import de.kif.frontend.iterator
|
||||||
|
import de.kif.frontend.launch
|
||||||
import de.kif.frontend.repository.PostRepository
|
import de.kif.frontend.repository.PostRepository
|
||||||
import org.w3c.dom.HTMLElement
|
import org.w3c.dom.HTMLElement
|
||||||
|
import org.w3c.dom.HTMLTextAreaElement
|
||||||
|
import org.w3c.dom.events.EventListener
|
||||||
import org.w3c.dom.get
|
import org.w3c.dom.get
|
||||||
import kotlin.browser.document
|
import kotlin.browser.document
|
||||||
|
|
||||||
|
@ -38,4 +41,24 @@ fun initPosts() {
|
||||||
for (post in postList) {
|
for (post in postList) {
|
||||||
PostView(post)
|
PostView(post)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initPostEdit() {
|
||||||
|
val textArea = document.getElementById("content") as HTMLTextAreaElement
|
||||||
|
val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement
|
||||||
|
|
||||||
|
textArea.addEventListener("change", EventListener {
|
||||||
|
launch {
|
||||||
|
preview.innerHTML = PostRepository.render(textArea.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
textArea.addEventListener("keyup", EventListener {
|
||||||
|
launch {
|
||||||
|
preview.innerHTML = PostRepository.render(textArea.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
launch {
|
||||||
|
preview.innerHTML = PostRepository.render(textArea.value)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1001,6 +1001,7 @@ form {
|
||||||
color: $text-primary-color;
|
color: $text-primary-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-edit {
|
.post-edit {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -1013,26 +1014,68 @@ form {
|
||||||
margin: 0.7rem 0;
|
margin: 0.7rem 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: rgba($text-primary-color, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
padding: 0.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-edit-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-edit-right {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.post-edit-container {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.post-edit-right {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
.overview-post {
|
.overview-post {
|
||||||
max-height: 20rem;
|
max-height: 20rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -1047,4 +1090,5 @@ form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: linear-gradient(0deg, $background-primary-color, transparent);
|
background: linear-gradient(0deg, $background-primary-color, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -12,9 +12,7 @@ import io.ktor.http.content.static
|
||||||
import io.ktor.jackson.jackson
|
import io.ktor.jackson.jackson
|
||||||
import io.ktor.routing.routing
|
import io.ktor.routing.routing
|
||||||
import io.ktor.websocket.WebSockets
|
import io.ktor.websocket.WebSockets
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
|
|
||||||
@ImplicitReflectionSerializer
|
|
||||||
fun Application.main() {
|
fun Application.main() {
|
||||||
install(DefaultHeaders)
|
install(DefaultHeaders)
|
||||||
install(CallLogging)
|
install(CallLogging)
|
||||||
|
|
|
@ -8,10 +8,8 @@ import io.ktor.application.Application
|
||||||
import io.ktor.server.engine.embeddedServer
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.server.netty.Netty
|
import io.ktor.server.netty.Netty
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
|
|
||||||
object Main {
|
object Main {
|
||||||
@ImplicitReflectionSerializer
|
|
||||||
@Suppress("UnusedMainParameter")
|
@Suppress("UnusedMainParameter")
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
|
|
@ -136,63 +136,70 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
content {
|
content {
|
||||||
h1 { +"Edit post" }
|
h1 { +"Edit post" }
|
||||||
form(method = FormMethod.post) {
|
div("post-edit-container") {
|
||||||
div("form-group") {
|
div("post-edit-left") {
|
||||||
label {
|
form(method = FormMethod.post) {
|
||||||
htmlFor = "name"
|
div("form-group") {
|
||||||
+"Name"
|
label {
|
||||||
}
|
htmlFor = "name"
|
||||||
input(
|
+"Name"
|
||||||
name = "name",
|
}
|
||||||
classes = "form-control"
|
input(
|
||||||
) {
|
name = "name",
|
||||||
id = "name"
|
classes = "form-control"
|
||||||
placeholder = "Name"
|
) {
|
||||||
value = editPost.name
|
id = "name"
|
||||||
}
|
placeholder = "Name"
|
||||||
}
|
value = editPost.name
|
||||||
div("form-group") {
|
}
|
||||||
label {
|
}
|
||||||
htmlFor = "url"
|
div("form-group") {
|
||||||
+"Url"
|
label {
|
||||||
}
|
htmlFor = "url"
|
||||||
input(
|
+"Url"
|
||||||
name = "url",
|
}
|
||||||
classes = "form-control"
|
input(
|
||||||
) {
|
name = "url",
|
||||||
id = "places"
|
classes = "form-control"
|
||||||
placeholder = "Places"
|
) {
|
||||||
value = editPost.url
|
id = "places"
|
||||||
}
|
placeholder = "Places"
|
||||||
}
|
value = editPost.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
label {
|
label {
|
||||||
htmlFor = "content"
|
htmlFor = "content"
|
||||||
+"Content"
|
+"Content"
|
||||||
}
|
}
|
||||||
textArea(rows = "10", classes = "form-control") {
|
textArea(rows = "10", classes = "form-control") {
|
||||||
name = "content"
|
name = "content"
|
||||||
id = "projector"
|
id = "content"
|
||||||
|
|
||||||
+editPost.content
|
+editPost.content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
a("/") {
|
a("/") {
|
||||||
button(classes = "form-btn") {
|
button(classes = "form-btn") {
|
||||||
+"Cancel"
|
+"Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
||||||
|
+"Save"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
a("/post/${editPost.id}/delete") {
|
||||||
+"Save"
|
button(classes = "form-btn btn-danger") {
|
||||||
|
+"Delete"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
div("post-edit-right post-content") {
|
||||||
a("/post/${editPost.id}/delete") {
|
|
||||||
button(classes = "form-btn btn-danger") {
|
|
||||||
+"Delete"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,58 +234,65 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
content {
|
content {
|
||||||
h1 { +"Create post" }
|
h1 { +"Create post" }
|
||||||
form(method = FormMethod.post) {
|
div("post-edit-container") {
|
||||||
div("form-group") {
|
div("post-edit-left") {
|
||||||
label {
|
form(method = FormMethod.post) {
|
||||||
htmlFor = "name"
|
div("form-group") {
|
||||||
+"Name"
|
label {
|
||||||
}
|
htmlFor = "name"
|
||||||
input(
|
+"Name"
|
||||||
name = "name",
|
}
|
||||||
classes = "form-control"
|
input(
|
||||||
) {
|
name = "name",
|
||||||
id = "name"
|
classes = "form-control"
|
||||||
placeholder = "Name"
|
) {
|
||||||
value = ""
|
id = "name"
|
||||||
}
|
placeholder = "Name"
|
||||||
}
|
value = ""
|
||||||
div("form-group") {
|
}
|
||||||
label {
|
}
|
||||||
htmlFor = "url"
|
div("form-group") {
|
||||||
+"Url"
|
label {
|
||||||
}
|
htmlFor = "url"
|
||||||
input(
|
+"Url"
|
||||||
name = "url",
|
}
|
||||||
classes = "form-control"
|
input(
|
||||||
) {
|
name = "url",
|
||||||
id = "places"
|
classes = "form-control"
|
||||||
placeholder = "Places"
|
) {
|
||||||
value = Post.generateUrl()
|
id = "places"
|
||||||
}
|
placeholder = "Places"
|
||||||
}
|
value = Post.generateUrl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
label {
|
label {
|
||||||
htmlFor = "content"
|
htmlFor = "content"
|
||||||
+"Content"
|
+"Content"
|
||||||
}
|
}
|
||||||
textArea(rows = "10", classes = "form-control") {
|
textArea(rows = "10", classes = "form-control") {
|
||||||
name = "content"
|
name = "content"
|
||||||
id = "projector"
|
id = "content"
|
||||||
|
|
||||||
+""
|
+""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
a("/") {
|
a("/") {
|
||||||
button(classes = "form-btn") {
|
button(classes = "form-btn") {
|
||||||
+"Cancel"
|
+"Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
||||||
|
+"Create"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
}
|
||||||
+"Create"
|
div("post-edit-right post-content") {
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import io.ktor.application.call
|
||||||
import io.ktor.html.respondHtml
|
import io.ktor.html.respondHtml
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.request.receive
|
import io.ktor.request.receive
|
||||||
import io.ktor.response.respond
|
import io.ktor.request.receiveText
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.routing.get
|
import io.ktor.routing.get
|
||||||
import io.ktor.routing.post
|
import io.ktor.routing.post
|
||||||
|
@ -58,7 +58,7 @@ fun Route.postApi() {
|
||||||
|
|
||||||
post("/api/post/{id}") {
|
post("/api/post/{id}") {
|
||||||
try {
|
try {
|
||||||
authenticate {
|
authenticate(Permission.POST) {
|
||||||
val id = call.parameters["id"]?.toLongOrNull()
|
val id = call.parameters["id"]?.toLongOrNull()
|
||||||
val post = call.receive<Post>().copy(id = id)
|
val post = call.receive<Post>().copy(id = id)
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ fun Route.postApi() {
|
||||||
}
|
}
|
||||||
post("/api/post/{id}/delete") {
|
post("/api/post/{id}/delete") {
|
||||||
try {
|
try {
|
||||||
authenticate {
|
authenticate(Permission.POST) {
|
||||||
val id = call.parameters["id"]?.toLongOrNull()
|
val id = call.parameters["id"]?.toLongOrNull()
|
||||||
|
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
|
@ -112,4 +112,18 @@ fun Route.postApi() {
|
||||||
call.error(HttpStatusCode.InternalServerError)
|
call.error(HttpStatusCode.InternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post("/api/render") {
|
||||||
|
try {
|
||||||
|
authenticate(Permission.POST) {
|
||||||
|
val data = call.receiveText()
|
||||||
|
val html = markdownToHtml(data)
|
||||||
|
call.success(html)
|
||||||
|
} onFailure {
|
||||||
|
call.error(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
call.error(HttpStatusCode.InternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,59 +2,51 @@ package de.kif.backend.util
|
||||||
|
|
||||||
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
|
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
|
||||||
import com.vladsch.flexmark.ext.emoji.EmojiExtension
|
import com.vladsch.flexmark.ext.emoji.EmojiExtension
|
||||||
|
import com.vladsch.flexmark.ext.emoji.EmojiImageType
|
||||||
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
|
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
|
||||||
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
|
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
|
||||||
|
import com.vladsch.flexmark.ext.gitlab.GitLabExtension
|
||||||
import com.vladsch.flexmark.ext.tables.TablesExtension
|
import com.vladsch.flexmark.ext.tables.TablesExtension
|
||||||
import com.vladsch.flexmark.util.ast.Node;
|
import com.vladsch.flexmark.ext.typographic.TypographicExtension
|
||||||
|
import com.vladsch.flexmark.ext.youtube.embedded.YouTubeLinkExtension
|
||||||
import com.vladsch.flexmark.html.HtmlRenderer;
|
import com.vladsch.flexmark.html.HtmlRenderer;
|
||||||
import com.vladsch.flexmark.parser.Parser;
|
import com.vladsch.flexmark.parser.Parser;
|
||||||
import com.vladsch.flexmark.util.options.MutableDataSet;
|
import com.vladsch.flexmark.util.options.MutableDataSet;
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/*
|
object ParseMarkdown {
|
||||||
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
|
||||||
import org.intellij.markdown.html.HtmlGenerator
|
|
||||||
import org.intellij.markdown.parser.MarkdownParser
|
|
||||||
|
|
||||||
fun markdownToHtml(content: String): String {
|
private val parser: Parser
|
||||||
val flavour = CommonMarkFlavourDescriptor()
|
private val renderer: HtmlRenderer
|
||||||
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content)
|
|
||||||
val html = HtmlGenerator(content, parsedTree, flavour).generateHtml()
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
import org.commonmark.parser.Parser
|
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
|
||||||
|
|
||||||
fun markdownToHtml(content: String): String {
|
fun parse(content: String): String {
|
||||||
val parser = Parser.builder().build()
|
val document = parser.parse(content) ?: return ""
|
||||||
val document = parser.parse(content)
|
return renderer.render(document) ?: ""
|
||||||
val renderer = HtmlRenderer.builder().build()
|
}
|
||||||
return renderer.render(document)
|
|
||||||
|
init {
|
||||||
|
val options = MutableDataSet()
|
||||||
|
|
||||||
|
options.set(
|
||||||
|
Parser.EXTENSIONS, Arrays.asList(
|
||||||
|
TablesExtension.create(),
|
||||||
|
StrikethroughExtension.create(),
|
||||||
|
TaskListExtension.create(),
|
||||||
|
EmojiExtension.create(),
|
||||||
|
AutolinkExtension.create(),
|
||||||
|
GitLabExtension.create(),
|
||||||
|
TypographicExtension.create(),
|
||||||
|
YouTubeLinkExtension.create()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
options.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_ONLY)
|
||||||
|
|
||||||
|
//options.set(HtmlRenderer.SOFT_BREAK, "<br />\n");
|
||||||
|
|
||||||
|
parser = Parser.builder(options).build()
|
||||||
|
renderer = HtmlRenderer.builder(options).build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
fun markdownToHtml(content: String): String = ParseMarkdown.parse(content)
|
||||||
|
|
||||||
fun markdownToHtml(content: String): String {
|
|
||||||
val options = MutableDataSet()
|
|
||||||
|
|
||||||
options.set(Parser.EXTENSIONS, Arrays.asList(
|
|
||||||
TablesExtension.create(),
|
|
||||||
StrikethroughExtension.create(),
|
|
||||||
TaskListExtension.create(),
|
|
||||||
EmojiExtension.create(),
|
|
||||||
AutolinkExtension.create()
|
|
||||||
));
|
|
||||||
|
|
||||||
//options.set(HtmlRenderer.SOFT_BREAK, "<br />\n");
|
|
||||||
|
|
||||||
val parser = Parser.builder(options).build()
|
|
||||||
val renderer = HtmlRenderer.builder(options).build()
|
|
||||||
|
|
||||||
// You can re-use parser and renderer instances
|
|
||||||
val document = parser.parse(content)
|
|
||||||
val html = renderer.render(document)
|
|
||||||
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
|
@ -51,4 +51,4 @@ fun Route.pushService() {
|
||||||
UserRepository.registerPushService()
|
UserRepository.registerPushService()
|
||||||
WorkGroupRepository.registerPushService()
|
WorkGroupRepository.registerPushService()
|
||||||
PostRepository.registerPushService()
|
PostRepository.registerPushService()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue