optimise topic pages

optimise profiles
simple nil ptr handling in template boolean expressions
This commit is contained in:
Azareal 2020-04-27 22:41:55 +10:00
parent 47d88f1744
commit 46a87defee
20 changed files with 756 additions and 250 deletions

View File

@ -16,6 +16,10 @@ func (c *NullUserCache) DeallocOverflow(evictPriority bool) (evicted int) {
func (c *NullUserCache) Get(id int) (*User, error) { func (c *NullUserCache) Get(id int) (*User, error) {
return nil, ErrNoRows return nil, ErrNoRows
} }
func (c *NullUserCache) Getn(id int) *User {
return nil
}
func (c *NullUserCache) BulkGet(ids []int) (list []*User) { func (c *NullUserCache) BulkGet(ids []int) (list []*User) {
return make([]*User, len(ids)) return make([]*User, len(ids))
} }

View File

@ -192,7 +192,7 @@ type TopicPage struct {
ItemList []*ReplyUser ItemList []*ReplyUser
Topic TopicUser Topic TopicUser
Forum *Forum Forum *Forum
Poll Poll Poll *Poll
Paginator Paginator
} }

View File

@ -29,6 +29,7 @@ type ReplyUser struct {
URL string URL string
//URLPrefix string //URLPrefix string
//URLName string //URLName string
Group int
Level int Level int
ActionIcon string ActionIcon string
@ -37,11 +38,11 @@ type ReplyUser struct {
} }
type Reply struct { type Reply struct {
ID int ID int
ParentID int ParentID int
Content string Content string
CreatedBy int CreatedBy int
Group int //Group int
CreatedAt time.Time CreatedAt time.Time
LastEdit int LastEdit int
LastEditBy int LastEditBy int
@ -49,7 +50,7 @@ type Reply struct {
IP string IP string
Liked bool Liked bool
LikeCount int LikeCount int
AttachCount int AttachCount uint16
ActionType string ActionType string
} }

View File

@ -105,7 +105,6 @@ func (s *SQLReplyStore) Create(t *Topic, content, ip string, uid int) (id int, e
if err != nil { if err != nil {
return 0, err return 0, err
} }
lastID, err := res.LastInsertId() lastID, err := res.LastInsertId()
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -227,7 +227,7 @@ func compileCommons(c *tmpl.CTemplateSet, head, head2 *Header, forumList []Forum
}*/ }*/
var topicsList []*TopicsRow var topicsList []*TopicsRow
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 1, 0, "classname", 0, "", user2, "", 0, user3, "General", "/forum/general.2", nil}) topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "::1", 1, 0, 1, 1, 0, "classname", 0, "", user2, "", 0, user3, "General", "/forum/general.2", nil})
topicListPage := TopicListPage{htitle("Topic List"), topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}} topicListPage := TopicListPage{htitle("Topic List"), topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}}
o.Add("topics", "c.TopicListPage", topicListPage) o.Add("topics", "c.TopicListPage", topicListPage)
o.Add("topics_mini", "c.TopicListPage", topicListPage) o.Add("topics_mini", "c.TopicListPage", topicListPage)
@ -246,11 +246,11 @@ func compileCommons(c *tmpl.CTemplateSet, head, head2 *Header, forumList []Forum
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", 58, false, miniAttach, nil, false} topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", 58, false, miniAttach, nil, false}
var replyList []*ReplyUser var replyList []*ReplyUser
reply := Reply{1, 1, "Yo!", 1, Config.DefaultGroup, now, 0, 0, 1, "::1", true, 1, 1, ""} reply := Reply{1, 1, "Yo!", 1 /*, Config.DefaultGroup*/, now, 0, 0, 1, "::1", true, 1, 1, ""}
ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: avatar, Level: 0, Attachments: miniAttach} ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: avatar, Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach}
ru.Init() ru.Init(user2)
replyList = append(replyList, ru) replyList = append(replyList, ru)
tpage := TopicPage{htitle("Topic Name"), replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, Paginator{[]int{1}, 1, 1}} tpage := TopicPage{htitle("Topic Name"), replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, &poll, Paginator{[]int{1}, 1, 1}}
tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID) tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID)
o.Add("topic", "c.TopicPage", tpage) o.Add("topic", "c.TopicPage", tpage)
o.Add("topic_mini", "c.TopicPage", tpage) o.Add("topic_mini", "c.TopicPage", tpage)
@ -276,20 +276,24 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
//topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach, nil} //topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach, nil}
// TODO: Do we want the UID on this to be 0? // TODO: Do we want the UID on this to be 0?
//avatar, microAvatar = BuildAvatar(0, "") //avatar, microAvatar = BuildAvatar(0, "")
reply := Reply{1, 1, "Yo!", 1, Config.DefaultGroup, now, 0, 0, 1, "::1", true, 1, 1, ""} reply := Reply{1, 1, "Yo!", 1 /*, Config.DefaultGroup*/, now, 0, 0, 1, "::1", true, 1, 1, ""}
ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: "", Level: 0, Attachments: miniAttach} ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: "", Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach}
ru.Init() ru.Init(user)
replyList = append(replyList, ru) replyList = append(replyList, ru)
// TODO: Use a dummy forum list to avoid o(n) problems // TODO: Use a dummy forum list to avoid o(n) problems
var forumList []Forum /*var forumList []Forum
forums, err := Forums.GetAll() forums, err := Forums.GetAll()
if err != nil { if err != nil {
return err return err
} }
for _, forum := range forums { for _, forum := range forums {
forumList = append(forumList, *forum) forumList = append(forumList, *forum)
} }*/
forum := BlankForum(1, "/forum/d.1", "d", "d desc", true, "", 0, "", 1)
forum.LastTopic = BlankTopic()
forum.LastReplyer = BlankUser()
forumList := []Forum{*forum}
// Convienience function to save a line here and there // Convienience function to save a line here and there
htitle := func(name string) *Header { htitle := func(name string) *Header {
@ -297,7 +301,7 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
return header return header
} }
t := TItemHold(make(map[string]TItem)) t := TItemHold(make(map[string]TItem))
err = compileCommons(c, header, header2, forumList, t) err := compileCommons(c, header, header2, forumList, t)
if err != nil { if err != nil {
return err return err
} }
@ -547,14 +551,14 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri
var replyList []*ReplyUser var replyList []*ReplyUser
// TODO: Do we really want the UID here to be zero? // TODO: Do we really want the UID here to be zero?
avatar, microAvatar = BuildAvatar(0, "") avatar, microAvatar = BuildAvatar(0, "")
reply := Reply{1, 1, "Yo!", 1, Config.DefaultGroup, now, 0, 0, 1, "::1", true, 1, 1, ""} reply := Reply{1, 1, "Yo!", 1 /*, Config.DefaultGroup*/, now, 0, 0, 1, "::1", true, 1, 1, ""}
ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: avatar, Level: 0, Attachments: miniAttach} ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: avatar, Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach}
ru.Init() ru.Init(user)
replyList = append(replyList, ru) replyList = append(replyList, ru)
varList = make(map[string]tmpl.VarItem) varList = make(map[string]tmpl.VarItem)
header.Title = "Topic Name" header.Title = "Topic Name"
tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, Paginator{[]int{1}, 1, 1}} tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, &poll, Paginator{[]int{1}, 1, 1}}
tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID) tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID)
t.AddStd("topic_posts", "c.TopicPage", tpage) t.AddStd("topic_posts", "c.TopicPage", tpage)
t.AddStd("topic_alt_posts", "c.TopicPage", tpage) t.AddStd("topic_alt_posts", "c.TopicPage", tpage)

View File

@ -25,13 +25,13 @@ type OutBufferFrame struct {
} }
type CContext struct { type CContext struct {
RootHolder string RootHolder string
VarHolder string VarHolder string
HoldReflect reflect.Value HoldReflect reflect.Value
RootTemplateName string RootTemplateName string
TemplateName string TemplateName string
LoopDepth int LoopDepth int
OutBuf *[]OutBufferFrame OutBuf *[]OutBufferFrame
} }
func (con *CContext) Push(nType string, body string) (index int) { func (con *CContext) Push(nType string, body string) (index int) {
@ -54,6 +54,20 @@ func (con *CContext) PushPhrasef(langIndex int, args string) (index int) {
return con.LastBufIndex() return con.LastBufIndex()
} }
func (con *CContext) StartIf(body string) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "startif", con.TemplateName, false, nil})
return con.LastBufIndex()
}
func (con *CContext) StartIfPtr(body string) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "startif", con.TemplateName, true, nil})
return con.LastBufIndex()
}
func (con *CContext) EndIf(startIndex int, body string) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "endif", con.TemplateName, startIndex, nil})
return con.LastBufIndex()
}
func (con *CContext) StartLoop(body string) (index int) { func (con *CContext) StartLoop(body string) (index int) {
con.LoopDepth++ con.LoopDepth++
return con.Push("startloop", body) return con.Push("startloop", body)
@ -73,15 +87,15 @@ func (con *CContext) EndTemplate(body string) (index int) {
func (con *CContext) AttachVars(vars string, index int) { func (con *CContext) AttachVars(vars string, index int) {
outBuf := *con.OutBuf outBuf := *con.OutBuf
node := outBuf[index] n := outBuf[index]
if node.Type != "starttemplate" && node.Type != "startloop" { if n.Type != "starttemplate" && n.Type != "startloop" && n.Type != "startif" {
panic("not a starttemplate node") panic("not a starttemplate, startloop or startif node")
} }
node.Body += vars n.Body += vars
outBuf[index] = node outBuf[index] = n
} }
func (con *CContext) addFrame(body string, ftype string, extra1 interface{}, extra2 interface{}) (index int) { func (con *CContext) addFrame(body, ftype string, extra1 interface{}, extra2 interface{}) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, ftype, con.TemplateName, extra1, extra2}) *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, ftype, con.TemplateName, extra1, extra2})
return con.LastBufIndex() return con.LastBufIndex()
} }

