Added support for per-topic view counters.
Added support for shutdown tasks. View counters are now saved on graceful shutdown. Dynamic routes are now tracked by the route view counter. The uploads route should now be tracked by the route view counter. Added a WYSIWYG Editor to the profiles for Cosora.
This commit is contained in:
parent
c7df616f5b
commit
964d219407
|
@ -47,7 +47,7 @@ func routeSitemapXml(w http.ResponseWriter, r *http.Request) common.RouteError {
|
||||||
writeXMLHeader(w, r)
|
writeXMLHeader(w, r)
|
||||||
w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"))
|
w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"))
|
||||||
sitemapItem("sitemaps/topics.xml")
|
sitemapItem("sitemaps/topics.xml")
|
||||||
sitemapItem("sitemaps/forums.xml")
|
//sitemapItem("sitemaps/forums.xml")
|
||||||
//sitemapItem("sitemaps/users.xml")
|
//sitemapItem("sitemaps/users.xml")
|
||||||
w.Write([]byte("</sitemapindex>"))
|
w.Write([]byte("</sitemapindex>"))
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
@ -28,6 +27,7 @@ func NewChunkedViewCounter() (*ChunkedViewCounter, error) {
|
||||||
}
|
}
|
||||||
AddScheduledFifteenMinuteTask(counter.Tick) // This is run once every fifteen minutes to match the frequency of the RouteViewCounter
|
AddScheduledFifteenMinuteTask(counter.Tick) // This is run once every fifteen minutes to match the frequency of the RouteViewCounter
|
||||||
//AddScheduledSecondTask(counter.Tick)
|
//AddScheduledSecondTask(counter.Tick)
|
||||||
|
AddShutdownTask(counter.Tick)
|
||||||
return counter, acc.FirstError()
|
return counter, acc.FirstError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +82,7 @@ func NewDefaultRouteViewCounter() (*DefaultRouteViewCounter, error) {
|
||||||
}
|
}
|
||||||
AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second
|
AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second
|
||||||
//AddScheduledSecondTask(counter.Tick)
|
//AddScheduledSecondTask(counter.Tick)
|
||||||
|
AddShutdownTask(counter.Tick)
|
||||||
return counter, acc.FirstError()
|
return counter, acc.FirstError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ func (counter *DefaultRouteViewCounter) insertChunk(count int, route int) error
|
||||||
|
|
||||||
func (counter *DefaultRouteViewCounter) Bump(route int) {
|
func (counter *DefaultRouteViewCounter) Bump(route int) {
|
||||||
// TODO: Test this check
|
// TODO: Test this check
|
||||||
log.Print("counter.routeBuckets[route]: ", counter.routeBuckets[route])
|
debugLog("counter.routeBuckets[", route, "]: ", counter.routeBuckets[route])
|
||||||
if len(counter.routeBuckets) <= route {
|
if len(counter.routeBuckets) <= route {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -157,41 +158,53 @@ func NewDefaultTopicViewCounter() (*DefaultTopicViewCounter, error) {
|
||||||
evenTopics: make(map[int]*RWMutexCounterBucket),
|
evenTopics: make(map[int]*RWMutexCounterBucket),
|
||||||
update: acc.Update("topics").Set("views = views + ?").Where("tid = ?").Prepare(),
|
update: acc.Update("topics").Set("views = views + ?").Where("tid = ?").Prepare(),
|
||||||
}
|
}
|
||||||
AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second
|
AddScheduledFifteenMinuteTask(counter.Tick) // Who knows how many topics we have queued up, we probably don't want this running too frequently
|
||||||
//AddScheduledSecondTask(counter.Tick)
|
//AddScheduledSecondTask(counter.Tick)
|
||||||
|
AddShutdownTask(counter.Tick)
|
||||||
return counter, acc.FirstError()
|
return counter, acc.FirstError()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (counter *DefaultTopicViewCounter) Tick() error {
|
func (counter *DefaultTopicViewCounter) Tick() error {
|
||||||
counter.oddLock.RLock()
|
counter.oddLock.RLock()
|
||||||
for topicID, topic := range counter.oddTopics {
|
oddTopics := counter.oddTopics
|
||||||
|
counter.oddLock.RUnlock()
|
||||||
|
for topicID, topic := range oddTopics {
|
||||||
var count int
|
var count int
|
||||||
topic.RLock()
|
topic.RLock()
|
||||||
count = topic.counter
|
count = topic.counter
|
||||||
topic.RUnlock()
|
topic.RUnlock()
|
||||||
|
// TODO: Only delete the bucket when it's zero to avoid hitting popular topics?
|
||||||
|
counter.oddLock.Lock()
|
||||||
|
delete(counter.oddTopics, topicID)
|
||||||
|
counter.oddLock.Unlock()
|
||||||
err := counter.insertChunk(count, topicID)
|
err := counter.insertChunk(count, topicID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
counter.oddLock.RUnlock()
|
|
||||||
|
|
||||||
counter.evenLock.RLock()
|
counter.evenLock.RLock()
|
||||||
for topicID, topic := range counter.evenTopics {
|
evenTopics := counter.evenTopics
|
||||||
|
counter.evenLock.RUnlock()
|
||||||
|
for topicID, topic := range evenTopics {
|
||||||
var count int
|
var count int
|
||||||
topic.RLock()
|
topic.RLock()
|
||||||
count = topic.counter
|
count = topic.counter
|
||||||
topic.RUnlock()
|
topic.RUnlock()
|
||||||
|
// TODO: Only delete the bucket when it's zero to avoid hitting popular topics?
|
||||||
|
counter.evenLock.Lock()
|
||||||
|
delete(counter.evenTopics, topicID)
|
||||||
|
counter.evenLock.Unlock()
|
||||||
err := counter.insertChunk(count, topicID)
|
err := counter.insertChunk(count, topicID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
counter.evenLock.RUnlock()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Optimise this further. E.g. Using IN() on every one view topic. Rinse and repeat for two views, three views, four views and five views.
|
||||||
func (counter *DefaultTopicViewCounter) insertChunk(count int, topicID int) error {
|
func (counter *DefaultTopicViewCounter) insertChunk(count int, topicID int) error {
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -204,9 +217,9 @@ func (counter *DefaultTopicViewCounter) insertChunk(count int, topicID int) erro
|
||||||
func (counter *DefaultTopicViewCounter) Bump(topicID int) {
|
func (counter *DefaultTopicViewCounter) Bump(topicID int) {
|
||||||
// Is the ID even?
|
// Is the ID even?
|
||||||
if topicID%2 == 0 {
|
if topicID%2 == 0 {
|
||||||
counter.evenLock.Lock()
|
counter.evenLock.RLock()
|
||||||
topic, ok := counter.evenTopics[topicID]
|
topic, ok := counter.evenTopics[topicID]
|
||||||
counter.evenLock.Unlock()
|
counter.evenLock.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
topic.Lock()
|
topic.Lock()
|
||||||
topic.counter++
|
topic.counter++
|
||||||
|
@ -219,9 +232,9 @@ func (counter *DefaultTopicViewCounter) Bump(topicID int) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
counter.oddLock.Lock()
|
counter.oddLock.RLock()
|
||||||
topic, ok := counter.oddTopics[topicID]
|
topic, ok := counter.oddTopics[topicID]
|
||||||
counter.oddLock.Unlock()
|
counter.oddLock.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
topic.Lock()
|
topic.Lock()
|
||||||
topic.counter++
|
topic.counter++
|
||||||
|
|
|
@ -21,6 +21,7 @@ type TaskStmts struct {
|
||||||
|
|
||||||
var ScheduledSecondTasks []func() error
|
var ScheduledSecondTasks []func() error
|
||||||
var ScheduledFifteenMinuteTasks []func() error
|
var ScheduledFifteenMinuteTasks []func() error
|
||||||
|
var ShutdownTasks []func() error
|
||||||
var taskStmts TaskStmts
|
var taskStmts TaskStmts
|
||||||
var lastSync time.Time
|
var lastSync time.Time
|
||||||
|
|
||||||
|
@ -45,6 +46,11 @@ func AddScheduledFifteenMinuteTask(task func() error) {
|
||||||
ScheduledFifteenMinuteTasks = append(ScheduledFifteenMinuteTasks, task)
|
ScheduledFifteenMinuteTasks = append(ScheduledFifteenMinuteTasks, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddShutdownTask is not concurrency safe
|
||||||
|
func AddShutdownTask(task func() error) {
|
||||||
|
ShutdownTasks = append(ShutdownTasks, task)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Use AddScheduledSecondTask
|
// TODO: Use AddScheduledSecondTask
|
||||||
func HandleExpiredScheduledGroups() error {
|
func HandleExpiredScheduledGroups() error {
|
||||||
rows, err := taskStmts.getExpiredScheduledGroups.Query()
|
rows, err := taskStmts.getExpiredScheduledGroups.Query()
|
||||||
|
|
|
@ -72,6 +72,8 @@ var RouteMap = map[string]interface{}{
|
||||||
"routeUnban": routeUnban,
|
"routeUnban": routeUnban,
|
||||||
"routeActivate": routeActivate,
|
"routeActivate": routeActivate,
|
||||||
"routeIps": routeIps,
|
"routeIps": routeIps,
|
||||||
|
"routeDynamic": routeDynamic,
|
||||||
|
"routeUploads": routeUploads,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ! NEVER RELY ON THESE REMAINING THE SAME BETWEEN COMMITS
|
// ! NEVER RELY ON THESE REMAINING THE SAME BETWEEN COMMITS
|
||||||
|
@ -133,6 +135,8 @@ var routeMapEnum = map[string]int{
|
||||||
"routeUnban": 54,
|
"routeUnban": 54,
|
||||||
"routeActivate": 55,
|
"routeActivate": 55,
|
||||||
"routeIps": 56,
|
"routeIps": 56,
|
||||||
|
"routeDynamic": 57,
|
||||||
|
"routeUploads": 58,
|
||||||
}
|
}
|
||||||
var reverseRouteMapEnum = map[int]string{
|
var reverseRouteMapEnum = map[int]string{
|
||||||
0: "routeAPI",
|
0: "routeAPI",
|
||||||
|
@ -192,6 +196,8 @@ var reverseRouteMapEnum = map[int]string{
|
||||||
54: "routeUnban",
|
54: "routeUnban",
|
||||||
55: "routeActivate",
|
55: "routeActivate",
|
||||||
56: "routeIps",
|
56: "routeIps",
|
||||||
|
57: "routeDynamic",
|
||||||
|
58: "routeUploads",
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Stop spilling these into the package scope?
|
// TODO: Stop spilling these into the package scope?
|
||||||
|
@ -794,6 +800,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
common.NotFound(w,req)
|
common.NotFound(w,req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
common.RouteViewCounter.Bump(58)
|
||||||
req.URL.Path += extraData
|
req.URL.Path += extraData
|
||||||
// TODO: Find a way to propagate errors up from this?
|
// TODO: Find a way to propagate errors up from this?
|
||||||
router.UploadHandler(w,req) // TODO: Count these views
|
router.UploadHandler(w,req) // TODO: Count these views
|
||||||
|
@ -837,8 +844,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
router.RUnlock()
|
router.RUnlock()
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
|
common.RouteViewCounter.Bump(57) // TODO: Be more specific about *which* dynamic route it is
|
||||||
req.URL.Path += extraData
|
req.URL.Path += extraData
|
||||||
err = handle(w,req,user) // TODO: Count these views
|
err = handle(w,req,user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
router.handleError(err,w,req,user)
|
router.handleError(err,w,req,user)
|
||||||
}
|
}
|
||||||
|
|
28
main.go
28
main.go
|
@ -234,6 +234,14 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var runTasks = func(tasks []func() error) {
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task() != nil {
|
||||||
|
common.LogError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run this goroutine once a second
|
// Run this goroutine once a second
|
||||||
secondTicker := time.NewTicker(1 * time.Second)
|
secondTicker := time.NewTicker(1 * time.Second)
|
||||||
fifteenMinuteTicker := time.NewTicker(15 * time.Minute)
|
fifteenMinuteTicker := time.NewTicker(15 * time.Minute)
|
||||||
|
@ -242,22 +250,16 @@ func main() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-secondTicker.C:
|
case <-secondTicker.C:
|
||||||
//log.Print("Running the second ticker")
|
|
||||||
// TODO: Add a plugin hook here
|
// TODO: Add a plugin hook here
|
||||||
|
runTasks(common.ScheduledSecondTasks)
|
||||||
|
|
||||||
for _, task := range common.ScheduledSecondTasks {
|
// TODO: Stop hard-coding this
|
||||||
if task() != nil {
|
|
||||||
common.LogError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := common.HandleExpiredScheduledGroups()
|
err := common.HandleExpiredScheduledGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(err)
|
common.LogError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle delayed moderation tasks
|
// TODO: Handle delayed moderation tasks
|
||||||
// TODO: Handle the daily clean-up. Move this to a 24 hour task?
|
|
||||||
|
|
||||||
// Sync with the database, if there are any changes
|
// Sync with the database, if there are any changes
|
||||||
err = common.HandleServerSync()
|
err = common.HandleServerSync()
|
||||||
|
@ -273,18 +275,15 @@ func main() {
|
||||||
// TODO: Add a plugin hook here
|
// TODO: Add a plugin hook here
|
||||||
case <-fifteenMinuteTicker.C:
|
case <-fifteenMinuteTicker.C:
|
||||||
// TODO: Add a plugin hook here
|
// TODO: Add a plugin hook here
|
||||||
|
runTasks(common.ScheduledFifteenMinuteTasks)
|
||||||
for _, task := range common.ScheduledFifteenMinuteTasks {
|
|
||||||
if task() != nil {
|
|
||||||
common.LogError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Automatically lock topics, if they're really old, and the associated setting is enabled.
|
// TODO: Automatically lock topics, if they're really old, and the associated setting is enabled.
|
||||||
// TODO: Publish scheduled posts.
|
// TODO: Publish scheduled posts.
|
||||||
|
|
||||||
// TODO: Add a plugin hook here
|
// TODO: Add a plugin hook here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Handle the daily clean-up.
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -329,6 +328,7 @@ func main() {
|
||||||
go func() {
|
go func() {
|
||||||
sig := <-sigs
|
sig := <-sigs
|
||||||
// TODO: Gracefully shutdown the HTTP server
|
// TODO: Gracefully shutdown the HTTP server
|
||||||
|
runTasks(common.ShutdownTasks)
|
||||||
log.Fatal("Received a signal to shutdown: ", sig)
|
log.Fatal("Received a signal to shutdown: ", sig)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -150,6 +150,9 @@ func main() {
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stubs for us to refer to these routes through
|
||||||
|
mapIt("routeDynamic")
|
||||||
|
mapIt("routeUploads")
|
||||||
tmplVars.AllRouteNames = allRouteNames
|
tmplVars.AllRouteNames = allRouteNames
|
||||||
tmplVars.AllRouteMap = allRouteMap
|
tmplVars.AllRouteMap = allRouteMap
|
||||||
|
|
||||||
|
@ -294,6 +297,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
common.NotFound(w,req)
|
common.NotFound(w,req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
common.RouteViewCounter.Bump({{.AllRouteMap.routeUploads}})
|
||||||
req.URL.Path += extraData
|
req.URL.Path += extraData
|
||||||
// TODO: Find a way to propagate errors up from this?
|
// TODO: Find a way to propagate errors up from this?
|
||||||
router.UploadHandler(w,req) // TODO: Count these views
|
router.UploadHandler(w,req) // TODO: Count these views
|
||||||
|
@ -337,8 +341,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
router.RUnlock()
|
router.RUnlock()
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
|
common.RouteViewCounter.Bump({{.AllRouteMap.routeDynamic}}) // TODO: Be more specific about *which* dynamic route it is
|
||||||
req.URL.Path += extraData
|
req.URL.Path += extraData
|
||||||
err = handle(w,req,user) // TODO: Count these views
|
err = handle(w,req,user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
router.handleError(err,w,req,user)
|
router.handleError(err,w,req,user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,12 @@ func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
|
http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Temporary stubs for view tracking
|
||||||
|
func routeDynamic() {
|
||||||
|
}
|
||||||
|
func routeUploads() {
|
||||||
|
}
|
||||||
|
|
||||||
// GET functions
|
// GET functions
|
||||||
func routeStatic(w http.ResponseWriter, r *http.Request) {
|
func routeStatic(w http.ResponseWriter, r *http.Request) {
|
||||||
file, ok := common.StaticFiles.Get(r.URL.Path)
|
file, ok := common.StaticFiles.Get(r.URL.Path)
|
||||||
|
@ -613,6 +619,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.InternalError(err, w, r)
|
return common.InternalError(err, w, r)
|
||||||
}
|
}
|
||||||
|
common.TopicViewCounter.Bump(topic.ID) // TODO Move this into the router?
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -601,8 +601,7 @@ var profile_21 = []byte(`
|
||||||
<div id="profile_comments_head" class="colstack_item colstack_head hash_hide">
|
<div id="profile_comments_head" class="colstack_item colstack_head hash_hide">
|
||||||
<div class="rowitem"><h1><a>Comments</a></h1></div>
|
<div class="rowitem"><h1><a>Comments</a></h1></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="profile_comments" class="colstack_item hash_hide">
|
<div id="profile_comments" class="colstack_item hash_hide">`)
|
||||||
`)
|
|
||||||
var profile_comments_row_0 = []byte(`
|
var profile_comments_row_0 = []byte(`
|
||||||
<div class="rowitem passive deletable_block editable_parent simple `)
|
<div class="rowitem passive deletable_block editable_parent simple `)
|
||||||
var profile_comments_row_1 = []byte(`" style="background-image: url(`)
|
var profile_comments_row_1 = []byte(`" style="background-image: url(`)
|
||||||
|
@ -672,8 +671,7 @@ var profile_comments_row_33 = []byte(`&type=user-reply"><button class="username
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
var profile_22 = []byte(`
|
var profile_22 = []byte(`</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
`)
|
`)
|
||||||
var profile_23 = []byte(`
|
var profile_23 = []byte(`
|
||||||
|
@ -682,9 +680,9 @@ var profile_23 = []byte(`
|
||||||
var profile_24 = []byte(`' type="hidden" />
|
var profile_24 = []byte(`' type="hidden" />
|
||||||
<div class="colstack_item topic_reply_form" style="border-top: none;">
|
<div class="colstack_item topic_reply_form" style="border-top: none;">
|
||||||
<div class="formrow">
|
<div class="formrow">
|
||||||
<div class="formitem"><textarea name="reply-content" placeholder="Insert reply here"></textarea></div>
|
<div class="formitem"><textarea class="input_content" name="reply-content" placeholder="Insert comment here"></textarea></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="formrow">
|
<div class="formrow quick_button_row">
|
||||||
<div class="formitem"><button name="reply-button" class="formbutton">Create Reply</button></div>
|
<div class="formitem"><button name="reply-button" class="formbutton">Create Reply</button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -69,18 +69,16 @@
|
||||||
<div id="profile_comments_head" class="colstack_item colstack_head hash_hide">
|
<div id="profile_comments_head" class="colstack_item colstack_head hash_hide">
|
||||||
<div class="rowitem"><h1><a>Comments</a></h1></div>
|
<div class="rowitem"><h1><a>Comments</a></h1></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="profile_comments" class="colstack_item hash_hide">
|
<div id="profile_comments" class="colstack_item hash_hide">{{template "profile_comments_row.html" . }}</div>
|
||||||
{{template "profile_comments_row.html" . }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if not .CurrentUser.IsBanned}}
|
{{if not .CurrentUser.IsBanned}}
|
||||||
<form id="profile_comments_form" class="hash_hide" action="/profile/reply/create/" method="post">
|
<form id="profile_comments_form" class="hash_hide" action="/profile/reply/create/" method="post">
|
||||||
<input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" />
|
<input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" />
|
||||||
<div class="colstack_item topic_reply_form" style="border-top: none;">
|
<div class="colstack_item topic_reply_form" style="border-top: none;">
|
||||||
<div class="formrow">
|
<div class="formrow">
|
||||||
<div class="formitem"><textarea name="reply-content" placeholder="Insert reply here"></textarea></div>
|
<div class="formitem"><textarea class="input_content" name="reply-content" placeholder="Insert comment here"></textarea></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="formrow">
|
<div class="formrow quick_button_row">
|
||||||
<div class="formitem"><button name="reply-button" class="formbutton">Create Reply</button></div>
|
<div class="formitem"><button name="reply-button" class="formbutton">Create Reply</button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -479,6 +479,9 @@ select, input, textarea, button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: min-content;
|
height: min-content;
|
||||||
}
|
}
|
||||||
|
.topic_reply_form .formrow {
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
.topic_reply_form .trumbowyg-button-pane:after {
|
.topic_reply_form .trumbowyg-button-pane:after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -935,6 +938,9 @@ select, input, textarea, button {
|
||||||
#profile_comments {
|
#profile_comments {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
#profile_comments:empty {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
#profile_comments .rowitem {
|
#profile_comments .rowitem {
|
||||||
background-image: none !important;
|
background-image: none !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,10 @@ $(document).ready(function(){
|
||||||
btns: btnlist,
|
btns: btnlist,
|
||||||
autogrow: true,
|
autogrow: true,
|
||||||
});
|
});
|
||||||
|
$('#profile_comments_form .topic_reply_form .input_content').trumbowyg({
|
||||||
|
btns: [['viewHTML'],['strong','em','del'],['link'],['insertImage'],['removeformat']],
|
||||||
|
autogrow: true,
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: Refactor this to use `each` less
|
// TODO: Refactor this to use `each` less
|
||||||
$('.button_menu').click(function(){
|
$('.button_menu').click(function(){
|
||||||
|
|
Loading…
Reference in New Issue