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) {
return nil, ErrNoRows
}
func (c *NullUserCache) Getn(id int) *User {
return nil
}
func (c *NullUserCache) BulkGet(ids []int) (list []*User) {
return make([]*User, len(ids))
}

View File

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

View File

@ -29,6 +29,7 @@ type ReplyUser struct {
URL string
//URLPrefix string
//URLName string
Group int
Level int
ActionIcon string
@ -41,7 +42,7 @@ type Reply struct {
ParentID int
Content string
CreatedBy int
Group int
//Group int
CreatedAt time.Time
LastEdit int
LastEditBy int
@ -49,7 +50,7 @@ type Reply struct {
IP string
Liked bool
LikeCount int
AttachCount int
AttachCount uint16
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 {
return 0, err
}
lastID, err := res.LastInsertId()
if err != nil {
return 0, err

View File

@ -227,7 +227,7 @@ func compileCommons(c *tmpl.CTemplateSet, head, head2 *Header, forumList []Forum
}*/
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}}
o.Add("topics", "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}
var replyList []*ReplyUser
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.Init()
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, Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach}
ru.Init(user2)
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)
o.Add("topic", "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}
// TODO: Do we want the UID on this to be 0?
//avatar, microAvatar = BuildAvatar(0, "")
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.Init()
reply := Reply{1, 1, "Yo!", 1 /*, Config.DefaultGroup*/, now, 0, 0, 1, "::1", true, 1, 1, ""}
ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: "", Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach}
ru.Init(user)
replyList = append(replyList, ru)
// TODO: Use a dummy forum list to avoid o(n) problems
var forumList []Forum
/*var forumList []Forum
forums, err := Forums.GetAll()
if err != nil {
return err
}
for _, forum := range forums {
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
htitle := func(name string) *Header {
@ -297,7 +301,7 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
return header
}
t := TItemHold(make(map[string]TItem))
err = compileCommons(c, header, header2, forumList, t)
err := compileCommons(c, header, header2, forumList, t)
if err != nil {
return err
}
@ -547,14 +551,14 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri
var replyList []*ReplyUser
// TODO: Do we really want the UID here to be zero?
avatar, microAvatar = BuildAvatar(0, "")
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.Init()
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, Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach}
ru.Init(user)
replyList = append(replyList, ru)
varList = make(map[string]tmpl.VarItem)
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)
t.AddStd("topic_posts", "c.TopicPage", tpage)
t.AddStd("topic_alt_posts", "c.TopicPage", tpage)

View File

@ -54,6 +54,20 @@ func (con *CContext) PushPhrasef(langIndex int, args string) (index int) {
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) {
con.LoopDepth++
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) {
outBuf := *con.OutBuf
node := outBuf[index]
if node.Type != "starttemplate" && node.Type != "startloop" {
panic("not a starttemplate node")
n := outBuf[index]
if n.Type != "starttemplate" && n.Type != "startloop" && n.Type != "startif" {
panic("not a starttemplate, startloop or startif node")
}
node.Body += vars
outBuf[index] = node
n.Body += vars
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})
return con.LastBufIndex()
}

View File

@ -513,8 +513,7 @@ if !ok {
}
`*/
fout += `var iw http.ResponseWriter
gzw, ok := w.(c.GzipResponseWriter)
if ok {
if gzw, ok := w.(c.GzipResponseWriter); ok {
iw = gzw.ResponseWriter
}
_ = iw
@ -711,14 +710,26 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
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)
if node.ElseList == nil {
c.detail("Selected Branch 1")
con.Push("endif", "}\n")
con.EndIf(startIf, "}\n")
if nilIf {
c.afterTemplate(con, startIf)
}
} else {
c.detail("Selected Branch 2")
con.Push("endif", "}")
con.EndIf(startIf, "}")
if nilIf {
c.afterTemplate(con, startIf)
}
con.Push("startelse", " else {\n")
c.compileSwitch(con, node.ElseList)
con.Push("endelse", "}\n")
@ -769,7 +780,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
c.detail("Range Kind Switch!")
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")
ccon := con
var depth string
@ -793,7 +804,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
con.EndLoop("}\n")
c.afterTemplate(con, startIndex)
if node.ElseList != nil {
con.Push("endif", "}")
con.EndIf(sIndex, "}")
con.Push("startelse", " else {\n")
if !useCopy {
ccon = con
@ -801,7 +812,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
c.compileSwitch(ccon, node.ElseList)
con.Push("endelse", "}\n")
} 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
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:
out += " > 0"
out += ">0"
case reflect.Bool: // Do nothing
case reflect.String:
out += " != \"\""
out += "!=\"\""
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:
c.logger.Println("Variable Name:", varname)
c.logger.Println("Variable Holder:", con.VarHolder)
@ -1675,9 +1689,9 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
return
}
}
con.Push("startif", "if "+varname+" {\n")
startIf := con.StartIf("if " + varname + " {\n")
c.addText(con, []byte("true"))
con.Push("endif", "} ")
con.EndIf(startIf, "} ")
con.Push("startelse", "else {\n")
c.addText(con, []byte("false"))
con.Push("endelse", "}\n")
@ -1945,42 +1959,66 @@ func (c *CTemplateSet) loadTemplate(fileDir, name string) (content string, err e
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)
defer c.retCall("afterTemplate")
loopDepth := 0
ifNilDepth := 0
var outBuf = *con.OutBuf
varcounts := make(map[string]int)
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++
}
// Exclude varsubs within loops for now
OLoop:
for i := loopStart; i < len(outBuf); i++ {
item := outBuf[i]
c.detail("item:", item)
if item.Type == "startloop" {
switch item.Type {
case "startloop":
loopDepth++
c.detail("loopDepth:", loopDepth)
} else if item.Type == "endloop" {
case "endloop":
loopDepth--
c.detail("loopDepth:", loopDepth)
if loopDepth == -1 {
break
break OLoop
}
} else if item.Type == "varsub" && loopDepth == 0 {
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"
@ -1991,16 +2029,32 @@ func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) {
// Exclude varsubs within loops for now
loopDepth = 0
ifNilDepth = 0
OOLoop:
for i := loopStart; i < len(outBuf); i++ {
item := outBuf[i]
if item.Type == "startloop" {
switch item.Type {
case "startloop":
loopDepth++
} else if item.Type == "endloop" {
case "endloop":
loopDepth--
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":
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)
@ -2009,6 +2063,122 @@ func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) {
}
}
}
}
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
}
}
}
}
con.AttachVars(varstr, startIndex)
}

View File

@ -188,6 +188,8 @@ func (t *TopicsRow) Topic() *Topic {
type TopicStmts struct {
getRids *sql.Stmt
getReplies *sql.Stmt
getReplies2 *sql.Stmt
getReplies3 *sql.Stmt
addReplies *sql.Stmt
updateLastReply *sql.Stmt
lock *sql.Stmt
@ -217,7 +219,9 @@ func init() {
t := "topics"
topicStmts = TopicStmts{
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(),
updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid=?").Prepare(),
lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(),
@ -570,8 +574,7 @@ var unlockai = "&#x1F513"
var stickai = "&#x1F4CC"
var unstickai = "&#x1F4CC" + aipost
func (ru *ReplyUser) Init() (group *Group, err error) {
ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy)
func (ru *ReplyUser) Init(u *User) (group *Group, err error) {
ru.ContentLines = strings.Count(ru.Content, "\n")
postGroup, err := Groups.Get(ru.Group)
@ -583,8 +586,117 @@ func (ru *ReplyUser) Init() (group *Group, err error) {
}
ru.Tag = postGroup.Tag
if u.ID != ru.CreatedBy {
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...
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
func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, ogdesc string, err error) {
var likedMap map[int]int
if user.Liked > 0 {
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{}
func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser /*, ogdesc string*/, err error) {
var likedMap, attachMap map[int]int
var likedQueryList, attachQueryList []int
var rid int
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)
ucache := Users.GetCache()
var ruser *User
if err == nil && ucache != nil {
if ucache != nil {
//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()
rf := func(r *ReplyUser) error {
rf := func(r *ReplyUser) (err error) {
//log.Printf("before r: %+v\n", r)
group, err := r.Init()
group, err := r.Init3(user, t)
if err != nil {
return err
}
@ -668,6 +775,7 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser,
if 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?
/*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 {
likedMap[r.ID] = len(rlist)
likedQueryList = append(likedQueryList, r.ID)
return nil
}
if user.Perms.EditReply && r.AttachCount > 0 {
attachMap[r.ID] = len(rlist)
attachQueryList = append(attachQueryList, r.ID)
rf3 := func(r *ReplyUser) error {
//log.Printf("before r: %+v\n", r)
group, err := r.Init2()
if err != nil {
return err
}
var parseSettings *ParseSettings
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
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
}
// 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")
reply := &ReplyUser{ClassName: "", Reply: *re, CreatedByName: ruser.Name, Avatar: ruser.Avatar /*URLPrefix: ruser.URLPrefix, URLName: ruser.URLName, */, Level: ruser.Level, Tag: ruser.Tag}
reply.Group = ruser.Group
err = rf(reply)
if err != nil {
return nil, "", err
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}
if err = rf3(r); 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 {
//log.Print("reply query serve")
rows, err := topicStmts.getReplies.Query(t.ID, offset, Config.ItemsPerPage)
rf2 := func(r *ReplyUser) {
if r.LikeCount > 0 && user.Liked > 0 {
if likedMap == nil {
likedMap = map[int]int{r.ID: len(rlist)}
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)
}
}
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 = append(rlist, r)
//log.Printf("r: %d-%d", r.ID, len(rlist)-1)
}
if !user.Perms.ViewIPs && ruser != nil {
rows, err := topicStmts.getReplies3.Query(t.ID, offset, Config.ItemsPerPage)
if err != nil {
return nil, "", err
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
return nil, err
}
err = rf(r)
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
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
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
if user.Liked > 0 && len(likedQueryList) > 1 /*&& user.LastLiked <= time.Now()*/ {
eids, err := Likes.BulkExists(likedQueryList[1:], user.ID, "replies")
if user.Liked > 0 && len(likedQueryList) > 0 /*&& user.LastLiked <= time.Now()*/ {
eids, err := Likes.BulkExists(likedQueryList, user.ID, "replies")
if err != nil {
return nil, "", err
return nil, err
}
for _, eid := range eids {
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)
amap, err := Attachments.BulkMiniGetList("replies", attachQueryList)
if err != nil && err != sql.ErrNoRows {
return nil, "", err
return nil, err
}
//log.Printf("amap: %+v\n", amap)
//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
@ -875,6 +1135,20 @@ func BuildTopicURL(slug string, tid int) string {
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.
// nolint
func getTopicURLPrefix() string {

View File

@ -9,6 +9,7 @@ import (
type UserCache interface {
DeallocOverflow(evictPriority bool) (evicted int) // May cause thread contention, looks for items to evict
Get(id int) (*User, error)
Getn(id int) *User
GetUnsafe(id int) (*User, error)
BulkGet(ids []int) (list []*User)
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.
func (s *MemoryUserCache) Get(id int) (*User, error) {
s.RLock()
item, ok := s.items[id]
item := s.items[id]
s.RUnlock()
if ok {
return item, nil
}
if item == nil {
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.

View File

@ -18,6 +18,7 @@ var ErrLongUsername = errors.New("this username is too long")
type UserStore interface {
DirtyGet(id int) *User
Get(id int) (*User, error)
Getn(id int) *User
GetByName(name string) (*User, error)
Exists(id int) bool
GetOffset(offset, perPage int) ([]*User, error)
@ -93,6 +94,15 @@ func (s *DefaultUserStore) Get(id int) (*User, error) {
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, err
}
if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr()
u.ParseSettings.NoEmbed = embeds == 0
}
u.Init()
s.cache.Set(u)*/
if err == nil {
if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr()
@ -104,21 +114,43 @@ func (s *DefaultUserStore) Get(id int) (*User, error) {
return u, err
}
// TODO: Log weird cache errors? Not just here but in every *Cache?
// ! This bypasses the cache, use frugally
func (s *DefaultUserStore) GetByName(name string) (*User, error) {
u := &User{Loggedin: true}
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.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 {
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?
// ! This bypasses the cache, use frugally
func (s *DefaultUserStore) GetByName(name string) (*User, error) {
u := &User{Loggedin: true}
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)
if err != nil {
return nil, err
}
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

View File

@ -27,7 +27,7 @@ func init() {
}
// 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 reContent, reCreatedByName, reAvatar string
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?
_, pid, err := ParseSEOURL(r.URL.Path[len("/user/"):])
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?
header.AddSheet(header.Theme.Name + "/profile.css")
h.AddSheet(h.Theme.Name + "/profile.css")
if user.Loggedin {
header.AddScriptAsync("profile_member.js")
h.AddScriptAsync("profile_member.js")
}
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?
puser, err = c.Users.Get(pid)
if err == sql.ErrNoRows {
return c.NotFound(w, r, header)
return c.NotFound(w, r, h)
} else if err != nil {
return c.InternalError(err, w, r)
}
}
header.Title = phrases.GetTitlePhrasef("profile", puser.Name)
header.Path = c.BuildProfileURL(c.NameToSlug(puser.Name), puser.ID)
h.Title = phrases.GetTitlePhrasef("profile", puser.Name)
h.Path = c.BuildProfileURL(c.NameToSlug(puser.Name), puser.ID)
// Get the replies..
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
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}
_, err = ru.Init()
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(user)
if err != nil {
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
canComment := !blockedInv && user.Perms.CreateProfileReply
ppage := c.ProfilePage{header, reList, *puser, currentScore, nextScore, blocked, canMessage, canComment}
return renderTemplate("profile", w, r, header, ppage)
ppage := c.ProfilePage{h, reList, *puser, currentScore, nextScore, blocked, canMessage, canComment}
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: 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"
rid, err := strconv.Atoi(srid)
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
lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID)
lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.EditReply {
return c.NoPermissionsJSQ(w, r, user, js)
if !u.Perms.ViewTopic || !u.Perms.EditReply {
return c.NoPermissionsJSQ(w, r, u, js)
}
if topic.IsClosed && !user.Perms.CloseTopic {
return c.NoPermissionsJSQ(w, r, user, js)
if topic.IsClosed && !u.Perms.CloseTopic {
return c.NoPermissionsJSQ(w, r, u, js)
}
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)
}
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 {
return rerr
}
@ -265,7 +265,7 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid
if !js {
http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
} 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 {
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: 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"
rid, err := strconv.Atoi(srid)
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
lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID)
lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil {
return ferr
}
if reply.CreatedBy != user.ID {
if !user.Perms.ViewTopic || !user.Perms.DeleteReply {
return c.NoPermissionsJSQ(w, r, user, js)
if reply.CreatedBy != u.ID {
if !u.Perms.ViewTopic || !u.Perms.DeleteReply {
return c.NoPermissionsJSQ(w, r, u, js)
}
}
if err := reply.Delete(); err != nil {
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 {
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)
}*/
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 {
return c.InternalErrorJSQ(err, w, r, js)
}
@ -455,7 +455,7 @@ func RemoveAttachFromReplySubmit(w http.ResponseWriter, r *http.Request, u *c.Us
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"
rid, err := strconv.Atoi(srid)
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
lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID)
lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, user, js)
if !u.Perms.ViewTopic || !u.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, u, js)
}
if reply.CreatedBy == user.ID {
return c.LocalErrorJSQ("You can't like your own replies", w, r, user, js)
if reply.CreatedBy == u.ID {
return c.LocalErrorJSQ("You can't like your own replies", w, r, u, js)
}
_, err = c.Users.Get(reply.CreatedBy)
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 {
return c.InternalErrorJSQ(err, w, r, js)
}
err = reply.Like(user.ID)
err = reply.Like(u.ID)
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 {
return c.InternalErrorJSQ(err, w, r, js)
}
// ! 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)
if err != nil {
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 {
return rerr
}
@ -522,7 +522,7 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid
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"
rid, err := strconv.Atoi(srid)
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
lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID)
lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, user, js)
if !u.Perms.ViewTopic || !u.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, u, js)
}
_, err = c.Users.Get(reply.CreatedBy)
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 {
return c.InternalErrorJSQ(err, w, r, js)
}
err = reply.Unlike(user.ID)
err = reply.Unlike(u.ID)
if err != nil {
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)
}
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 {
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)
}
var poll c.Poll
var poll *c.Poll
if topic.Poll != 0 {
pPoll, err := c.Polls.Get(topic.Poll)
if err != nil {
log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID))
return c.InternalError(err, w, r)
}
poll = pPoll.Copy()
poll = new(c.Poll)
*poll = pPoll.Copy()
}
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-") {
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 {
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 {
return c.InternalError(err, w, r)
}
header.OGDesc = ogdesc
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
tmpl := forum.Tmpl
if r.FormValue("i") == "1" {
if tpage.Poll.ID != 0 {
if tpage.Poll != nil {
header.AddXRes("chartist/chartist.min.css", "chartist/chartist.min.js")
}
if tmpl == "" {
@ -179,7 +179,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H
}
}
} else {
if tpage.Poll.ID != 0 {
if tpage.Poll != nil {
header.AddSheet("chartist/chartist.min.css")
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="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>

View File

@ -4,10 +4,10 @@
<div id="panel_users" class="colstack_item rowlist bgavatars">
{{range .ItemList}}
<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">
</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">
<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}}

View File

@ -26,32 +26,7 @@
</div>
<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>
<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}}
{{if .Poll}}{{template "topic_alt_poll.html" . }}{{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"}}">
{{template "topic_alt_userinfo.html" .Topic }}
<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.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}}
<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}}
<span class="attach_item_path" aid="{{.ID}}" fullPath="//{{$.Header.Site.URL}}/attachs/{{.Path}}">{{.Path}}</span>
{{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>
<button class="attach_item_select">{{lang "topic.select_button_text"}}</button>
<button class="attach_item_copy">{{lang "topic.copy_button_text"}}</button>
</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}}
<div class="content_container">
{{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}}
<div class="editable_block user_content"itemprop="text">{{.ContentHtml}}</div>
{{if $.CurrentUser.Loggedin}}
@ -13,15 +13,15 @@
{{range .Attachments}}
<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}}
<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_copy">{{lang "topic.copy_button_text"}}</button>
</div>
{{end}}
<div class="attach_item attach_item_buttons">
{{if $.CurrentUser.Perms.UploadFiles}}
<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}}
<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}}
<button class="attach_item_delete formbutton">{{lang "topic.delete_button_text"}}</button>
</div>
</div>
@ -31,23 +31,23 @@
<div class="action_button_left">
{{if $.CurrentUser.Loggedin}}
{{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}}
<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}}
{{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}}
{{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 $.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}}
{{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}}
<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>
{{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}}
<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>
{{end}}
</div>
<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 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}}
<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>
{{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>
{{end}}

View File

@ -17,17 +17,17 @@
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
{{if .CurrentUser.Perms.EditTopic}}
<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>
{{end}}
{{end}}
</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"}}">
<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}}
<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}}
<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>
<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" name="tid" value='{{.Topic.ID}}' 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="formitem">
<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="formitem">
<div class="pollinput" data-pollinput=0>
<input type="checkbox" disabled />
<input type="checkbox" disabled>
<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>
@ -96,7 +96,7 @@
<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>
{{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>
<div id="upload_file_dock"></div>{{end}}
</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="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}}">
<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>
<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>
<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 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="auto_hide poll_no_results">{{lang "topic.poll_no_results"}}</div>
</div>