Threaded comments (Reply UI)

This commit is contained in:
Eduard Urbach 2018-11-05 20:57:37 +09:00
parent 08497f0c37
commit 960b1e4b92
14 changed files with 133 additions and 46 deletions

View File

@ -1,7 +1,17 @@
component NewPostArea(user *arn.User, placeholder string) component NewPostArea(user *arn.User, placeholder string)
.post.mountable #new-post.post.mountable
.post-parent .post-parent
.post-author .post-author
Avatar(user) Avatar(user)
textarea#new-post.post-content(placeholder=placeholder + "...", aria-label=placeholder) textarea#new-post-text.post-content(placeholder=placeholder + "...", aria-label=placeholder)
component NewPostActions(parentType string, parentID string)
#new-post-actions.buttons
button#reply-button.mountable.action(data-action="createPost", data-trigger="click", data-parent-type=parentType, data-parent-id=parentID)
Icon("mail-reply")
span Reply
button#reply-cancel-button.mountable.action(data-action="cancelReply", data-trigger="click")
Icon("close")
span Cancel

View File

@ -14,7 +14,9 @@ component Postable(post arn.Postable, user *arn.User, includeReplies bool, heade
.post-edit-interface .post-edit-interface
if post.Type() == "Thread" if post.Type() == "Thread"
input.post-title-input.hidden(id="title-" + post.GetID(), value=post.TitleByUser(user), type="text", placeholder="Thread title") input.post-title-input.hidden(id="title-" + post.GetID(), value=post.TitleByUser(user), type="text", placeholder="Thread title")
textarea.post-text-input.hidden(id="source-" + post.GetID())= post.GetText() textarea.post-text-input.hidden(id="source-" + post.GetID())= post.GetText()
.buttons.hidden(id="edit-toolbar-" + post.GetID()) .buttons.hidden(id="edit-toolbar-" + post.GetID())
a.button.post-save.action(data-action="savePost", data-trigger="click", data-id=post.GetID()) a.button.post-save.action(data-action="savePost", data-trigger="click", data-id=post.GetID())
Icon("save") Icon("save")
@ -43,7 +45,7 @@ component Postable(post arn.Postable, user *arn.User, includeReplies bool, heade
//- a.post-tool.post-edit.tip(href=post.Link() + "/edit", aria-label="Edit") //- a.post-tool.post-edit.tip(href=post.Link() + "/edit", aria-label="Edit")
//- Icon("edit") //- Icon("edit")
a.post-tool.post-reply.tip.action(id="reply-" + post.GetID(), aria-label="Reply", data-action="reply", data-trigger="click") a.post-tool.post-reply.tip.action(data-post-id=post.GetID(), aria-label="Reply", data-action="reply", data-trigger="click")
Icon("reply") Icon("reply")
if user.ID == post.Creator().ID if user.ID == post.Creator().ID
@ -58,7 +60,7 @@ component Postable(post arn.Postable, user *arn.User, includeReplies bool, heade
a.post-tool.post-permalink.tip(href=post.Link(), aria-label="Link") a.post-tool.post-permalink.tip(href=post.Link(), aria-label="Link")
Icon("link") Icon("link")
.replies .replies(id="replies-" + post.GetID())
if includeReplies if includeReplies
each reply in post.Posts() each reply in post.Posts()
Postable(reply, user, true, "", highlightAuthorID) Postable(reply, user, true, "", highlightAuthorID)

View File

@ -3,8 +3,7 @@ component ActivityFeed(entries []*arn.EditLogEntry, user *arn.User)
.activities .activities
each entry in entries each entry in entries
.activity ActivityPost(entry.Object().(arn.Postable), user)
ActivityPost(entry.Object().(arn.Postable), user)
component ActivityPost(post arn.Postable, user *arn.User) component ActivityPost(post arn.Postable, user *arn.User)
if post.Parent() != nil if post.Parent() != nil

View File

@ -3,9 +3,3 @@
width 100% width 100%
max-width forum-width max-width forum-width
margin 0 auto margin 0 auto
// .activity
// margin-bottom 1rem
.activity-header
font-size 0.9rem

View File

@ -3,6 +3,9 @@ package apiroutes
import ( import (
"strings" "strings"
"github.com/animenotifier/notify.moe/pages/post"
"github.com/animenotifier/notify.moe/pages/thread"
"github.com/aerogo/aero" "github.com/aerogo/aero"
"github.com/aerogo/layout" "github.com/aerogo/layout"
@ -39,6 +42,12 @@ func Register(l *layout.Layout, app *aero.Application) {
app.Get("/api/next/soundtrack", soundtrack.Next) app.Get("/api/next/soundtrack", soundtrack.Next)
app.Get("/api/character/:id/ranking", character.Ranking) app.Get("/api/character/:id/ranking", character.Ranking)
// Thread
app.Get("/api/thread/:id/reply/ui", thread.ReplyUI)
// Post
app.Get("/api/post/:id/reply/ui", post.ReplyUI)
// SoundTrack // SoundTrack
app.Post("/api/soundtrack/:id/download", soundtrack.Download) app.Post("/api/soundtrack/:id/download", soundtrack.Download)

View File

@ -1,13 +0,0 @@
package post
import (
"github.com/aerogo/aero"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
)
// NewPostArea renders a new post area.
func NewPostArea(ctx *aero.Context) string {
user := utils.GetUser(ctx)
return ctx.HTML(components.NewPostArea(user, "Reply"))
}

23
pages/post/reply-ui.go Normal file
View File

@ -0,0 +1,23 @@
package post
import (
"net/http"
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
)
// ReplyUI renders a new post area.
func ReplyUI(ctx *aero.Context) string {
id := ctx.Get("id")
user := utils.GetUser(ctx)
post, err := arn.GetPost(id)
if err != nil {
return ctx.Error(http.StatusNotFound, "Post not found", err)
}
return ctx.HTML(components.NewPostArea(user, "Reply") + components.NewPostActions(post.Type(), post.ID))
}

23
pages/thread/reply-ui.go Normal file
View File

@ -0,0 +1,23 @@
package thread
import (
"net/http"
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
)
// ReplyUI renders a new post area.
func ReplyUI(ctx *aero.Context) string {
id := ctx.Get("id")
user := utils.GetUser(ctx)
thread, err := arn.GetThread(id)
if err != nil {
return ctx.Error(http.StatusNotFound, "Thread not found", err)
}
return ctx.HTML(components.NewPostArea(user, "Reply") + components.NewPostActions(thread.Type(), thread.ID))
}

View File

@ -15,9 +15,7 @@ component Thread(thread *arn.Thread, user *arn.User)
.buttons .buttons
if !thread.Locked if !thread.Locked
button.mountable.action(data-action="createPost", data-trigger="click", data-parent-type="Thread", data-parent-id=thread.ID) NewPostActions("Thread", thread.ID)
Icon("mail-reply")
span Reply
if user.Role == "admin" || user.Role == "editor" if user.Role == "admin" || user.Role == "editor"
if thread.Locked if thread.Locked

View File

@ -11,6 +11,9 @@
vertical vertical
margin-bottom 1.75rem margin-bottom 1.75rem
:last-child
margin-bottom 0
.post-author .post-author
margin-bottom 0.25rem margin-bottom 0.25rem
@ -25,9 +28,12 @@
margin-top 0.75rem margin-top 0.75rem
margin-left content-padding margin-left content-padding
:empty
margin-top 0
> 600px > 600px
.post .post
margin-bottom 0 margin-bottom 0.75rem
.post-author .post-author
margin-bottom 0 margin-bottom 0

View File

@ -58,7 +58,7 @@ export function deletePost(arn: AnimeNotifier, element: HTMLElement) {
// Create post // Create post
export function createPost(arn: AnimeNotifier, element: HTMLElement) { export function createPost(arn: AnimeNotifier, element: HTMLElement) {
let textarea = document.getElementById("new-post") as HTMLTextAreaElement let textarea = document.getElementById("new-post-text") as HTMLTextAreaElement
let {parentId, parentType} = element.dataset let {parentId, parentType} = element.dataset
let post = { let post = {
@ -92,8 +92,40 @@ export function createThread(arn: AnimeNotifier) {
} }
// Reply to a post // Reply to a post
export function reply(arn: AnimeNotifier, element: HTMLElement) { export async function reply(arn: AnimeNotifier, element: HTMLElement) {
let apiEndpoint = arn.findAPIEndpoint(element)
let repliesId = `replies-${element.dataset.postId}`
let replies = document.getElementById(repliesId)
// Delete old reply area
let oldReplyArea = document.getElementById("new-post")
if(oldReplyArea) {
oldReplyArea.remove()
}
// Delete old reply button
let oldPostActions = document.getElementById("new-post-actions")
if(oldPostActions) {
oldPostActions.remove()
}
// Fetch new reply UI
try {
let response = await fetch(`${apiEndpoint}/reply/ui`)
let html = await response.text()
replies.innerHTML = html + replies.innerHTML
arn.onNewContent(replies)
arn.assignActions()
} catch(err) {
arn.statusMessage.showError(err)
}
}
// Cancel replying to a post
export function cancelReply(arn: AnimeNotifier, element: HTMLElement) {
arn.reloadContent()
} }
// Lock thread // Lock thread

View File

@ -1,5 +1,5 @@
import AnimeNotifier from "../AnimeNotifier" import AnimeNotifier from "../AnimeNotifier"
import { delay, requestIdleCallback, findAllInside } from "../Utils" import { delay, requestIdleCallback } from "../Utils"
// Search page reference // Search page reference
var emptySearchHTML = "" var emptySearchHTML = ""
@ -172,20 +172,10 @@ function showResponseInElement(arn: AnimeNotifier, url: string, typeName: string
} }
await arn.innerHTML(element, html) await arn.innerHTML(element, html)
arn.onNewContent(element)
showSearchResults(arn, element)
} }
} }
export function showSearchResults(arn: AnimeNotifier, element: HTMLElement) {
// Do the same as for the content loaded event,
// except here we are limiting it to the element.
arn.app.ajaxify(element.getElementsByTagName("a"))
arn.lazyLoad(findAllInside("lazy", element))
arn.mountMountables(findAllInside("mountable", element))
arn.assignTooltipOffsets(findAllInside("tip", element))
}
export function searchBySpeech(arn: AnimeNotifier, element: HTMLElement) { export function searchBySpeech(arn: AnimeNotifier, element: HTMLElement) {
if(recognition) { if(recognition) {
recognition.stop() recognition.stop()

View File

@ -9,9 +9,9 @@ import Analytics from "./Analytics"
import SideBar from "./SideBar" import SideBar from "./SideBar"
import InfiniteScroller from "./InfiniteScroller" import InfiniteScroller from "./InfiniteScroller"
import ServiceWorkerManager from "./ServiceWorkerManager" import ServiceWorkerManager from "./ServiceWorkerManager"
import { displayAiringDate, displayDate, displayTime } from "./DateView"
import { findAll, canUseWebP, requestIdleCallback, swapElements, delay } from "./Utils"
import { checkNewVersionDelayed } from "./NewVersionCheck" import { checkNewVersionDelayed } from "./NewVersionCheck"
import { displayAiringDate, displayDate, displayTime } from "./DateView"
import { findAll, canUseWebP, requestIdleCallback, swapElements, delay, findAllInside } from "./Utils"
import * as actions from "./Actions" import * as actions from "./Actions"
export default class AnimeNotifier { export default class AnimeNotifier {
@ -954,6 +954,15 @@ export default class AnimeNotifier {
}) })
} }
onNewContent(element: HTMLElement) {
// Do the same as for the content loaded event,
// except here we are limiting it to the element.
this.app.ajaxify(element.getElementsByTagName("a"))
this.lazyLoad(findAllInside("lazy", element))
this.mountMountables(findAllInside("mountable", element))
this.assignTooltipOffsets(findAllInside("tip", element))
}
scrollTo(target: HTMLElement) { scrollTo(target: HTMLElement) {
const duration = 250.0 const duration = 250.0
const fullSin = Math.PI / 2 const fullSin = Math.PI / 2

View File

@ -83,6 +83,11 @@ post-content-padding-y = 0.75rem
align-items flex-start align-items flex-start
margin-right 0.5rem margin-right 0.5rem
#new-post-actions
horizontal
justify-content flex-end
margin-bottom 0.75rem
// Toolbar // Toolbar
.post-toolbar .post-toolbar