View File

@ -513,8 +513,7 @@ if !ok {
} }
`*/ `*/
fout += `var iw http.ResponseWriter fout += `var iw http.ResponseWriter
gzw, ok := w.(c.GzipResponseWriter) if gzw, ok := w.(c.GzipResponseWriter); ok {
if ok {
iw = gzw.ResponseWriter iw = gzw.ResponseWriter
} }
_ = iw _ = iw
@ -711,14 +710,26 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
return return
} }
con.Push("startif", "if "+expr+" {\n") var startIf int
var nilIf = strings.HasPrefix(expr, con.RootHolder) && strings.HasSuffix(expr, "!=nil")
if nilIf {
startIf = con.StartIfPtr("if " + expr + " {\n")
} else {
startIf = con.StartIf("if " + expr + " {\n")
}
c.compileSwitch(con, node.List) c.compileSwitch(con, node.List)
if node.ElseList == nil { if node.ElseList == nil {
c.detail("Selected Branch 1") c.detail("Selected Branch 1")
con.Push("endif", "}\n") con.EndIf(startIf, "}\n")
if nilIf {
c.afterTemplate(con, startIf)
}
} else { } else {
c.detail("Selected Branch 2") c.detail("Selected Branch 2")
con.Push("endif", "}") con.EndIf(startIf, "}")
if nilIf {
c.afterTemplate(con, startIf)
}
con.Push("startelse", " else {\n") con.Push("startelse", " else {\n")
c.compileSwitch(con, node.ElseList) c.compileSwitch(con, node.ElseList)
con.Push("endelse", "}\n") con.Push("endelse", "}\n")
@ -769,7 +780,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
c.detail("Range Kind Switch!") c.detail("Range Kind Switch!")
startIf := func(item reflect.Value, useCopy bool) { startIf := func(item reflect.Value, useCopy bool) {
con.Push("startif", "if len("+expr+") != 0 {\n") sIndex := con.StartIf("if len(" + expr + ")!=0 {\n")
startIndex := con.StartLoop("for _, item := range " + expr + " {\n") startIndex := con.StartLoop("for _, item := range " + expr + " {\n")
ccon := con ccon := con
var depth string var depth string
@ -793,7 +804,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
con.EndLoop("}\n") con.EndLoop("}\n")
c.afterTemplate(con, startIndex) c.afterTemplate(con, startIndex)
if node.ElseList != nil { if node.ElseList != nil {
con.Push("endif", "}") con.EndIf(sIndex, "}")
con.Push("startelse", " else {\n") con.Push("startelse", " else {\n")
if !useCopy { if !useCopy {
ccon = con ccon = con
@ -801,7 +812,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
c.compileSwitch(ccon, node.ElseList) c.compileSwitch(ccon, node.ElseList)
con.Push("endelse", "}\n") con.Push("endelse", "}\n")
} else { } else {
con.Push("endif", "}\n") con.EndIf(sIndex, "}\n")
} }
} }
@ -1512,12 +1523,15 @@ func (c *CTemplateSet) compileBoolSub(con CContext, varname string) string {
// TODO: What if it's a pointer or an interface? I *think* we've got pointers handled somewhere, but not interfaces which we don't know the types of at compile time // TODO: What if it's a pointer or an interface? I *think* we've got pointers handled somewhere, but not interfaces which we don't know the types of at compile time
switch val.Kind() { switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
out += " > 0" out += ">0"
case reflect.Bool: // Do nothing case reflect.Bool: // Do nothing
case reflect.String: case reflect.String:
out += " != \"\"" out += "!=\"\""
case reflect.Slice, reflect.Map: case reflect.Slice, reflect.Map:
out = "len(" + out + ") != 0" out = "len(" + out + ")!=0"
// TODO: Follow the pointer and evaluate it?
case reflect.Ptr:
out += "!=nil"
default: default:
c.logger.Println("Variable Name:", varname) c.logger.Println("Variable Name:", varname)
c.logger.Println("Variable Holder:", con.VarHolder) c.logger.Println("Variable Holder:", con.VarHolder)
@ -1675,9 +1689,9 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
return return
} }
} }
con.Push("startif", "if "+varname+" {\n") startIf := con.StartIf("if " + varname + " {\n")
c.addText(con, []byte("true")) c.addText(con, []byte("true"))
con.Push("endif", "} ") con.EndIf(startIf, "} ")
con.Push("startelse", "else {\n") con.Push("startelse", "else {\n")
c.addText(con, []byte("false")) c.addText(con, []byte("false"))
con.Push("endelse", "}\n") con.Push("endelse", "}\n")
@ -1945,42 +1959,66 @@ func (c *CTemplateSet) loadTemplate(fileDir, name string) (content string, err e
return content, nil return content, nil
} }
func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) { func (c *CTemplateSet) afterTemplate(con CContext, startIndex int /*, svmap map[string]int*/) {
c.dumpCall("afterTemplate", con, startIndex) c.dumpCall("afterTemplate", con, startIndex)
defer c.retCall("afterTemplate") defer c.retCall("afterTemplate")
loopDepth := 0 loopDepth := 0
ifNilDepth := 0
var outBuf = *con.OutBuf var outBuf = *con.OutBuf
varcounts := make(map[string]int) varcounts := make(map[string]int)
loopStart := startIndex loopStart := startIndex
if outBuf[startIndex].Type == "startloop" && (len(outBuf) > startIndex+1) { otype := outBuf[startIndex].Type
if otype == "startloop" && (len(outBuf) > startIndex+1) {
loopStart++
}
if otype == "startif" && (len(outBuf) > startIndex+1) {
loopStart++ loopStart++
} }
// Exclude varsubs within loops for now // Exclude varsubs within loops for now
OLoop:
for i := loopStart; i < len(outBuf); i++ { for i := loopStart; i < len(outBuf); i++ {
item := outBuf[i] item := outBuf[i]
c.detail("item:", item) c.detail("item:", item)
if item.Type == "startloop" { switch item.Type {
case "startloop":
loopDepth++ loopDepth++
c.detail("loopDepth:", loopDepth) c.detail("loopDepth:", loopDepth)
} else if item.Type == "endloop" { case "endloop":
loopDepth-- loopDepth--
c.detail("loopDepth:", loopDepth) c.detail("loopDepth:", loopDepth)
if loopDepth == -1 { if loopDepth == -1 {
break break OLoop
}
case "startif":
if item.Extra.(bool) == true {
ifNilDepth++
}
case "endif":
item2 := outBuf[item.Extra.(int)]
if item2.Extra.(bool) == true {
ifNilDepth--
}
if ifNilDepth == -1 {
break OLoop
}
case "varsub":
if loopDepth == 0 && ifNilDepth == 0 {
count := varcounts[item.Body]
varcounts[item.Body] = count + 1
c.detail("count " + strconv.Itoa(count) + " for " + item.Body)
c.detail("loopDepth:", loopDepth)
} }
} else if item.Type == "varsub" && loopDepth == 0 {
count := varcounts[item.Body]
varcounts[item.Body] = count + 1
c.detail("count " + strconv.Itoa(count) + " for " + item.Body)
c.detail("loopDepth:", loopDepth)
} }
} }
var varstr string var varstr string
var i int var i int
varmap := make(map[string]int) varmap := make(map[string]int)
/*for svkey, sventry := range svmap {
varmap[svkey] = sventry
}*/
for name, count := range varcounts { for name, count := range varcounts {
if count > 1 { if count > 1 {
varstr += "var c_v_" + strconv.Itoa(i) + "=" + name + "\n" varstr += "var c_v_" + strconv.Itoa(i) + "=" + name + "\n"
@ -1991,21 +2029,153 @@ func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) {
// Exclude varsubs within loops for now // Exclude varsubs within loops for now
loopDepth = 0 loopDepth = 0
ifNilDepth = 0
OOLoop:
for i := loopStart; i < len(outBuf); i++ { for i := loopStart; i < len(outBuf); i++ {
item := outBuf[i] item := outBuf[i]
if item.Type == "startloop" { switch item.Type {
case "startloop":
loopDepth++ loopDepth++
} else if item.Type == "endloop" { case "endloop":
loopDepth-- loopDepth--
if loopDepth == -1 { if loopDepth == -1 {
break break OOLoop
} //con.Push("startif", "if "+varname+" {\n")
case "startif":
if item.Extra.(bool) == true {
ifNilDepth++
} }
} else if item.Type == "varsub" && loopDepth == 0 { case "endif":
index, ok := varmap[item.Body] item2 := outBuf[item.Extra.(int)]
if ok { if item2.Extra.(bool) == true {
item.Body = "c_v_" + strconv.Itoa(index) ifNilDepth--
item.Type = "cvarsub" }
outBuf[i] = item if ifNilDepth == -1 {
break OOLoop
}
case "varsub":
if loopDepth == 0 && ifNilDepth == 0 {
index, ok := varmap[item.Body]
if ok {
item.Body = "c_v_" + strconv.Itoa(index)
item.Type = "cvarsub"
outBuf[i] = item
}
}
}
}
con.AttachVars(varstr, startIndex)
}
const (
ATTmpl = iota
ATLoop
ATIfPtr
)
func (c *CTemplateSet) afterTemplateV2(con CContext, startIndex int /*, typ int*/, svmap map[string]int) {
c.dumpCall("afterTemplateV2", con, startIndex)
defer c.retCall("afterTemplateV2")
loopDepth := 0
ifNilDepth := 0
var outBuf = *con.OutBuf
varcounts := make(map[string]int)
loopStart := startIndex
otype := outBuf[startIndex].Type
if otype == "startloop" && (len(outBuf) > startIndex+1) {
loopStart++
}
if otype == "startif" && (len(outBuf) > startIndex+1) {
loopStart++
}
// Exclude varsubs within loops for now
OLoop:
for i := loopStart; i < len(outBuf); i++ {
item := outBuf[i]
c.detail("item:", item)
switch item.Type {
case "startloop":
loopDepth++
c.detail("loopDepth:", loopDepth)
case "endloop":
loopDepth--
c.detail("loopDepth:", loopDepth)
if loopDepth == -1 {
break OLoop
}
case "startif":
if item.Extra.(bool) == true {
ifNilDepth++
}
case "endif":
item2 := outBuf[item.Extra.(int)]
if item2.Extra.(bool) == true {
ifNilDepth--
}
if ifNilDepth == -1 {
break OLoop
}
case "varsub":
if loopDepth == 0 && ifNilDepth == 0 {
count := varcounts[item.Body]
varcounts[item.Body] = count + 1
c.detail("count " + strconv.Itoa(count) + " for " + item.Body)
c.detail("loopDepth:", loopDepth)
}
}
}
var varstr string
var i int
varmap := make(map[string]int)
/*for svkey, sventry := range svmap {
varmap[svkey] = sventry
}*/
for name, count := range varcounts {
if count > 1 {
varstr += "var c_v_" + strconv.Itoa(i) + "=" + name + "\n"
varmap[name] = i
i++
}
}
// Exclude varsubs within loops for now
loopDepth = 0
ifNilDepth = 0
OOLoop:
for i := loopStart; i < len(outBuf); i++ {
item := outBuf[i]
switch item.Type {
case "startloop":
loopDepth++
case "endloop":
loopDepth--
if loopDepth == -1 {
break OOLoop
} //con.Push("startif", "if "+varname+" {\n")
case "startif":
if item.Extra.(bool) == true {
ifNilDepth++
}
case "endif":
item2 := outBuf[item.Extra.(int)]
if item2.Extra.(bool) == true {
ifNilDepth--
}
if ifNilDepth == -1 {
break OOLoop
}
case "varsub":
if loopDepth == 0 && ifNilDepth == 0 {
index, ok := varmap[item.Body]
if ok {
item.Body = "c_v_" + strconv.Itoa(index)
item.Type = "cvarsub"
outBuf[i] = item
}
} }
} }
} }

View File

@ -188,6 +188,8 @@ func (t *TopicsRow) Topic() *Topic {
type TopicStmts struct { type TopicStmts struct {
getRids *sql.Stmt getRids *sql.Stmt
getReplies *sql.Stmt getReplies *sql.Stmt
getReplies2 *sql.Stmt
getReplies3 *sql.Stmt
addReplies *sql.Stmt addReplies *sql.Stmt
updateLastReply *sql.Stmt updateLastReply *sql.Stmt
lock *sql.Stmt lock *sql.Stmt
@ -217,7 +219,9 @@ func init() {
t := "topics" t := "topics"
topicStmts = TopicStmts{ topicStmts = TopicStmts{
getRids: acc.Select("replies").Columns("rid").Where("tid=?").Orderby("rid ASC").Limit("?,?").Prepare(), getRids: acc.Select("replies").Columns("rid").Where("tid=?").Orderby("rid ASC").Limit("?,?").Prepare(),
getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ip, r.likeCount, r.attachCount, r.actionType", "r.createdBy = u.uid", "r.tid = ?", "r.rid ASC", "?,?"), getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ip, r.likeCount, r.attachCount, r.actionType", "r.createdBy=u.uid", "r.tid=?", "r.rid ASC", "?,?"),
getReplies2: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.likeCount, r.attachCount, r.actionType", "r.createdBy=u.uid", "r.tid=?", "r.rid ASC", "?,?"),
getReplies3: acc.Select("replies").Columns("rid, content, createdBy, createdAt, lastEdit, lastEditBy, likeCount, attachCount, actionType").Where("tid=?").Orderby("rid ASC").Limit("?,?").Prepare(),
addReplies: acc.Update(t).Set("postCount=postCount+?, lastReplyBy=?, lastReplyAt=UTC_TIMESTAMP()").Where("tid=?").Prepare(), addReplies: acc.Update(t).Set("postCount=postCount+?, lastReplyBy=?, lastReplyAt=UTC_TIMESTAMP()").Where("tid=?").Prepare(),
updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid=?").Prepare(), updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid=?").Prepare(),
lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(), lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(),
@ -570,8 +574,7 @@ var unlockai = "&#x1F513"
var stickai = "&#x1F4CC" var stickai = "&#x1F4CC"
var unstickai = "&#x1F4CC" + aipost var unstickai = "&#x1F4CC" + aipost
func (ru *ReplyUser) Init() (group *Group, err error) { func (ru *ReplyUser) Init(u *User) (group *Group, err error) {
ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy)
ru.ContentLines = strings.Count(ru.Content, "\n") ru.ContentLines = strings.Count(ru.Content, "\n")
postGroup, err := Groups.Get(ru.Group) postGroup, err := Groups.Get(ru.Group)
@ -583,8 +586,117 @@ func (ru *ReplyUser) Init() (group *Group, err error) {
} }
ru.Tag = postGroup.Tag ru.Tag = postGroup.Tag
// TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the c.UserStore initialise this? if u.ID != ru.CreatedBy {
ru.Avatar, ru.MicroAvatar = BuildAvatar(ru.CreatedBy, ru.Avatar) ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy)
// TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the c.UserStore initialise this?
ru.Avatar, ru.MicroAvatar = BuildAvatar(ru.CreatedBy, ru.Avatar)
} else {
ru.UserLink = u.Link
ru.Avatar, ru.MicroAvatar = u.Avatar, u.MicroAvatar
}
// We really shouldn't have inline HTML, we should do something about this...
if ru.ActionType != "" {
aarr := strings.Split(ru.ActionType, "-")
action := aarr[0]
switch action {
case "lock":
ru.ActionIcon = lockai
case "unlock":
ru.ActionIcon = unlockai
case "stick":
ru.ActionIcon = stickai
case "unstick":
ru.ActionIcon = unstickai
case "move":
if len(aarr) == 2 {
fid, _ := strconv.Atoi(aarr[1])
forum, err := Forums.Get(fid)
if err == nil {
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_move_dest", forum.Link, forum.Name, ru.UserLink, ru.CreatedByName)
return postGroup, nil
}
}
default:
// TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry?
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_default", ru.ActionType)
return postGroup, nil
}
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_"+action, ru.UserLink, ru.CreatedByName)
}
return postGroup, nil
}
func (ru *ReplyUser) Init2() (group *Group, err error) {
//ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy)
ru.ContentLines = strings.Count(ru.Content, "\n")
postGroup, err := Groups.Get(ru.Group)
if err != nil {
return nil, err
}
if postGroup.IsMod {
ru.ClassName = Config.StaffCSS
}
ru.Tag = postGroup.Tag
// We really shouldn't have inline HTML, we should do something about this...
if ru.ActionType != "" {
aarr := strings.Split(ru.ActionType, "-")
action := aarr[0]
switch action {
case "lock":
ru.ActionIcon = lockai
case "unlock":
ru.ActionIcon = unlockai
case "stick":
ru.ActionIcon = stickai
case "unstick":
ru.ActionIcon = unstickai
case "move":
if len(aarr) == 2 {
fid, _ := strconv.Atoi(aarr[1])
forum, err := Forums.Get(fid)
if err == nil {
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_move_dest", forum.Link, forum.Name, ru.UserLink, ru.CreatedByName)
return postGroup, nil
}
}
default:
// TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry?
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_default", ru.ActionType)
return postGroup, nil
}
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_"+action, ru.UserLink, ru.CreatedByName)
}
return postGroup, nil
}
func (ru *ReplyUser) Init3(u *User, tu *TopicUser) (group *Group, err error) {
ru.ContentLines = strings.Count(ru.Content, "\n")
postGroup, err := Groups.Get(ru.Group)
if err != nil {
return nil, err
}
if postGroup.IsMod {
ru.ClassName = Config.StaffCSS
}
ru.Tag = postGroup.Tag
if u.ID == ru.CreatedBy {
ru.UserLink = u.Link
ru.Avatar, ru.MicroAvatar = u.Avatar, u.MicroAvatar
} else if tu.CreatedBy == ru.CreatedBy {
ru.UserLink = tu.UserLink
ru.Avatar, ru.MicroAvatar = tu.Avatar, tu.MicroAvatar
} else {
ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy)
// TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the c.UserStore initialise this?
ru.Avatar, ru.MicroAvatar = BuildAvatar(ru.CreatedBy, ru.Avatar)
}
// We really shouldn't have inline HTML, we should do something about this... // We really shouldn't have inline HTML, we should do something about this...
if ru.ActionType != "" { if ru.ActionType != "" {
@ -620,18 +732,9 @@ func (ru *ReplyUser) Init() (group *Group, err error) {
} }
// TODO: Factor TopicUser into a *Topic and *User, as this starting to become overly complicated x.x // TODO: Factor TopicUser into a *Topic and *User, as this starting to become overly complicated x.x
func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, ogdesc string, err error) { func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser /*, ogdesc string*/, err error) {
var likedMap map[int]int var likedMap, attachMap map[int]int
if user.Liked > 0 { var likedQueryList, attachQueryList []int
likedMap = make(map[int]int)
}
likedQueryList := []int{user.ID}
var attachMap map[int]int
if user.Perms.EditReply {
attachMap = make(map[int]int)
}
attachQueryList := []int{}
var rid int var rid int
if len(t.Rids) > 0 { if len(t.Rids) > 0 {
@ -641,15 +744,19 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser,
re, err := Rstore.GetCache().Get(rid) re, err := Rstore.GetCache().Get(rid)
ucache := Users.GetCache() ucache := Users.GetCache()
var ruser *User var ruser *User
if err == nil && ucache != nil { if ucache != nil {
//log.Print("ucache step") //log.Print("ucache step")
ruser, err = ucache.Get(re.CreatedBy) if err == nil {
ruser = ucache.Getn(re.CreatedBy)
} else if t.PostCount == 2 {
ruser = ucache.Getn(t.LastReplyBy)
}
} }
hTbl := GetHookTable() hTbl := GetHookTable()
rf := func(r *ReplyUser) error { rf := func(r *ReplyUser) (err error) {
//log.Printf("before r: %+v\n", r) //log.Printf("before r: %+v\n", r)
group, err := r.Init() group, err := r.Init3(user, t)
if err != nil { if err != nil {
return err return err
} }
@ -668,6 +775,7 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser,
if r.ContentHtml == r.Content { if r.ContentHtml == r.Content {
r.ContentHtml = r.Content r.ContentHtml = r.Content
} }
r.Deletable = user.Perms.DeleteReply || r.CreatedBy == user.ID
// TODO: This doesn't work properly so pick the first one instead? // TODO: This doesn't work properly so pick the first one instead?
/*if r.ID == pFrag { /*if r.ID == pFrag {
@ -677,61 +785,211 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser,
} }
}*/ }*/
if r.LikeCount > 0 && user.Liked > 0 { return nil
likedMap[r.ID] = len(rlist) }
likedQueryList = append(likedQueryList, r.ID)
rf3 := func(r *ReplyUser) error {
//log.Printf("before r: %+v\n", r)
group, err := r.Init2()
if err != nil {
return err
} }
if user.Perms.EditReply && r.AttachCount > 0 {
attachMap[r.ID] = len(rlist) var parseSettings *ParseSettings
attachQueryList = append(attachQueryList, r.ID) if !group.Perms.AutoEmbed && (user.ParseSettings == nil || !user.ParseSettings.NoEmbed) {
parseSettings = DefaultParseSettings.CopyPtr()
parseSettings.NoEmbed = true
} else {
parseSettings = user.ParseSettings
}
r.ContentHtml = ParseMessage(r.Content, t.ParentID, "forums", parseSettings, user)
// TODO: Do this more efficiently by avoiding the allocations entirely in ParseMessage, if there's nothing to do.
if r.ContentHtml == r.Content {
r.ContentHtml = r.Content
} }
r.Deletable = user.Perms.DeleteReply || r.CreatedBy == user.ID r.Deletable = user.Perms.DeleteReply || r.CreatedBy == user.ID
hTbl.VhookNoRet("topic_reply_row_assign", &rlist, &r)
// TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis?
rlist = append(rlist, r)
//log.Printf("r: %d-%d", r.ID, len(rlist)-1)
return nil return nil
} }
// TODO: Factor the user fields out and embed a user struct instead // TODO: Factor the user fields out and embed a user struct instead
if err == nil { if err == nil && ruser != nil {
//log.Print("reply cached serve") //log.Print("reply cached serve")
reply := &ReplyUser{ClassName: "", Reply: *re, CreatedByName: ruser.Name, Avatar: ruser.Avatar /*URLPrefix: ruser.URLPrefix, URLName: ruser.URLName, */, Level: ruser.Level, Tag: ruser.Tag} r := &ReplyUser{ /*ClassName: "", */ Reply: *re, CreatedByName: ruser.Name, UserLink: ruser.Link, Avatar: ruser.Avatar, MicroAvatar: ruser.MicroAvatar /*URLPrefix: ruser.URLPrefix, URLName: ruser.URLName, */, Group: ruser.Group, Level: ruser.Level, Tag: ruser.Tag}
reply.Group = ruser.Group if err = rf3(r); err != nil {
err = rf(reply) return nil, err
if err != nil {
return nil, "", err
} }
if r.LikeCount > 0 && user.Liked > 0 {
likedMap = map[int]int{r.ID: len(rlist)}
likedQueryList = []int{r.ID}
}
if user.Perms.EditReply && r.AttachCount > 0 {
attachMap = map[int]int{r.ID: len(rlist)}
attachQueryList = []int{r.ID}
}
hTbl.VhookNoRet("topic_reply_row_assign", &r)
// TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis?
rlist = []*ReplyUser{r}
//log.Printf("r: %d-%d", r.ID, len(rlist)-1)
} else { } else {
//log.Print("reply query serve") //log.Print("reply query serve")
rows, err := topicStmts.getReplies.Query(t.ID, offset, Config.ItemsPerPage) rf2 := func(r *ReplyUser) {
if err != nil { if r.LikeCount > 0 && user.Liked > 0 {
return nil, "", err if likedMap == nil {
} likedMap = map[int]int{r.ID: len(rlist)}
defer rows.Close() likedQueryList = []int{r.ID}
} else {
likedMap[r.ID] = len(rlist)
likedQueryList = append(likedQueryList, r.ID)
}
}
if user.Perms.EditReply && r.AttachCount > 0 {
if attachMap == nil {
attachMap = map[int]int{r.ID: len(rlist)}
attachQueryList = []int{r.ID}
} else {
attachMap[r.ID] = len(rlist)
attachQueryList = append(attachQueryList, r.ID)
}
}
for rows.Next() { hTbl.VhookNoRet("topic_reply_row_assign", &r)
r := &ReplyUser{} // TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis?
err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.Avatar, &r.CreatedByName, &r.Group /*&r.URLPrefix, &r.URLName,*/, &r.Level, &r.IP, &r.LikeCount, &r.AttachCount, &r.ActionType) rlist = append(rlist, r)
if err != nil { //log.Printf("r: %d-%d", r.ID, len(rlist)-1)
return nil, "", err
}
err = rf(r)
if err != nil {
return nil, "", err
}
} }
if err = rows.Err(); err != nil { if !user.Perms.ViewIPs && ruser != nil {
return nil, "", err rows, err := topicStmts.getReplies3.Query(t.ID, offset, Config.ItemsPerPage)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
r := &ReplyUser{Avatar: ruser.Avatar, MicroAvatar: ruser.MicroAvatar, UserLink: ruser.Link, CreatedByName: ruser.Name, Group: ruser.Group, Level: ruser.Level}
err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.LikeCount, &r.AttachCount, &r.ActionType)
if err != nil {
return nil, err
}
if err = rf3(r); err != nil {
return nil, err
}
rf2(r)
}
if err = rows.Err(); err != nil {
return nil, err
}
} else if user.Perms.ViewIPs {
rows, err := topicStmts.getReplies.Query(t.ID, offset, Config.ItemsPerPage)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
r := &ReplyUser{}
err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.Avatar, &r.CreatedByName, &r.Group /*&r.URLPrefix, &r.URLName,*/, &r.Level, &r.IP, &r.LikeCount, &r.AttachCount, &r.ActionType)
if err != nil {
return nil, err
}
if err = rf(r); err != nil {
return nil, err
}
rf2(r)
}
if err = rows.Err(); err != nil {
return nil, err
}
} else if t.PostCount >= 20 {
rows, err := topicStmts.getReplies3.Query(t.ID, offset, Config.ItemsPerPage)
if err != nil {
return nil, err
}
defer rows.Close()
reqUserList := make(map[int]bool)
for rows.Next() {
r := &ReplyUser{}
err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy /*&r.URLPrefix, &r.URLName,*/, &r.LikeCount, &r.AttachCount, &r.ActionType)
if err != nil {
return nil, err
}
if r.CreatedBy != t.CreatedBy && r.CreatedBy != user.ID {
reqUserList[r.CreatedBy] = true
}
}
if err = rows.Err(); err != nil {
return nil, err
}
var userList map[int]*User
if len(reqUserList) > 0 {
// Convert the user ID map to a slice, then bulk load the users
idSlice := make([]int, len(reqUserList))
var i int
for userID := range reqUserList {
idSlice[i] = userID
i++
}
userList, err = Users.BulkGetMap(idSlice)
if err != nil {
return nil, nil // TODO: Implement this!
}
}
for _, r := range rlist {
var u *User
if r.CreatedBy == t.CreatedBy {
r.CreatedByName = t.CreatedByName
r.Avatar = t.Avatar
r.MicroAvatar = t.MicroAvatar
r.Group = t.Group
r.Level = t.Level
} else {
if r.CreatedBy == user.ID {
u = user
} else {
u = userList[r.CreatedBy]
}
r.CreatedByName = u.Name
r.Avatar = u.Avatar
r.MicroAvatar = u.MicroAvatar
r.Group = u.Group
r.Level = u.Level
}
if err = rf(r); err != nil {
return nil, err
}
rf2(r)
}
} else {
rows, err := topicStmts.getReplies2.Query(t.ID, offset, Config.ItemsPerPage)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
r := &ReplyUser{}
err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.Avatar, &r.CreatedByName, &r.Group /*&r.URLPrefix, &r.URLName,*/, &r.Level, &r.LikeCount, &r.AttachCount, &r.ActionType)
if err != nil {
return nil, err
}
if err = rf(r); err != nil {
return nil, err
}
rf2(r)
}
if err = rows.Err(); err != nil {
return nil, err
}
} }
} }
// TODO: Add a config setting to disable the liked query for a burst of extra speed // TODO: Add a config setting to disable the liked query for a burst of extra speed
if user.Liked > 0 && len(likedQueryList) > 1 /*&& user.LastLiked <= time.Now()*/ { if user.Liked > 0 && len(likedQueryList) > 0 /*&& user.LastLiked <= time.Now()*/ {
eids, err := Likes.BulkExists(likedQueryList[1:], user.ID, "replies") eids, err := Likes.BulkExists(likedQueryList, user.ID, "replies")
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
for _, eid := range eids { for _, eid := range eids {
rlist[likedMap[eid]].Liked = true rlist[likedMap[eid]].Liked = true
@ -742,7 +1000,7 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser,
//log.Printf("attachQueryList: %+v\n", attachQueryList) //log.Printf("attachQueryList: %+v\n", attachQueryList)
amap, err := Attachments.BulkMiniGetList("replies", attachQueryList) amap, err := Attachments.BulkMiniGetList("replies", attachQueryList)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return nil, "", err return nil, err
} }
//log.Printf("amap: %+v\n", amap) //log.Printf("amap: %+v\n", amap)
//log.Printf("attachMap: %+v\n", attachMap) //log.Printf("attachMap: %+v\n", attachMap)
@ -755,7 +1013,9 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser,
} }
} }
return rlist, ogdesc, nil //hTbl.VhookNoRet("topic_reply_end", &rlist)
return rlist, nil
} }
// TODO: Test this // TODO: Test this
@ -875,6 +1135,20 @@ func BuildTopicURL(slug string, tid int) string {
return "/topic/" + slug + "." + strconv.Itoa(tid) return "/topic/" + slug + "." + strconv.Itoa(tid)
} }
func BuildTopicURLSb(sb *strings.Builder, slug string, tid int) {
if slug == "" || !Config.BuildSlugs {
sb.Grow(7 + 2)
sb.WriteString("/topic/")
sb.WriteString(strconv.Itoa(tid))
return
}
sb.Grow(7 + 3 + len(slug))
sb.WriteString("/topic/")
sb.WriteString(slug)
sb.WriteRune('.')
sb.WriteString(strconv.Itoa(tid))
}
// I don't care if it isn't used,, it will likely be in the future. Nolint. // I don't care if it isn't used,, it will likely be in the future. Nolint.
// nolint // nolint
func getTopicURLPrefix() string { func getTopicURLPrefix() string {

View File

@ -9,6 +9,7 @@ import (
type UserCache interface { type UserCache interface {
DeallocOverflow(evictPriority bool) (evicted int) // May cause thread contention, looks for items to evict DeallocOverflow(evictPriority bool) (evicted int) // May cause thread contention, looks for items to evict
Get(id int) (*User, error) Get(id int) (*User, error)
Getn(id int) *User
GetUnsafe(id int) (*User, error) GetUnsafe(id int) (*User, error)
BulkGet(ids []int) (list []*User) BulkGet(ids []int) (list []*User)
Set(item *User) error Set(item *User) error
@ -95,12 +96,19 @@ func (s *MemoryUserCache) DeallocOverflow(evictPriority bool) (evicted int) {
// Get fetches a user by ID. Returns ErrNoRows if not present. // Get fetches a user by ID. Returns ErrNoRows if not present.
func (s *MemoryUserCache) Get(id int) (*User, error) { func (s *MemoryUserCache) Get(id int) (*User, error) {
s.RLock() s.RLock()
item, ok := s.items[id] item := s.items[id]
s.RUnlock() s.RUnlock()
if ok { if item == nil {
return item, nil return item, ErrNoRows
} }
return item, ErrNoRows return item, nil
}
func (s *MemoryUserCache) Getn(id int) *User {
s.RLock()
item := s.items[id]
s.RUnlock()
return item
} }
// BulkGet fetches multiple users by their IDs. Indices without users will be set to nil, so make sure you check for those, we might want to change this behaviour to make it less confusing. // BulkGet fetches multiple users by their IDs. Indices without users will be set to nil, so make sure you check for those, we might want to change this behaviour to make it less confusing.

View File

@ -18,6 +18,7 @@ var ErrLongUsername = errors.New("this username is too long")
type UserStore interface { type UserStore interface {
DirtyGet(id int) *User DirtyGet(id int) *User
Get(id int) (*User, error) Get(id int) (*User, error)
Getn(id int) *User
GetByName(name string) (*User, error) GetByName(name string) (*User, error)
Exists(id int) bool Exists(id int) bool
GetOffset(offset, perPage int) ([]*User, error) GetOffset(offset, perPage int) ([]*User, error)
@ -93,6 +94,15 @@ func (s *DefaultUserStore) Get(id int) (*User, error) {
u = &User{ID: id, Loggedin: true} u = &User{ID: id, Loggedin: true}
var embeds int var embeds int
err = s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) err = s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds)
/*if err != nil {
return nil, err
}
if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr()
u.ParseSettings.NoEmbed = embeds == 0
}
u.Init()
s.cache.Set(u)*/
if err == nil { if err == nil {
if embeds != -1 { if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr() u.ParseSettings = DefaultParseSettings.CopyPtr()
@ -104,21 +114,43 @@ func (s *DefaultUserStore) Get(id int) (*User, error) {
return u, err return u, err
} }
func (s *DefaultUserStore) Getn(id int) *User {
u := s.cache.Getn(id)
if u != nil {
return u
}
u = &User{ID: id, Loggedin: true}
var embeds int
err := s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds)
if err != nil {
return nil
}
if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr()
u.ParseSettings.NoEmbed = embeds == 0
}
u.Init()
s.cache.Set(u)
return u
}
// TODO: Log weird cache errors? Not just here but in every *Cache? // TODO: Log weird cache errors? Not just here but in every *Cache?
// ! This bypasses the cache, use frugally // ! This bypasses the cache, use frugally
func (s *DefaultUserStore) GetByName(name string) (*User, error) { func (s *DefaultUserStore) GetByName(name string) (*User, error) {
u := &User{Loggedin: true} u := &User{Loggedin: true}
var embeds int var embeds int
err := s.getByName.QueryRow(name).Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) err := s.getByName.QueryRow(name).Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds)
if err == nil { if err != nil {
if embeds != -1 { return nil, err
u.ParseSettings = DefaultParseSettings.CopyPtr()
u.ParseSettings.NoEmbed = embeds == 0
}
u.Init()
s.cache.Set(u)
} }
return u, err if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr()
u.ParseSettings.NoEmbed = embeds == 0
}
u.Init()
s.cache.Set(u)
return u, nil
} }
// TODO: Optimise this, so we don't wind up hitting the database every-time for small gaps // TODO: Optimise this, so we don't wind up hitting the database every-time for small gaps

View File

@ -27,7 +27,7 @@ func init() {
} }
// TODO: Remove the View part of the name? // TODO: Remove the View part of the name?
func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c.Header) c.RouteError { func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header) c.RouteError {
var reCreatedAt time.Time var reCreatedAt time.Time
var reContent, reCreatedByName, reAvatar string var reContent, reCreatedByName, reAvatar string
var rid, reCreatedBy, reLastEdit, reLastEditBy, reGroup int var rid, reCreatedBy, reLastEdit, reLastEditBy, reGroup int
@ -36,13 +36,13 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c
// TODO: Do a 301 if it's the wrong username? Do a canonical too? // TODO: Do a 301 if it's the wrong username? Do a canonical too?
_, pid, err := ParseSEOURL(r.URL.Path[len("/user/"):]) _, pid, err := ParseSEOURL(r.URL.Path[len("/user/"):])
if err != nil { if err != nil {
return c.SimpleError(phrases.GetErrorPhrase("url_id_must_be_integer"), w, r, header) return c.SimpleError(phrases.GetErrorPhrase("url_id_must_be_integer"), w, r, h)
} }
// TODO: Preload this? // TODO: Preload this?
header.AddSheet(header.Theme.Name + "/profile.css") h.AddSheet(h.Theme.Name + "/profile.css")
if user.Loggedin { if user.Loggedin {
header.AddScriptAsync("profile_member.js") h.AddScriptAsync("profile_member.js")
} }
var puser *c.User var puser *c.User
@ -54,13 +54,13 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c
// TODO: Add a shared function for checking for ErrNoRows and internal erroring if it's not that case? // TODO: Add a shared function for checking for ErrNoRows and internal erroring if it's not that case?
puser, err = c.Users.Get(pid) puser, err = c.Users.Get(pid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return c.NotFound(w, r, header) return c.NotFound(w, r, h)
} else if err != nil { } else if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
} }
header.Title = phrases.GetTitlePhrasef("profile", puser.Name) h.Title = phrases.GetTitlePhrasef("profile", puser.Name)
header.Path = c.BuildProfileURL(c.NameToSlug(puser.Name), puser.ID) h.Path = c.BuildProfileURL(c.NameToSlug(puser.Name), puser.ID)
// Get the replies.. // Get the replies..
rows, err := profileStmts.getReplies.Query(puser.ID) rows, err := profileStmts.getReplies.Query(puser.ID)
@ -77,8 +77,8 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c
reLiked := false reLiked := false
reLikeCount := 0 reLikeCount := 0
ru := &c.ReplyUser{Reply: c.Reply{rid, puser.ID, reContent, reCreatedBy, reGroup, reCreatedAt, reLastEdit, reLastEditBy, 0, "", reLiked, reLikeCount, 0, ""}, ContentHtml: c.ParseMessage(reContent, 0, "", user.ParseSettings, user), CreatedByName: reCreatedByName, Avatar: reAvatar, Level: 0} ru := &c.ReplyUser{Reply: c.Reply{rid, puser.ID, reContent, reCreatedBy /*, reGroup*/, reCreatedAt, reLastEdit, reLastEditBy, 0, "", reLiked, reLikeCount, 0, ""}, ContentHtml: c.ParseMessage(reContent, 0, "", user.ParseSettings, user), CreatedByName: reCreatedByName, Avatar: reAvatar, Group: reGroup, Level: 0}
_, err = ru.Init() _, err = ru.Init(user)
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
@ -111,6 +111,6 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c
canMessage := (!blockedInv && user.Perms.UseConvos) || (!blockedInv && puser.IsSuperMod && user.Perms.UseConvosOnlyWithMod) || user.IsSuperMod canMessage := (!blockedInv && user.Perms.UseConvos) || (!blockedInv && puser.IsSuperMod && user.Perms.UseConvosOnlyWithMod) || user.IsSuperMod
canComment := !blockedInv && user.Perms.CreateProfileReply canComment := !blockedInv && user.Perms.CreateProfileReply
ppage := c.ProfilePage{header, reList, *puser, currentScore, nextScore, blocked, canMessage, canComment} ppage := c.ProfilePage{h, reList, *puser, currentScore, nextScore, blocked, canMessage, canComment}
return renderTemplate("profile", w, r, header, ppage) return renderTemplate("profile", w, r, h, ppage)
} }

View File

@ -209,7 +209,7 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.R
// TODO: Disable stat updates in posts handled by plugin_guilds // TODO: Disable stat updates in posts handled by plugin_guilds
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid string) c.RouteError { func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, u *c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1" js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid) rid, err := strconv.Atoi(srid)
if err != nil { if err != nil {
@ -231,15 +231,15 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.EditReply { if !u.Perms.ViewTopic || !u.Perms.EditReply {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
if topic.IsClosed && !user.Perms.CloseTopic { if topic.IsClosed && !u.Perms.CloseTopic {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
err = reply.SetPost(r.PostFormValue("edit_item")) err = reply.SetPost(r.PostFormValue("edit_item"))
@ -257,7 +257,7 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
skip, rerr := lite.Hooks.VhookSkippable("action_end_edit_reply", reply.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_edit_reply", reply.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }
@ -265,7 +265,7 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid
if !js { if !js {
http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther) http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
} else { } else {
outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums", user.ParseSettings, user)}) outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums", u.ParseSettings, u)})
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -277,7 +277,7 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid
// TODO: Refactor this // TODO: Refactor this
// TODO: Disable stat updates in posts handled by plugin_guilds // TODO: Disable stat updates in posts handled by plugin_guilds
func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid string) c.RouteError { func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, u *c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1" js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid) rid, err := strconv.Atoi(srid)
if err != nil { if err != nil {
@ -298,20 +298,20 @@ func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sri
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if reply.CreatedBy != user.ID { if reply.CreatedBy != u.ID {
if !user.Perms.ViewTopic || !user.Perms.DeleteReply { if !u.Perms.ViewTopic || !u.Perms.DeleteReply {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
} }
if err := reply.Delete(); err != nil { if err := reply.Delete(); err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
skip, rerr := lite.Hooks.VhookSkippable("action_end_delete_reply", reply.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_delete_reply", reply.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }
@ -334,7 +334,7 @@ func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sri
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
}*/ }*/
err = c.ModLogs.Create("delete", reply.ParentID, "reply", user.GetIP(), user.ID) err = c.ModLogs.Create("delete", reply.ParentID, "reply", u.GetIP(), u.ID)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -455,7 +455,7 @@ func RemoveAttachFromReplySubmit(w http.ResponseWriter, r *http.Request, u *c.Us
return nil return nil
} }
func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid string) c.RouteError { func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, u *c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1" js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid) rid, err := strconv.Atoi(srid)
if err != nil { if err != nil {
@ -477,39 +477,39 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.LikeItem { if !u.Perms.ViewTopic || !u.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
if reply.CreatedBy == user.ID { if reply.CreatedBy == u.ID {
return c.LocalErrorJSQ("You can't like your own replies", w, r, user, js) return c.LocalErrorJSQ("You can't like your own replies", w, r, u, js)
} }
_, err = c.Users.Get(reply.CreatedBy) _, err = c.Users.Get(reply.CreatedBy)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return c.LocalErrorJSQ("The target user doesn't exist", w, r, user, js) return c.LocalErrorJSQ("The target user doesn't exist", w, r, u, js)
} else if err != nil { } else if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
err = reply.Like(user.ID) err = reply.Like(u.ID)
if err == c.ErrAlreadyLiked { if err == c.ErrAlreadyLiked {
return c.LocalErrorJSQ("You've already liked this!", w, r, user, js) return c.LocalErrorJSQ("You've already liked this!", w, r, u, js)
} else if err != nil { } else if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
// ! Be careful about leaking per-route permission state with user ptr // ! Be careful about leaking per-route permission state with user ptr
alert := c.Alert{ActorID: user.ID, TargetUserID: reply.CreatedBy, Event: "like", ElementType: "post", ElementID: rid, Actor: user} alert := c.Alert{ActorID: u.ID, TargetUserID: reply.CreatedBy, Event: "like", ElementType: "post", ElementID: rid, Actor: u}
err = c.AddActivityAndNotifyTarget(alert) err = c.AddActivityAndNotifyTarget(alert)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
skip, rerr := lite.Hooks.VhookSkippable("action_end_like_reply", reply.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_like_reply", reply.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }
@ -522,7 +522,7 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid
return nil return nil
} }
func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid string) c.RouteError { func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, u *c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1" js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid) rid, err := strconv.Atoi(srid)
if err != nil { if err != nil {
@ -544,22 +544,22 @@ func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sri
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.LikeItem { if !u.Perms.ViewTopic || !u.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
_, err = c.Users.Get(reply.CreatedBy) _, err = c.Users.Get(reply.CreatedBy)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return c.LocalErrorJSQ("The target user doesn't exist", w, r, user, js) return c.LocalErrorJSQ("The target user doesn't exist", w, r, u, js)
} else if err != nil { } else if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
err = reply.Unlike(user.ID) err = reply.Unlike(u.ID)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -577,7 +577,7 @@ func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sri
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_reply", reply.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_reply", reply.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }

View File

@ -109,14 +109,15 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
var poll c.Poll var poll *c.Poll
if topic.Poll != 0 { if topic.Poll != 0 {
pPoll, err := c.Polls.Get(topic.Poll) pPoll, err := c.Polls.Get(topic.Poll)
if err != nil { if err != nil {
log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID)) log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID))
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
poll = pPoll.Copy() poll = new(c.Poll)
*poll = pPoll.Copy()
} }
if topic.LikeCount > 0 && user.Liked > 0 { if topic.LikeCount > 0 && user.Liked > 0 {
@ -149,13 +150,12 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H
if strings.HasPrefix(r.URL.Fragment, "post-") { if strings.HasPrefix(r.URL.Fragment, "post-") {
pFrag, _ = strconv.Atoi(strings.TrimPrefix(r.URL.Fragment, "post-")) pFrag, _ = strconv.Atoi(strings.TrimPrefix(r.URL.Fragment, "post-"))
} }
rlist, ogdesc, err := topic.Replies(offset, pFrag, user) rlist, err := topic.Replies(offset, pFrag, user)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return c.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) return c.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user)
} else if err != nil { } else if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
header.OGDesc = ogdesc
tpage.ItemList = rlist tpage.ItemList = rlist
} }
@ -166,7 +166,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H
var rerr c.RouteError var rerr c.RouteError
tmpl := forum.Tmpl tmpl := forum.Tmpl
if r.FormValue("i") == "1" { if r.FormValue("i") == "1" {
if tpage.Poll.ID != 0 { if tpage.Poll != nil {
header.AddXRes("chartist/chartist.min.css", "chartist/chartist.min.js") header.AddXRes("chartist/chartist.min.css", "chartist/chartist.min.js")
} }
if tmpl == "" { if tmpl == "" {
@ -179,7 +179,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H
} }
} }
} else { } else {
if tpage.Poll.ID != 0 { if tpage.Poll != nil {
header.AddSheet("chartist/chartist.min.css") header.AddSheet("chartist/chartist.min.css")
header.AddScript("chartist/chartist.min.js") header.AddScript("chartist/chartist.min.js")
} }

View File

@ -1,4 +1,4 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}"> <nav class="colstack_left"aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div> <div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div> </div>

View File

@ -4,10 +4,10 @@
<div id="panel_users" class="colstack_item rowlist bgavatars"> <div id="panel_users" class="colstack_item rowlist bgavatars">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem" style="background-image:url('{{.Avatar}}');"> <div class="rowitem" style="background-image:url('{{.Avatar}}');">
<a class="rowAvatar"{{if $.CurrentUser.Perms.EditUser}} href="/panel/users/edit/{{.ID}}"{{end}}> <a class="rowAvatar"{{if $.CurrentUser.Perms.EditUser}}href="/panel/users/edit/{{.ID}}"{{end}}>
<img class="bgsub"src="{{.Avatar}}"alt="Avatar"aria-hidden="true"> <img class="bgsub"src="{{.Avatar}}"alt="Avatar"aria-hidden="true">
</a> </a>
<a class="rowTitle"{{if $.CurrentUser.Perms.EditUser}} href="/panel/users/edit/{{.ID}}"{{end}}>{{.Name}}</a> <a class="rowTitle"{{if $.CurrentUser.Perms.EditUser}}href="/panel/users/edit/{{.ID}}"{{end}}>{{.Name}}</a>
<span class="panel_floater"> <span class="panel_floater">
<a href="{{.Link}}" class="tag-mini profile_url">{{lang "panel_users_profile"}}</a> <a href="{{.Link}}" class="tag-mini profile_url">{{lang "panel_users_profile"}}</a>
{{if (.Tag) and (.IsSuperMod)}}<span class="panel_tag">{{.Tag}}</span></span>{{end}} {{if (.Tag) and (.IsSuperMod)}}<span class="panel_tag">{{.Tag}}</span></span>{{end}}

View File

@ -26,32 +26,7 @@
</div> </div>
<div class="rowblock post_container"> <div class="rowblock post_container">
{{if .Poll.ID}}<form id="poll_{{.Poll.ID}}_form" action="/poll/vote/{{.Poll.ID}}?s={{.CurrentUser.Session}}" method="post"></form> {{if .Poll}}{{template "topic_alt_poll.html" . }}{{end}}
<article class="rowitem passive deletable_block editable_parent post_item poll_item top_post hide_on_edit">
{{/**{{template "topic_alt_userinfo.html" .Topic }}**/}}
<div id="poll_voter_{{.Poll.ID}}" class="content_container poll_voter">
<div class="topic_content user_content">
{{range .Poll.QuickOptions}}
<div class="poll_option">
<input form="poll_{{$.Poll.ID}}_form" id="poll_option_{{.ID}}" name="poll_option_input" type="checkbox" value="{{.ID}}">
<label class="poll_option_label" for="poll_option_{{.ID}}"><div class="sel"></div></label>
<span id="poll_option_text_{{.ID}}" class="poll_option_text">{{.Value}}</span>
</div>
{{end}}
<div class="poll_buttons">
<button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">{{lang "topic.poll_vote"}}</button>
<button class="poll_results_button" data-poll-id="{{.Poll.ID}}">{{lang "topic.poll_results"}}</button>
<a href="#"><button class="poll_cancel_button">{{lang "topic.poll_cancel"}}</button></a>
</div>
</div>
</div>
<div id="poll_results_{{.Poll.ID}}" class="content_container poll_results auto_hide">
<div class="topic_content user_content">
<div class="auto_hide poll_no_results">{{lang "topic.poll_no_results"}}</div>
</div>
</div>
</article>
{{end}}
<article {{scope "opening_post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item top_post{{if .Topic.Attachments}} has_attachs{{end}}" aria-label="{{lang "topic.opening_post_aria"}}"> <article {{scope "opening_post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item top_post{{if .Topic.Attachments}} has_attachs{{end}}" aria-label="{{lang "topic.opening_post_aria"}}">
{{template "topic_alt_userinfo.html" .Topic }} {{template "topic_alt_userinfo.html" .Topic }}
<div class="content_container"> <div class="content_container">
@ -59,11 +34,11 @@
{{if .CurrentUser.Loggedin}}<textarea name="topic_content" class="show_on_edit topic_content_input edit_source">{{.Topic.Content}}</textarea> {{if .CurrentUser.Loggedin}}<textarea name="topic_content" class="show_on_edit topic_content_input edit_source">{{.Topic.Content}}</textarea>
{{if .CurrentUser.Perms.EditTopic}} {{if .CurrentUser.Perms.EditTopic}}
<div class="show_on_edit attach_edit_bay" type="topic" id="{{.Topic.ID}}"> <div class="show_on_edit attach_edit_bay"type="topic"id="{{.Topic.ID}}">
{{range .Topic.Attachments}} {{range .Topic.Attachments}}
<div class="attach_item attach_item_item{{if .Image}} attach_image_holder{{end}}"> <div class="attach_item attach_item_item{{if .Image}} attach_image_holder{{end}}">
{{if .Image}}<img src="//{{$.Header.Site.URL}}/attachs/{{.Path}}?sid={{.SectionID}}&amp;stype=forums" height=24 width=24>{{end}} {{if .Image}}<img src="//{{$.Header.Site.URL}}/attachs/{{.Path}}?sid={{.SectionID}}&amp;stype=forums"height=24 width=24>{{end}}
<span class="attach_item_path" aid="{{.ID}}" fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span> <span class="attach_item_path"aid="{{.ID}}"fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span>
<button class="attach_item_select">{{lang "topic.select_button_text"}}</button> <button class="attach_item_select">{{lang "topic.select_button_text"}}</button>
<button class="attach_item_copy">{{lang "topic.copy_button_text"}}</button> <button class="attach_item_copy">{{lang "topic.copy_button_text"}}</button>
</div> </div>

View File

@ -0,0 +1,25 @@
<form id="poll_{{.Poll.ID}}_form" action="/poll/vote/{{.Poll.ID}}?s={{.CurrentUser.Session}}"method="post"></form>
<article class="rowitem passive deletable_block editable_parent post_item poll_item top_post hide_on_edit">
{{/**{{template "topic_alt_userinfo.html" .Topic }}**/}}
<div id="poll_voter_{{.Poll.ID}}" class="content_container poll_voter">
<div class="topic_content user_content">
{{range .Poll.QuickOptions}}
<div class="poll_option">
<input form="poll_{{$.Poll.ID}}_form" id="poll_option_{{.ID}}" name="poll_option_input" type="checkbox" value="{{.ID}}">
<label class="poll_option_label"for="poll_option_{{.ID}}"><div class="sel"></div></label>
<span id="poll_option_text_{{.ID}}"class="poll_option_text">{{.Value}}</span>
</div>
{{end}}
<div class="poll_buttons">
<button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">{{lang "topic.poll_vote"}}</button>
<button class="poll_results_button"data-poll-id="{{.Poll.ID}}">{{lang "topic.poll_results"}}</button>
<a href="#"><button class="poll_cancel_button">{{lang "topic.poll_cancel"}}</button></a>
</div>
</div>
</div>
<div id="poll_results_{{.Poll.ID}}" class="content_container poll_results auto_hide">
<div class="topic_content user_content">
<div class="auto_hide poll_no_results">{{lang "topic.poll_no_results"}}</div>
</div>
</div>
</article>

View File

@ -1,8 +1,8 @@
{{range .ItemList}}<article {{scope "post"}} id="post-{{.ID}}" itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item{{if .ActionType}} action_item{{end}}{{if .Attachments}} has_attachs{{end}}"> {{range .ItemList}}<article {{scope "post"}} id="post-{{.ID}}"itemscope itemtype="http://schema.org/CreativeWork"class="rowitem passive deletable_block editable_parent post_item{{if .ActionType}} action_item{{end}}{{if .Attachments}} has_attachs{{end}}">
{{if js}}js{{/**{{ptmpl "topic_alt_userinfo" .}}**/}}{{else}}{{template "topic_alt_userinfo.html" . }}{{end}} {{if js}}js{{/**{{ptmpl "topic_alt_userinfo" .}}**/}}{{else}}{{template "topic_alt_userinfo.html" . }}{{end}}
<div class="content_container"> <div class="content_container">
{{if .ActionType}} {{if .ActionType}}
<span class="action_icon" aria-hidden="true">{{.ActionIcon}}</span><span itemprop="text">{{.ActionType}}</span> <span class="action_icon"aria-hidden="true">{{.ActionIcon}}</span><span itemprop="text">{{.ActionType}}</span>
{{else}} {{else}}
<div class="editable_block user_content"itemprop="text">{{.ContentHtml}}</div> <div class="editable_block user_content"itemprop="text">{{.ContentHtml}}</div>
{{if $.CurrentUser.Loggedin}} {{if $.CurrentUser.Loggedin}}
@ -13,15 +13,15 @@
{{range .Attachments}} {{range .Attachments}}
<div class="attach_item attach_item_item{{if .Image}} attach_image_holder{{end}}"> <div class="attach_item attach_item_item{{if .Image}} attach_image_holder{{end}}">
{{if .Image}}<img src="//{{$.Header.Site.URL}}/attachs/{{.Path}}?sid={{.SectionID}}&amp;stype=forums"height=24 width=24>{{end}} {{if .Image}}<img src="//{{$.Header.Site.URL}}/attachs/{{.Path}}?sid={{.SectionID}}&amp;stype=forums"height=24 width=24>{{end}}
<span class="attach_item_path" aid={{.ID}} fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span> <span class="attach_item_path"aid={{.ID}} fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span>
<button class="attach_item_select">{{lang "topic.select_button_text"}}</button> <button class="attach_item_select">{{lang "topic.select_button_text"}}</button>
<button class="attach_item_copy">{{lang "topic.copy_button_text"}}</button> <button class="attach_item_copy">{{lang "topic.copy_button_text"}}</button>
</div> </div>
{{end}} {{end}}
<div class="attach_item attach_item_buttons"> <div class="attach_item attach_item_buttons">
{{if $.CurrentUser.Perms.UploadFiles}} {{if $.CurrentUser.Perms.UploadFiles}}
<input name="upload_files" class="upload_files_post auto_hide" id="upload_files_post_{{.ID}}" multiple type="file"> <input name="upload_files"class="upload_files_post auto_hide"id="upload_files_post_{{.ID}}"multiple type="file">
<label for="upload_files_post_{{.ID}}" class="formbutton add_file_button">{{lang "topic.upload_button_text"}}</label>{{end}} <label for="upload_files_post_{{.ID}}"class="formbutton add_file_button">{{lang "topic.upload_button_text"}}</label>{{end}}
<button class="attach_item_delete formbutton">{{lang "topic.delete_button_text"}}</button> <button class="attach_item_delete formbutton">{{lang "topic.delete_button_text"}}</button>
</div> </div>
</div> </div>
@ -31,23 +31,23 @@
<div class="action_button_left"> <div class="action_button_left">
{{if $.CurrentUser.Loggedin}} {{if $.CurrentUser.Loggedin}}
{{if $.CurrentUser.Perms.LikeItem}}{{if ne $.CurrentUser.ID .CreatedBy}} {{if $.CurrentUser.Perms.LikeItem}}{{if ne $.CurrentUser.ID .CreatedBy}}
{{if .Liked}}<a href="/reply/unlike/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button like_item remove_like" aria-label="{{lang "topic.post_unlike_aria"}}" data-action="unlike"></a>{{else}} {{if .Liked}}<a href="/reply/unlike/submit/{{.ID}}?s={{$.CurrentUser.Session}}"class="action_button like_item remove_like"aria-label="{{lang "topic.post_unlike_aria"}}"data-action="unlike"></a>{{else}}
<a href="/reply/like/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button like_item add_like" aria-label="{{lang "topic.post_like_aria"}}" data-action="like"></a>{{end}} <a href="/reply/like/submit/{{.ID}}?s={{$.CurrentUser.Session}}"class="action_button like_item add_like"aria-label="{{lang "topic.post_like_aria"}}"data-action="like"></a>{{end}}
{{end}}{{end}} {{end}}{{end}}
<a href="" class="action_button quote_item" aria-label="{{lang "topic.quote_aria"}}" data-action="quote"></a> <a href=""class="action_button quote_item"aria-label="{{lang "topic.quote_aria"}}"data-action="quote"></a>
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}} {{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button edit_item" aria-label="{{lang "topic.post_edit_aria"}}" data-action="edit"></a>{{end}} {{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}"class="action_button edit_item"aria-label="{{lang "topic.post_edit_aria"}}"data-action="edit"></a>{{end}}
{{end}} {{end}}
{{if .Deletable}}<a href="/reply/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="action_button delete_item" aria-label="{{lang "topic.post_delete_aria"}}" data-action="delete"></a>{{end}} {{if .Deletable}}<a href="/reply/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}"class="action_button delete_item"aria-label="{{lang "topic.post_delete_aria"}}"data-action="delete"></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IP}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item_button hide_on_big" aria-label="{{lang "topic.ip_full_aria"}}" data-action="ip"></a>{{end}} {{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IP}}"title="{{lang "topic.ip_full_tooltip"}}"class="action_button ip_item_button hide_on_big"aria-label="{{lang "topic.ip_full_aria"}}"data-action="ip"></a>{{end}}
<a href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&amp;type=reply" class="action_button report_item" aria-label="{{lang "topic.report_aria"}}" data-action="report"></a> <a href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&amp;type=reply"class="action_button report_item"aria-label="{{lang "topic.report_aria"}}"data-action="report"></a>
<a href="#" class="action_button button_menu"></a> <a href="#"class="action_button button_menu"></a>
{{end}} {{end}}
</div> </div>
<div class="action_button_right"> <div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a> <a class="action_button like_count hide_on_micro"aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile" title="{{abstime .CreatedAt}}">{{reltime .CreatedAt}}</a> <a class="action_button created_at hide_on_mobile"title="{{abstime .CreatedAt}}">{{reltime .CreatedAt}}</a>
{{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IP}}"title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IP}}</a>{{end}}{{end}} {{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IP}}"title="IP Address"class="action_button ip_item hide_on_mobile"aria-hidden="true">{{.IP}}</a>{{end}}{{end}}
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -17,17 +17,17 @@
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
{{if .CurrentUser.Perms.EditTopic}} {{if .CurrentUser.Perms.EditTopic}}
<form id="edit_topic_form" action='/topic/edit/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}' method="post"></form> <form id="edit_topic_form" action='/topic/edit/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}' method="post"></form>
<input form='edit_topic_form' class='show_on_edit topic_name_input' name="topic_name" value='{{.Topic.Title}}' type="text" aria-label="{{lang "topic.title_input_aria"}}"/> <input form='edit_topic_form' class='show_on_edit topic_name_input' name="topic_name" value='{{.Topic.Title}}' type="text" aria-label="{{lang "topic.title_input_aria"}}">
<button form='edit_topic_form' name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic.update_button"}}</button> <button form='edit_topic_form' name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic.update_button"}}</button>
{{end}} {{end}}
{{end}} {{end}}
</div> </div>
</div> </div>
{{if .Poll.ID}}{{template "topic_poll.html" . }}{{end}} {{if .Poll}}{{template "topic_poll.html" . }}{{end}}
<article {{scope "opening_post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowblock post_container top_post" aria-label="{{lang "topic.opening_post_aria"}}"> <article {{scope "opening_post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowblock post_container top_post" aria-label="{{lang "topic.opening_post_aria"}}">
<div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="background-image:url({{.Topic.Avatar}}),url(/s/{{.Header.Theme.Name}}/post-avatar-bg.jpg);background-position:0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat,repeat-y;"> <div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="background-image:url({{.Topic.Avatar}}),url(/s/{{.Header.Theme.Name}}/post-avatar-bg.jpg);background-position:0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat,repeat-y;">
<div class="hide_on_edit topic_content user_content" itemprop="text">{{.Topic.ContentHTML}}</div> <div class="hide_on_edit topic_content user_content"itemprop="text">{{.Topic.ContentHTML}}</div>
{{if .CurrentUser.Loggedin}}<textarea name="topic_content" class="show_on_edit topic_content_input edit_source">{{.Topic.Content}}</textarea>{{end}} {{if .CurrentUser.Loggedin}}<textarea name="topic_content" class="show_on_edit topic_content_input edit_source">{{.Topic.Content}}</textarea>{{end}}
<span class="controls{{if .Topic.LikeCount}} has_likes{{end}}" aria-label="{{lang "topic.post_controls_aria"}}"> <span class="controls{{if .Topic.LikeCount}} has_likes{{end}}" aria-label="{{lang "topic.post_controls_aria"}}">
@ -75,8 +75,8 @@
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
<div class="rowblock topic_reply_form quick_create_form" aria-label="{{lang "topic.reply_aria"}}"> <div class="rowblock topic_reply_form quick_create_form" aria-label="{{lang "topic.reply_aria"}}">
<form id="quick_post_form" enctype="multipart/form-data" action="/reply/create/?s={{.CurrentUser.Session}}" method="post"></form> <form id="quick_post_form" enctype="multipart/form-data" action="/reply/create/?s={{.CurrentUser.Session}}" method="post"></form>
<input form="quick_post_form" name="tid" value='{{.Topic.ID}}' type="hidden"/> <input form="quick_post_form" name="tid" value='{{.Topic.ID}}' type="hidden">
<input form="quick_post_form" id="has_poll_input" name="has_poll" value=0 type="hidden"/> <input form="quick_post_form" id="has_poll_input" name="has_poll" value=0 type="hidden">
<div class="formrow real_first_child"> <div class="formrow real_first_child">
<div class="formitem"> <div class="formitem">
<textarea id="input_content" form="quick_post_form" name="content" placeholder="{{lang "topic.reply_content"}}" required></textarea> <textarea id="input_content" form="quick_post_form" name="content" placeholder="{{lang "topic.reply_content"}}" required></textarea>
@ -85,9 +85,9 @@
<div class="formrow poll_content_row auto_hide"> <div class="formrow poll_content_row auto_hide">
<div class="formitem"> <div class="formitem">
<div class="pollinput" data-pollinput=0> <div class="pollinput" data-pollinput=0>
<input type="checkbox" disabled /> <input type="checkbox" disabled>
<label class="pollinputlabel"></label> <label class="pollinputlabel"></label>
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "topic.reply_add_poll_option_first"}}"/> <input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "topic.reply_add_poll_option_first"}}">
</div> </div>
</div> </div>
</div> </div>
@ -96,7 +96,7 @@
<button form="quick_post_form" name="reply-button" class="formbutton">{{lang "topic.reply_button"}}</button> <button form="quick_post_form" name="reply-button" class="formbutton">{{lang "topic.reply_button"}}</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">{{lang "topic.reply_add_poll_button"}}</button> <button form="quick_post_form" class="formbutton" id="add_poll_button">{{lang "topic.reply_add_poll_button"}}</button>
{{if .CurrentUser.Perms.UploadFiles}} {{if .CurrentUser.Perms.UploadFiles}}
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display:none;" /> <input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" class="auto_hide">
<label for="upload_files" class="formbutton add_file_button">{{lang "topic.reply_add_file_button"}}</label> <label for="upload_files" class="formbutton add_file_button">{{lang "topic.reply_add_file_button"}}</label>
<div id="upload_file_dock"></div>{{end}} <div id="upload_file_dock"></div>{{end}}
</div> </div>

View File

@ -1,22 +1,22 @@
<article class="rowblock post_container poll" aria-level="{{lang "topic.poll_aria"}}"> <article class="rowblock post_container poll"aria-level="{{lang "topic.poll_aria"}}">
<div class="rowitem passive editable_parent post_item poll_item {{.Topic.ClassName}}" style="background-image:url({{.Topic.Avatar}}),url(/s/{{.Header.Theme.Name}}/post-avatar-bg.jpg);background-position:0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat,repeat-y;"> <div class="rowitem passive editable_parent post_item poll_item {{.Topic.ClassName}}" style="background-image:url({{.Topic.Avatar}}),url(/s/{{.Header.Theme.Name}}/post-avatar-bg.jpg);background-position:0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat,repeat-y;">
<div class="topic_content user_content"> <div class="topic_content user_content">
{{range .Poll.QuickOptions}} {{range .Poll.QuickOptions}}
<div class="poll_option"> <div class="poll_option">
<input form="poll_{{$.Poll.ID}}_form" id="poll_option_{{.ID}}" name="poll_option_input" type="checkbox" value="{{.ID}}"> <input form="poll_{{$.Poll.ID}}_form"id="poll_option_{{.ID}}"name="poll_option_input"type="checkbox"value="{{.ID}}">
<label class="poll_option_label" for="poll_option_{{.ID}}"> <label class="poll_option_label"for="poll_option_{{.ID}}">
<div class="sel"></div> <div class="sel"></div>
</label> </label>
<span id="poll_option_text_{{.ID}}" class="poll_option_text">{{.Value}}</span> <span id="poll_option_text_{{.ID}}"class="poll_option_text">{{.Value}}</span>
</div> </div>
{{end}} {{end}}
<div class="poll_buttons"> <div class="poll_buttons">
<button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">{{lang "topic.poll_vote"}}</button> <button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">{{lang "topic.poll_vote"}}</button>
<button class="poll_results_button" data-poll-id="{{.Poll.ID}}">{{lang "topic.poll_results"}}</button> <button class="poll_results_button"data-poll-id="{{.Poll.ID}}">{{lang "topic.poll_results"}}</button>
<a href="#"><button class="poll_cancel_button">{{lang "topic.poll_cancel"}}</button></a> <a href="#"><button class="poll_cancel_button">{{lang "topic.poll_cancel"}}</button></a>
</div> </div>
</div> </div>
<div id="poll_results_{{.Poll.ID}}" class="poll_results auto_hide"> <div id="poll_results_{{.Poll.ID}}"class="poll_results auto_hide">
<div class="topic_content user_content"> <div class="topic_content user_content">
<div class="auto_hide poll_no_results">{{lang "topic.poll_no_results"}}</div> <div class="auto_hide poll_no_results">{{lang "topic.poll_no_results"}}</div>
</div> </div>