Almost finished live topic lists, you can find them at /topics/. You can disable them via config.json

The topic list cache can handle more groups now, but don't go too crazy with groups (e.g. thousands of them).

Make the suspicious request logs more descriptive.
Added the phrases API endpoint.
Split the template phrases up by prefix, more work on this coming up.
Removed #dash_saved and part of #dash_username.
Removed some temporary artifacts from trying to implement FA5 in Nox.
Removed some commented CSS.
Fixed template artifact deletion on Windows.
Tweaked HTTPSRedirect to make it more compact.
Fixed NullUserCache not complying with the expectations for BulkGet.
Swapped out a few RunVhook calls for more appropriate RunVhookNoreturn calls.
Removed a few redundant IsAdmin checks when IsMod would suffice.
Commented out a few pushers.
Desktop notification permission requests are no longer served to guests.
Split topics.html into topics.html and topics_topic.html
RunThemeTemplate should now fallback to interpreted templates properly when the transpiled variants aren't avaialb.e
Changed TopicsRow.CreatedAt from a string to a time.Time
Added SkipTmplPtrMap to CTemplateConfig.
Added SetBuildTags to CTemplateSet.
A bit more data is dumped when something goes wrong while transpiling templates now.
topics_topic, topic_posts, and topic_alt_posts are now transpiled for the client, although not all of them are ready to be served to the client yet.
Client rendered templates now support phrases.
Client rendered templates now support loops.
Fixed loadAlerts in global.js
Refactored some of the template initialisation code to make it less repetitive.
Split topic.html into topic.html and topic_posts.html
Split topic_alt.html into topic_alt.html and topic_alt_posts.html
Added comments for PollCache.
Fixed a data race in the MemoryPollCache.
The writer is now closed properly in WsHubImpl.broadcastMessage.
Fixed a potential deadlock in WsHubImpl.broadcastMessage.
Removed some old commented code in websockets.go

Added the DisableLiveTopicList config setting.
This commit is contained in:
Azareal 2018-06-24 23:49:29 +10:00
parent 163d417831
commit 7be011a30d
54 changed files with 1504 additions and 996 deletions

View File

@ -2,7 +2,9 @@
rem TODO: Make these deletes a little less noisy rem TODO: Make these deletes a little less noisy
del "template_*.go" del "template_*.go"
del "gen_*.go" del "gen_*.go"
del "tmpl_client/template_*.go" cd tmpl_client
del "template_*.go"
cd ..
del "gosora.exe" del "gosora.exe"
echo Generating the dynamic code echo Generating the dynamic code

View File

@ -371,6 +371,7 @@ func RunHookNoreturn(name string, data interface{}) {
} }
} }
// TODO: Use RunHook semantics to allow multiple lined up plugins / modules their turn?
func RunVhook(name string, data ...interface{}) interface{} { func RunVhook(name string, data ...interface{}) interface{} {
hook := Vhooks[name] hook := Vhooks[name]
if hook != nil { if hook != nil {

View File

@ -3,7 +3,9 @@ package common
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"mime" "mime"
"strconv"
"strings" "strings"
"sync" "sync"
//"errors" //"errors"
@ -40,6 +42,9 @@ func (list SFileList) JSTmplInit() error {
DebugLog("Initialising the client side templates") DebugLog("Initialising the client side templates")
var fragMap = make(map[string][][]byte) var fragMap = make(map[string][][]byte)
fragMap["alert"] = tmpl.GetFrag("alert") fragMap["alert"] = tmpl.GetFrag("alert")
fragMap["topics_topic"] = tmpl.GetFrag("topics_topic")
fragMap["topic_posts"] = tmpl.GetFrag("topic_posts")
fragMap["topic_alt_posts"] = tmpl.GetFrag("topic_alt_posts")
DebugLog("fragMap: ", fragMap) DebugLog("fragMap: ", fragMap)
return filepath.Walk("./tmpl_client", func(path string, f os.FileInfo, err error) error { return filepath.Walk("./tmpl_client", func(path string, f os.FileInfo, err error) error {
if f.IsDir() { if f.IsDir() {
@ -56,18 +61,27 @@ func (list SFileList) JSTmplInit() error {
return err return err
} }
path = strings.TrimPrefix(path, "tmpl_client/")
tmplName := strings.TrimSuffix(path, ".go")
shortName := strings.TrimPrefix(tmplName, "template_")
var replace = func(data []byte, replaceThis string, withThis string) []byte { var replace = func(data []byte, replaceThis string, withThis string) []byte {
return bytes.Replace(data, []byte(replaceThis), []byte(withThis), -1) return bytes.Replace(data, []byte(replaceThis), []byte(withThis), -1)
} }
startIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("func Template")) startIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("func init() {"))
if !hasFunc {
return errors.New("no init function found")
}
data = data[startIndex-len([]byte("func init() {")):]
data = replace(data, "func ", "function ")
data = replace(data, "function init() {", "tmplInits[\""+tmplName+"\"] = ")
data = replace(data, " error {\n", " {\nlet out = \"\"\n")
funcIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("function Template_"))
if !hasFunc { if !hasFunc {
return errors.New("no template function found") return errors.New("no template function found")
} }
data = data[startIndex-len([]byte("func Template")):] spaceIndex, hasSpace := skipUntilIfExists(data, funcIndex, ' ')
data = replace(data, "func ", "function ")
data = replace(data, " error {\n", " {\nlet out = \"\"\n")
spaceIndex, hasSpace := skipUntilIfExists(data, 10, ' ')
if !hasSpace { if !hasSpace {
return errors.New("no spaces found after the template function name") return errors.New("no spaces found after the template function name")
} }
@ -75,13 +89,16 @@ func (list SFileList) JSTmplInit() error {
if !hasBrace { if !hasBrace {
return errors.New("no right brace found after the template function name") return errors.New("no right brace found after the template function name")
} }
//fmt.Println("spaceIndex: ", spaceIndex) fmt.Println("spaceIndex: ", spaceIndex)
//fmt.Println("endBrace: ", endBrace) fmt.Println("endBrace: ", endBrace)
//fmt.Println("string(data[spaceIndex:endBrace]): ", string(data[spaceIndex:endBrace])) fmt.Println("string(data[spaceIndex:endBrace]): ", string(data[spaceIndex:endBrace]))
preLen := len(data) preLen := len(data)
data = replace(data, string(data[spaceIndex:endBrace]), "") data = replace(data, string(data[spaceIndex:endBrace]), "")
data = replace(data, "))\n", "\n") data = replace(data, "))\n", "\n")
endBrace -= preLen - len(data) // Offset it as we've deleted portions endBrace -= preLen - len(data) // Offset it as we've deleted portions
fmt.Println("new endBrace: ", endBrace)
fmt.Println("data: ", string(data))
/*var showPos = func(data []byte, index int) (out string) { /*var showPos = func(data []byte, index int) (out string) {
out = "[" out = "["
@ -99,6 +116,9 @@ func (list SFileList) JSTmplInit() error {
var each = func(phrase string, handle func(index int)) { var each = func(phrase string, handle func(index int)) {
//fmt.Println("find each '" + phrase + "'") //fmt.Println("find each '" + phrase + "'")
var index = endBrace var index = endBrace
if index < 0 {
panic("index under zero: " + strconv.Itoa(index))
}
var foundIt bool var foundIt bool
for { for {
//fmt.Println("in index: ", index) //fmt.Println("in index: ", index)
@ -145,9 +165,24 @@ func (list SFileList) JSTmplInit() error {
data[braceAt-1] = ')' // Drop a brace here to satisfy JS data[braceAt-1] = ')' // Drop a brace here to satisfy JS
} }
}) })
each("for _, item := range ", func(index int) {
//fmt.Println("for index: ", index)
braceAt, hasBrace := skipUntilIfExists(data, index, '{')
if hasBrace {
if data[braceAt-1] != ' ' {
panic("couldn't find space before brace, found ' " + string(data[braceAt-1]) + "' instead")
}
data[braceAt-1] = ')' // Drop a brace here to satisfy JS
}
})
data = replace(data, "for _, item := range ", "for(item of ")
data = replace(data, "w.Write([]byte(", "out += ") data = replace(data, "w.Write([]byte(", "out += ")
data = replace(data, "w.Write(", "out += ") data = replace(data, "w.Write(", "out += ")
data = replace(data, "strconv.Itoa(", "") data = replace(data, "strconv.Itoa(", "")
data = replace(data, "common.", "")
data = replace(data, shortName+"_tmpl_phrase_id = RegisterTmplPhraseNames([]string{", "[")
data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];")
//data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];\nconsole.log('tmplName:','"+tmplName+"')\nconsole.log('phrases:', phrases);")
data = replace(data, "if ", "if(") data = replace(data, "if ", "if(")
data = replace(data, "return nil", "return out") data = replace(data, "return nil", "return out")
data = replace(data, " )", ")") data = replace(data, " )", ")")
@ -155,19 +190,25 @@ func (list SFileList) JSTmplInit() error {
data = replace(data, "\n", ";\n") data = replace(data, "\n", ";\n")
data = replace(data, "{;", "{") data = replace(data, "{;", "{")
data = replace(data, "};", "}") data = replace(data, "};", "}")
data = replace(data, "[;", "[")
data = replace(data, ";;", ";") data = replace(data, ";;", ";")
data = replace(data, ",;", ",")
data = replace(data, "=;", "=")
data = replace(data, `,
});
}`, "\n\t];")
data = replace(data, `=
}`, "= []")
path = strings.TrimPrefix(path, "tmpl_client/") fragset, ok := fragMap[shortName]
tmplName := strings.TrimSuffix(path, ".go")
fragset, ok := fragMap[strings.TrimPrefix(tmplName, "template_")]
if !ok { if !ok {
DebugLog("tmplName: ", tmplName) DebugLog("tmplName: ", tmplName)
return errors.New("couldn't find template in fragmap") return errors.New("couldn't find template in fragmap")
} }
var sfrags = []byte("let alert_frags = [];\n") var sfrags = []byte("let " + shortName + "_frags = [];\n")
for _, frags := range fragset { for _, frags := range fragset {
sfrags = append(sfrags, []byte("alert_frags.push(`"+string(frags)+"`);\n")...) sfrags = append(sfrags, []byte(shortName+"_frags.push(`"+string(frags)+"`);\n")...)
} }
data = append(sfrags, data...) data = append(sfrags, data...)
data = replace(data, "\n;", "\n") data = replace(data, "\n;", "\n")

View File

@ -174,20 +174,30 @@ func PermmapToQuery(permmap map[string]*ForumPerms, fid int) error {
if err != nil { if err != nil {
return err return err
} }
return FPStore.Reload(fid) err = FPStore.Reload(fid)
if err != nil {
return err
}
return TopicList.RebuildPermTree()
} }
// TODO: FPStore.Reload?
func ReplaceForumPermsForGroup(gid int, presetSet map[int]string, permSets map[int]*ForumPerms) error { func ReplaceForumPermsForGroup(gid int, presetSet map[int]string, permSets map[int]*ForumPerms) error {
tx, err := qgen.Builder.Begin() tx, err := qgen.Builder.Begin()
if err != nil { if err != nil {
return err return err
} }
defer tx.Rollback() defer tx.Rollback()
err = ReplaceForumPermsForGroupTx(tx, gid, presetSet, permSets) err = ReplaceForumPermsForGroupTx(tx, gid, presetSet, permSets)
if err != nil { if err != nil {
return err return err
} }
return tx.Commit() err = tx.Commit()
if err != nil {
return err
}
return TopicList.RebuildPermTree()
} }
func ReplaceForumPermsForGroupTx(tx *sql.Tx, gid int, presetSets map[int]string, permSets map[int]*ForumPerms) error { func ReplaceForumPermsForGroupTx(tx *sql.Tx, gid int, presetSets map[int]string, permSets map[int]*ForumPerms) error {

View File

@ -284,6 +284,10 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
if err != nil { if err != nil {
return gid, err return gid, err
} }
err = TopicList.RebuildPermTree()
if err != nil {
return gid, err
}
return gid, nil return gid, nil
} }

View File

@ -204,7 +204,8 @@ func skipAllUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, h
expectIndex := 0 expectIndex := 0
//fmt.Printf("tmplData: %+v\n", string(tmplData)) //fmt.Printf("tmplData: %+v\n", string(tmplData))
for ; j < len(tmplData) && expectIndex < len(expects); j++ { for ; j < len(tmplData) && expectIndex < len(expects); j++ {
//fmt.Println("tmplData[j]: ", string(tmplData[j]) + " ") //fmt.Println("j: ", j)
//fmt.Println("tmplData[j]: ", string(tmplData[j])+" ")
if tmplData[j] == expects[expectIndex] { if tmplData[j] == expects[expectIndex] {
//fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex) //fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex)
expectIndex++ expectIndex++

View File

@ -13,8 +13,8 @@ func NewNullUserCache() *NullUserCache {
func (mus *NullUserCache) Get(id int) (*User, error) { func (mus *NullUserCache) Get(id int) (*User, error) {
return nil, ErrNoRows return nil, ErrNoRows
} }
func (mus *NullUserCache) BulkGet(_ []int) (list []*User) { func (mus *NullUserCache) BulkGet(ids []int) (list []*User) {
return nil return make([]*User, len(ids))
} }
func (mus *NullUserCache) GetUnsafe(id int) (*User, error) { func (mus *NullUserCache) GetUnsafe(id int) (*User, error) {
return nil, ErrNoRows return nil, ErrNoRows

View File

@ -13,7 +13,7 @@ type Header struct {
Title string Title string
NoticeList []string NoticeList []string
Scripts []string Scripts []string
//PreloadScripts []string //Preload []string
Stylesheets []string Stylesheets []string
Widgets PageWidgets Widgets PageWidgets
Site *site Site *site
@ -32,8 +32,8 @@ func (header *Header) AddScript(name string) {
header.Scripts = append(header.Scripts, name) header.Scripts = append(header.Scripts, name)
} }
/*func (header *Header) PreloadScript(name string) { /*func (header *Header) Preload(name string) {
header.PreloadScripts = append(header.PreloadScripts, name) header.Preload = append(header.Preload, name)
}*/ }*/
func (header *Header) AddSheet(name string) { func (header *Header) AddSheet(name string) {

View File

@ -1,7 +1,7 @@
/* /*
* *
* Gosora Phrase System * Gosora Phrase System
* Copyright Azareal 2017 - 2018 * Copyright Azareal 2017 - 2019
* *
*/ */
package common package common
@ -13,6 +13,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
) )
@ -34,21 +35,22 @@ type LevelPhrases struct {
// ! For the sake of thread safety, you must never modify a *LanguagePack directly, but to create a copy of it and overwrite the entry in the sync.Map // ! For the sake of thread safety, you must never modify a *LanguagePack directly, but to create a copy of it and overwrite the entry in the sync.Map
type LanguagePack struct { type LanguagePack struct {
Name string Name string
Phrases map[string]string // Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent. Phrases map[string]string // Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent.
Levels LevelPhrases Levels LevelPhrases
GlobalPerms map[string]string GlobalPerms map[string]string
LocalPerms map[string]string LocalPerms map[string]string
SettingPhrases map[string]string SettingPhrases map[string]string
PermPresets map[string]string PermPresets map[string]string
Accounts map[string]string // TODO: Apply these phrases in the software proper Accounts map[string]string // TODO: Apply these phrases in the software proper
UserAgents map[string]string UserAgents map[string]string
OperatingSystems map[string]string OperatingSystems map[string]string
HumanLanguages map[string]string HumanLanguages map[string]string
Errors map[string]map[string]string // map[category]map[name]value Errors map[string]map[string]string // map[category]map[name]value
NoticePhrases map[string]string NoticePhrases map[string]string
PageTitles map[string]string PageTitles map[string]string
TmplPhrases map[string]string TmplPhrases map[string]string
TmplPhrasesPrefixes map[string]map[string]string // [prefix][name]phrase
TmplIndicesToPhrases [][][]byte // [tmplID][index]phrase TmplIndicesToPhrases [][][]byte // [tmplID][index]phrase
} }
@ -81,6 +83,17 @@ func InitPhrases() error {
return err return err
} }
// [prefix][name]phrase
langPack.TmplPhrasesPrefixes = make(map[string]map[string]string)
for name, phrase := range langPack.TmplPhrases {
prefix := strings.Split(name, ".")[0]
_, ok := langPack.TmplPhrasesPrefixes[prefix]
if !ok {
langPack.TmplPhrasesPrefixes[prefix] = make(map[string]string)
}
langPack.TmplPhrasesPrefixes[prefix][name] = phrase
}
langPack.TmplIndicesToPhrases = make([][][]byte, len(langTmplIndicesToNames)) langPack.TmplIndicesToPhrases = make([][][]byte, len(langTmplIndicesToNames))
for tmplID, phraseNames := range langTmplIndicesToNames { for tmplID, phraseNames := range langTmplIndicesToNames {
var phraseSet = make([][]byte, len(phraseNames)) var phraseSet = make([][]byte, len(phraseNames))
@ -233,6 +246,11 @@ func GetTmplPhrases() map[string]string {
return currentLangPack.Load().(*LanguagePack).TmplPhrases return currentLangPack.Load().(*LanguagePack).TmplPhrases
} }
func GetTmplPhrasesByPrefix(prefix string) (phrases map[string]string, ok bool) {
res, ok := currentLangPack.Load().(*LanguagePack).TmplPhrasesPrefixes[prefix]
return res, ok
}
func getPhrasePlaceholder(prefix string, suffix string) string { func getPhrasePlaceholder(prefix string, suffix string) string {
return "{lang." + prefix + "[" + suffix + "]}" return "{lang." + prefix + "[" + suffix + "]}"
} }

View File

@ -5,6 +5,7 @@ import (
"sync/atomic" "sync/atomic"
) )
// PollCache is an interface which spits out polls from a fast cache rather than the database, whether from memory or from an application like Redis. Polls may not be present in the cache but may be in the database
type PollCache interface { type PollCache interface {
Get(id int) (*Poll, error) Get(id int) (*Poll, error)
GetUnsafe(id int) (*Poll, error) GetUnsafe(id int) (*Poll, error)
@ -20,6 +21,7 @@ type PollCache interface {
GetCapacity() int GetCapacity() int
} }
// MemoryPollCache stores and pulls polls out of the current process' memory
type MemoryPollCache struct { type MemoryPollCache struct {
items map[int]*Poll items map[int]*Poll
length int64 length int64
@ -36,6 +38,7 @@ func NewMemoryPollCache(capacity int) *MemoryPollCache {
} }
} }
// Get fetches a poll by ID. Returns ErrNoRows if not present.
func (mus *MemoryPollCache) Get(id int) (*Poll, error) { func (mus *MemoryPollCache) Get(id int) (*Poll, error) {
mus.RLock() mus.RLock()
item, ok := mus.items[id] item, ok := mus.items[id]
@ -46,6 +49,7 @@ func (mus *MemoryPollCache) Get(id int) (*Poll, error) {
return item, ErrNoRows return item, ErrNoRows
} }
// BulkGet fetches multiple polls by their IDs. Indices without polls will be set to nil, so make sure you check for those, we might want to change this behaviour to make it less confusing.
func (mus *MemoryPollCache) BulkGet(ids []int) (list []*Poll) { func (mus *MemoryPollCache) BulkGet(ids []int) (list []*Poll) {
list = make([]*Poll, len(ids)) list = make([]*Poll, len(ids))
mus.RLock() mus.RLock()
@ -56,6 +60,7 @@ func (mus *MemoryPollCache) BulkGet(ids []int) (list []*Poll) {
return list return list
} }
// GetUnsafe fetches a poll by ID. Returns ErrNoRows if not present. THIS METHOD IS NOT THREAD-SAFE.
func (mus *MemoryPollCache) GetUnsafe(id int) (*Poll, error) { func (mus *MemoryPollCache) GetUnsafe(id int) (*Poll, error) {
item, ok := mus.items[id] item, ok := mus.items[id]
if ok { if ok {
@ -64,6 +69,7 @@ func (mus *MemoryPollCache) GetUnsafe(id int) (*Poll, error) {
return item, ErrNoRows return item, ErrNoRows
} }
// Set overwrites the value of a poll in the cache, whether it's present or not. May return a capacity overflow error.
func (mus *MemoryPollCache) Set(item *Poll) error { func (mus *MemoryPollCache) Set(item *Poll) error {
mus.Lock() mus.Lock()
user, ok := mus.items[item.ID] user, ok := mus.items[item.ID]
@ -81,17 +87,21 @@ func (mus *MemoryPollCache) Set(item *Poll) error {
return nil return nil
} }
// Add adds a poll to the cache, similar to Set, but it's only intended for new items. This method might be deprecated in the near future, use Set. May return a capacity overflow error.
// ? Is this redundant if we have Set? Are the efficiency wins worth this? Is this even used?
func (mus *MemoryPollCache) Add(item *Poll) error { func (mus *MemoryPollCache) Add(item *Poll) error {
mus.Lock()
if int(mus.length) >= mus.capacity { if int(mus.length) >= mus.capacity {
mus.Unlock()
return ErrStoreCapacityOverflow return ErrStoreCapacityOverflow
} }
mus.Lock()
mus.items[item.ID] = item mus.items[item.ID] = item
mus.length = int64(len(mus.items)) mus.length = int64(len(mus.items))
mus.Unlock() mus.Unlock()
return nil return nil
} }
// AddUnsafe is the unsafe version of Add. May return a capacity overflow error. THIS METHOD IS NOT THREAD-SAFE.
func (mus *MemoryPollCache) AddUnsafe(item *Poll) error { func (mus *MemoryPollCache) AddUnsafe(item *Poll) error {
if int(mus.length) >= mus.capacity { if int(mus.length) >= mus.capacity {
return ErrStoreCapacityOverflow return ErrStoreCapacityOverflow
@ -101,6 +111,7 @@ func (mus *MemoryPollCache) AddUnsafe(item *Poll) error {
return nil return nil
} }
// Remove removes a poll from the cache by ID, if they exist. Returns ErrNoRows if no items exist.
func (mus *MemoryPollCache) Remove(id int) error { func (mus *MemoryPollCache) Remove(id int) error {
mus.Lock() mus.Lock()
_, ok := mus.items[id] _, ok := mus.items[id]
@ -114,6 +125,7 @@ func (mus *MemoryPollCache) Remove(id int) error {
return nil return nil
} }
// RemoveUnsafe is the unsafe version of Remove. THIS METHOD IS NOT THREAD-SAFE.
func (mus *MemoryPollCache) RemoveUnsafe(id int) error { func (mus *MemoryPollCache) RemoveUnsafe(id int) error {
_, ok := mus.items[id] _, ok := mus.items[id]
if !ok { if !ok {
@ -124,6 +136,7 @@ func (mus *MemoryPollCache) RemoveUnsafe(id int) error {
return nil return nil
} }
// Flush removes all the polls from the cache, useful for tests.
func (mus *MemoryPollCache) Flush() { func (mus *MemoryPollCache) Flush() {
mus.Lock() mus.Lock()
mus.items = make(map[int]*Poll) mus.items = make(map[int]*Poll)
@ -132,19 +145,23 @@ func (mus *MemoryPollCache) Flush() {
} }
// ! Is this concurrent? // ! Is this concurrent?
// Length returns the number of users in the memory cache // Length returns the number of polls in the memory cache
func (mus *MemoryPollCache) Length() int { func (mus *MemoryPollCache) Length() int {
return int(mus.length) return int(mus.length)
} }
// SetCapacity sets the maximum number of polls which this cache can hold
func (mus *MemoryPollCache) SetCapacity(capacity int) { func (mus *MemoryPollCache) SetCapacity(capacity int) {
// Ints are moved in a single instruction, so this should be thread-safe
mus.capacity = capacity mus.capacity = capacity
} }
// GetCapacity returns the maximum number of polls this cache can hold
func (mus *MemoryPollCache) GetCapacity() int { func (mus *MemoryPollCache) GetCapacity() int {
return mus.capacity return mus.capacity
} }
// NullPollCache is a poll cache to be used when you don't want a cache and just want queries to passthrough to the database
type NullPollCache struct { type NullPollCache struct {
} }
@ -153,50 +170,38 @@ func NewNullPollCache() *NullPollCache {
return &NullPollCache{} return &NullPollCache{}
} }
// nolint
func (mus *NullPollCache) Get(id int) (*Poll, error) { func (mus *NullPollCache) Get(id int) (*Poll, error) {
return nil, ErrNoRows return nil, ErrNoRows
} }
func (mus *NullPollCache) BulkGet(ids []int) (list []*Poll) { func (mus *NullPollCache) BulkGet(ids []int) (list []*Poll) {
return list return make([]*Poll, len(ids))
} }
func (mus *NullPollCache) GetUnsafe(id int) (*Poll, error) { func (mus *NullPollCache) GetUnsafe(id int) (*Poll, error) {
return nil, ErrNoRows return nil, ErrNoRows
} }
func (mus *NullPollCache) Set(_ *Poll) error { func (mus *NullPollCache) Set(_ *Poll) error {
return nil return nil
} }
func (mus *NullPollCache) Add(_ *Poll) error {
func (mus *NullPollCache) Add(item *Poll) error {
_ = item
return nil return nil
} }
func (mus *NullPollCache) AddUnsafe(_ *Poll) error {
func (mus *NullPollCache) AddUnsafe(item *Poll) error {
_ = item
return nil return nil
} }
func (mus *NullPollCache) Remove(id int) error { func (mus *NullPollCache) Remove(id int) error {
return nil return nil
} }
func (mus *NullPollCache) RemoveUnsafe(id int) error { func (mus *NullPollCache) RemoveUnsafe(id int) error {
return nil return nil
} }
func (mus *NullPollCache) Flush() { func (mus *NullPollCache) Flush() {
} }
func (mus *NullPollCache) Length() int { func (mus *NullPollCache) Length() int {
return 0 return 0
} }
func (mus *NullPollCache) SetCapacity(_ int) { func (mus *NullPollCache) SetCapacity(_ int) {
} }
func (mus *NullPollCache) GetCapacity() int { func (mus *NullPollCache) GetCapacity() int {
return 0 return 0
} }

View File

@ -149,7 +149,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
stats.Reports = 0 // TODO: Do the report count. Only show open threads? stats.Reports = 0 // TODO: Do the report count. Only show open threads?
// TODO: Remove this as it might be counter-productive // TODO: Remove this as it might be counter-productive
pusher, ok := w.(http.Pusher) /*pusher, ok := w.(http.Pusher)
if ok { if ok {
pusher.Push("/static/"+theme.Name+"/main.css", nil) pusher.Push("/static/"+theme.Name+"/main.css", nil)
pusher.Push("/static/"+theme.Name+"/panel.css", nil) pusher.Push("/static/"+theme.Name+"/panel.css", nil)
@ -163,7 +163,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
pusher.Push("/static/"+script, nil) pusher.Push("/static/"+script, nil)
} }
// TODO: Push avatars? // TODO: Push avatars?
} }*/
return header, stats, nil return header, stats, nil
} }
@ -230,7 +230,7 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Head
} }
} }
pusher, ok := w.(http.Pusher) /*pusher, ok := w.(http.Pusher)
if ok { if ok {
pusher.Push("/static/"+theme.Name+"/main.css", nil) pusher.Push("/static/"+theme.Name+"/main.css", nil)
pusher.Push("/static/global.js", nil) pusher.Push("/static/global.js", nil)
@ -243,7 +243,7 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Head
pusher.Push("/static/"+script, nil) pusher.Push("/static/"+script, nil)
} }
// TODO: Push avatars? // TODO: Push avatars?
} }*/
return header, nil return header, nil
} }

View File

@ -81,6 +81,8 @@ type config struct {
BuildSlugs bool // TODO: Make this a setting? BuildSlugs bool // TODO: Make this a setting?
ServerCount int ServerCount int
DisableLiveTopicList bool
Noavatar string // ? - Move this into the settings table? Noavatar string // ? - Move this into the settings table?
ItemsPerPage int // ? - Move this into the settings table? ItemsPerPage int // ? - Move this into the settings table?
MaxTopicTitleLength int MaxTopicTitleLength int

View File

@ -122,25 +122,15 @@ var Template_ip_search_handle = func(pi IPSearchPage, w io.Writer) error {
return Templates.ExecuteTemplate(w, mapping+".html", pi) return Templates.ExecuteTemplate(w, mapping+".html", pi)
} }
// ? - Add template hooks? func tmplInitUsers() (User, User, User) {
func CompileTemplates() error {
var config tmpl.CTemplateConfig
config.Minify = Config.MinifyTemplates
config.SuperDebug = Dev.TemplateDebug
c := tmpl.NewCTemplateSet()
c.SetConfig(config)
c.SetBaseImportMap(map[string]string{
"io": "io",
"./common": "./common",
})
// Schemas to train the template compiler on what to expect
// TODO: Add support for interface{}s
user := User{62, BuildProfileURL("fake-user", 62), "Fake User", "compiler@localhost", 0, false, false, false, false, false, false, GuestPerms, make(map[string]bool), "", false, BuildAvatar(62, ""), "", "", "", "", 0, 0, 0, "0.0.0.0.0", 0} user := User{62, BuildProfileURL("fake-user", 62), "Fake User", "compiler@localhost", 0, false, false, false, false, false, false, GuestPerms, make(map[string]bool), "", false, BuildAvatar(62, ""), "", "", "", "", 0, 0, 0, "0.0.0.0.0", 0}
// TODO: Do a more accurate level calculation for this? // TODO: Do a more accurate level calculation for this?
user2 := User{1, BuildProfileURL("admin-alice", 1), "Admin Alice", "alice@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(1, ""), "", "", "", "", 58, 1000, 0, "127.0.0.1", 0} user2 := User{1, BuildProfileURL("admin-alice", 1), "Admin Alice", "alice@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(1, ""), "", "", "", "", 58, 1000, 0, "127.0.0.1", 0}
user3 := User{2, BuildProfileURL("admin-fred", 62), "Admin Fred", "fred@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(2, ""), "", "", "", "", 42, 900, 0, "::1", 0} user3 := User{2, BuildProfileURL("admin-fred", 62), "Admin Fred", "fred@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(2, ""), "", "", "", "", 42, 900, 0, "::1", 0}
return user, user2, user3
}
func tmplInitHeaders(user User, user2 User, user3 User) (*Header, *Header, *Header) {
header := &Header{ header := &Header{
Site: Site, Site: Site,
Settings: SettingBox.Load().(SettingMap), Settings: SettingBox.Load().(SettingMap),
@ -163,9 +153,32 @@ func CompileTemplates() error {
*header3 = *header *header3 = *header
header3.CurrentUser = user3 header3.CurrentUser = user3
return header, header2, header3
}
// ? - Add template hooks?
func CompileTemplates() error {
var config tmpl.CTemplateConfig
config.Minify = Config.MinifyTemplates
config.Debug = Dev.DebugMode
config.SuperDebug = Dev.TemplateDebug
c := tmpl.NewCTemplateSet()
c.SetConfig(config)
c.SetBaseImportMap(map[string]string{
"io": "io",
"./common": "./common",
})
c.SetBuildTags("!no_templategen")
// Schemas to train the template compiler on what to expect
// TODO: Add support for interface{}s
user, user2, user3 := tmplInitUsers()
header, header2, _ := tmplInitHeaders(user, user2, user3)
now := time.Now()
log.Print("Compiling the templates") log.Print("Compiling the templates")
var now = time.Now()
poll := Poll{ID: 1, Type: 0, Options: map[int]string{0: "Nothing", 1: "Something"}, Results: map[int]int{0: 5, 1: 2}, QuickOptions: []PollOption{ poll := Poll{ID: 1, Type: 0, Options: map[int]string{0: "Nothing", 1: "Something"}, Results: map[int]int{0: 5, 1: 2}, QuickOptions: []PollOption{
PollOption{0, "Nothing"}, PollOption{0, "Nothing"},
PollOption{1, "Something"}, PollOption{1, "Something"},
@ -213,7 +226,7 @@ func CompileTemplates() error {
} }
var topicsList []*TopicsRow var topicsList []*TopicsRow
topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, "Date", time.Now(), "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"})
header2.Title = "Topic List" header2.Title = "Topic List"
topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, Paginator{[]int{1}, 1, 1}} topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, Paginator{[]int{1}, 1, 1}}
topicListTmpl, err := c.Compile("topics.html", "templates/", "common.TopicListPage", topicListPage, varList) topicListTmpl, err := c.Compile("topics.html", "templates/", "common.TopicListPage", topicListPage, varList)
@ -310,9 +323,11 @@ func CompileJSTemplates() error {
log.Print("Compiling the JS templates") log.Print("Compiling the JS templates")
var config tmpl.CTemplateConfig var config tmpl.CTemplateConfig
config.Minify = Config.MinifyTemplates config.Minify = Config.MinifyTemplates
config.Debug = Dev.DebugMode
config.SuperDebug = Dev.TemplateDebug config.SuperDebug = Dev.TemplateDebug
config.SkipHandles = true config.SkipHandles = true
config.SkipInitBlock = true config.SkipTmplPtrMap = true
config.SkipInitBlock = false
config.PackageName = "tmpl" config.PackageName = "tmpl"
c := tmpl.NewCTemplateSet() c := tmpl.NewCTemplateSet()
@ -321,6 +336,11 @@ func CompileJSTemplates() error {
"io": "io", "io": "io",
"../common/alerts": "../common/alerts", "../common/alerts": "../common/alerts",
}) })
c.SetBuildTags("!no_templategen")
user, user2, user3 := tmplInitUsers()
header, _, _ := tmplInitHeaders(user, user2, user3)
now := time.Now()
var varList = make(map[string]tmpl.VarItem) var varList = make(map[string]tmpl.VarItem)
// TODO: Check what sort of path is sent exactly and use it here // TODO: Check what sort of path is sent exactly and use it here
@ -330,6 +350,39 @@ func CompileJSTemplates() error {
return err return err
} }
c.SetBaseImportMap(map[string]string{
"io": "io",
"../common": "../common",
})
// TODO: Fix the import loop so we don't have to use this hack anymore
c.SetBuildTags("!no_templategen,tmplgentopic")
var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}
topicListItemTmpl, err := c.Compile("topics_topic.html", "templates/", "*common.TopicsRow", topicsRow, varList)
if err != nil {
return err
}
poll := Poll{ID: 1, Type: 0, Options: map[int]string{0: "Nothing", 1: "Something"}, Results: map[int]int{0: 5, 1: 2}, QuickOptions: []PollOption{
PollOption{0, "Nothing"},
PollOption{1, "Something"},
}, VoteCount: 7}
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, RelativeTime(now), now, RelativeTime(now), 0, "", "127.0.0.1", 0, 1, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, "", 0, "", "", "", "", "", 58, false}
var replyList []ReplyUser
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
varList = make(map[string]tmpl.VarItem)
header.Title = "Topic Name"
tpage := TopicPage{header, replyList, topic, poll, 1, 1}
topicIDTmpl, err := c.Compile("topic_posts.html", "templates/", "common.TopicPage", tpage, varList)
if err != nil {
return err
}
topicIDAltTmpl, err := c.Compile("topic_alt_posts.html", "templates/", "common.TopicPage", tpage, varList)
if err != nil {
return err
}
var dirPrefix = "./tmpl_client/" var dirPrefix = "./tmpl_client/"
var wg sync.WaitGroup var wg sync.WaitGroup
var writeTemplate = func(name string, content string) { var writeTemplate = func(name string, content string) {
@ -348,6 +401,9 @@ func CompileJSTemplates() error {
}() }()
} }
writeTemplate("alert", alertTmpl) writeTemplate("alert", alertTmpl)
writeTemplate("topics_topic", topicListItemTmpl)
writeTemplate("topic_posts", topicIDTmpl)
writeTemplate("topic_alt_posts", topicIDAltTmpl)
writeTemplateList(c, &wg, dirPrefix) writeTemplateList(c, &wg, dirPrefix)
return nil return nil
} }

View File

@ -28,12 +28,13 @@ type VarItemReflect struct {
} }
type CTemplateConfig struct { type CTemplateConfig struct {
Minify bool Minify bool
Debug bool Debug bool
SuperDebug bool SuperDebug bool
SkipHandles bool SkipHandles bool
SkipInitBlock bool SkipTmplPtrMap bool
PackageName string SkipInitBlock bool
PackageName string
} }
// nolint // nolint
@ -58,6 +59,7 @@ type CTemplateSet struct {
//tempVars map[string]string //tempVars map[string]string
config CTemplateConfig config CTemplateConfig
baseImportMap map[string]string baseImportMap map[string]string
buildTags string
expectsInt interface{} expectsInt interface{}
} }
@ -103,6 +105,10 @@ func (c *CTemplateSet) SetBaseImportMap(importMap map[string]string) {
c.baseImportMap = importMap c.baseImportMap = importMap
} }
func (c *CTemplateSet) SetBuildTags(tags string) {
c.buildTags = tags
}
func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) { func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
if c.config.Debug { if c.config.Debug {
fmt.Println("Compiling template '" + name + "'") fmt.Println("Compiling template '" + name + "'")
@ -179,7 +185,12 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
varString += "var " + varItem.Name + " " + varItem.Type + " = " + varItem.Destination + "\n" varString += "var " + varItem.Name + " " + varItem.Type + " = " + varItem.Destination + "\n"
} }
fout := "// +build !no_templategen\n\n// Code generated by Gosora. More below:\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n" var fout string
if c.buildTags != "" {
fout += "// +build " + c.buildTags + "\n\n"
}
fout += "// Code generated by Gosora. More below:\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n"
fout += "package " + c.config.PackageName + "\n" + importList + "\n" fout += "package " + c.config.PackageName + "\n" + importList + "\n"
if !c.config.SkipInitBlock { if !c.config.SkipInitBlock {
@ -194,7 +205,9 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
fout += "\tcommon.Ctemplates = append(common.Ctemplates,\"" + fname + "\")\n\tcommon.TmplPtrMap[\"" + fname + "\"] = &common.Template_" + fname + "_handle\n" fout += "\tcommon.Ctemplates = append(common.Ctemplates,\"" + fname + "\")\n\tcommon.TmplPtrMap[\"" + fname + "\"] = &common.Template_" + fname + "_handle\n"
} }
fout += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n" if !c.config.SkipTmplPtrMap {
fout += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n"
}
if len(c.langIndexToName) > 0 { if len(c.langIndexToName) > 0 {
fout += "\t" + fname + "_tmpl_phrase_id = common.RegisterTmplPhraseNames([]string{\n" fout += "\t" + fname + "_tmpl_phrase_id = common.RegisterTmplPhraseNames([]string{\n"
for _, name := range c.langIndexToName { for _, name := range c.langIndexToName {
@ -805,6 +818,7 @@ func (c *CTemplateSet) compileIfVarsub(varname string, varholder string, templat
out += ".(" + cur.Type().Name() + ")" out += ".(" + cur.Type().Name() + ")"
} }
if !cur.IsValid() { if !cur.IsValid() {
fmt.Println("cur: ", cur)
panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?") panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?")
} }
c.detail("Data Kind:", cur.Kind()) c.detail("Data Kind:", cur.Kind())

View File

@ -342,7 +342,7 @@ func RunThemeTemplate(theme string, template string, pi interface{}, w io.Writer
return tmplO(pi.(ErrorPage), w) return tmplO(pi.(ErrorPage), w)
case func(Page, io.Writer) error: case func(Page, io.Writer) error:
return tmplO(pi.(Page), w) return tmplO(pi.(Page), w)
case string: case nil, string:
mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[template] mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[template]
if !ok { if !ok {
mapping = template mapping = template
@ -370,14 +370,21 @@ func RunThemeTemplate(theme string, template string, pi interface{}, w io.Writer
// GetThemeTemplate attempts to get the template for a specific theme, otherwise it falls back on the default template pointer, which if absent will fallback onto the template interpreter // GetThemeTemplate attempts to get the template for a specific theme, otherwise it falls back on the default template pointer, which if absent will fallback onto the template interpreter
func GetThemeTemplate(theme string, template string) interface{} { func GetThemeTemplate(theme string, template string) interface{} {
// TODO: Figure out why we're getting a nil pointer here when transpiled templates are disabled, I would have assumed that we would just fall back to !ok on this
// Might have something to do with it being the theme's TmplPtr map, investigate.
tmpl, ok := Themes[theme].TmplPtr[template] tmpl, ok := Themes[theme].TmplPtr[template]
if ok { if ok {
//fmt.Println("tmpl: ", tmpl)
//fmt.Println("exiting at Themes[theme].TmplPtr[template]")
return tmpl return tmpl
} }
tmpl, ok = TmplPtrMap[template] tmpl, ok = TmplPtrMap[template]
if ok { if ok {
//fmt.Println("exiting at TmplPtrMap[template]")
return tmpl return tmpl
} }
//fmt.Println("just passing back the template name")
return template return template
} }

View File

@ -81,14 +81,15 @@ type TopicUser struct {
} }
type TopicsRow struct { type TopicsRow struct {
ID int ID int
Link string Link string
Title string Title string
Content string Content string
CreatedBy int CreatedBy int
IsClosed bool IsClosed bool
Sticky bool Sticky bool
CreatedAt string CreatedAt time.Time
//RelativeCreatedAt string
LastReplyAt time.Time LastReplyAt time.Time
RelativeLastReplyAt string RelativeLastReplyAt string
LastReplyBy int LastReplyBy int
@ -109,6 +110,31 @@ type TopicsRow struct {
ForumLink string ForumLink string
} }
type WsTopicsRow struct {
ID int
Link string
Title string
CreatedBy int
IsClosed bool
Sticky bool
CreatedAt time.Time
LastReplyAt time.Time
RelativeLastReplyAt string
LastReplyBy int
ParentID int
PostCount int
LikeCount int
ClassName string
Creator *WsJSONUser
LastUser *WsJSONUser
ForumName string
ForumLink string
}
func (row *TopicsRow) WebSockets() *WsTopicsRow {
return &WsTopicsRow{row.ID, row.Link, row.Title, row.CreatedBy, row.IsClosed, row.Sticky, row.CreatedAt, row.LastReplyAt, row.RelativeLastReplyAt, row.LastReplyBy, row.ParentID, row.PostCount, row.LikeCount, row.ClassName, row.Creator.WebSockets(), row.LastUser.WebSockets(), row.ForumName, row.ForumLink}
}
type TopicStmts struct { type TopicStmts struct {
addRepliesToTopic *sql.Stmt addRepliesToTopic *sql.Stmt
lock *sql.Stmt lock *sql.Stmt
@ -302,6 +328,7 @@ func (topic *Topic) Copy() Topic {
return *topic return *topic
} }
// TODO: Load LastReplyAt?
func TopicByReplyID(rid int) (*Topic, error) { func TopicByReplyID(rid int) (*Topic, error) {
topic := Topic{ID: 0} topic := Topic{ID: 0}
err := topicStmts.getByReplyID.QueryRow(rid).Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) err := topicStmts.getByReplyID.QueryRow(rid).Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
@ -310,6 +337,7 @@ func TopicByReplyID(rid int) (*Topic, error) {
} }
// TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser // TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser
// TODO: Load LastReplyAt everywhere in here?
func GetTopicUser(tid int) (TopicUser, error) { func GetTopicUser(tid int) (TopicUser, error) {
tcache := Topics.GetCache() tcache := Topics.GetCache()
ucache := Users.GetCache() ucache := Users.GetCache()

View File

@ -3,6 +3,7 @@ package common
import ( import (
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"../query_gen/lib" "../query_gen/lib"
) )
@ -16,119 +17,82 @@ type TopicListHolder struct {
} }
type TopicListInt interface { type TopicListInt interface {
GetListByCanSee(canSee []int, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error)
GetListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) GetListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error)
GetList(page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) GetList(page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error)
RebuildPermTree() error
} }
type DefaultTopicList struct { type DefaultTopicList struct {
// TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group?
oddGroups map[int]*TopicListHolder oddGroups map[int]*TopicListHolder
evenGroups map[int]*TopicListHolder evenGroups map[int]*TopicListHolder
oddLock sync.RWMutex oddLock sync.RWMutex
evenLock sync.RWMutex evenLock sync.RWMutex
groupList []int // TODO: Use an atomic.Value instead to allow this to be updated on long ticks permTree atomic.Value // [string(canSee)]canSee
//permTree map[string][]int // [string(canSee)]canSee
} }
// We've removed the topic list cache cap as admins really shouldn't be abusing groups like this with plugin_guilds around and it was extremely fiddly.
// If this becomes a problem later on, then we can revisit this with a fresh perspective, particularly with regards to what people expect a group to really be
// Also, keep in mind that as-long as the groups don't all have unique sets of forums they can see, then we can optimise a large portion of the work away.
func NewDefaultTopicList() (*DefaultTopicList, error) { func NewDefaultTopicList() (*DefaultTopicList, error) {
tList := &DefaultTopicList{ tList := &DefaultTopicList{
oddGroups: make(map[int]*TopicListHolder), oddGroups: make(map[int]*TopicListHolder),
evenGroups: make(map[int]*TopicListHolder), evenGroups: make(map[int]*TopicListHolder),
} }
var slots = make([]int, 8) // Only cache the topic list for eight groups err := tList.RebuildPermTree()
// TODO: Do something more efficient than this
allGroups, err := Groups.GetAll()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(allGroups) > 0 {
var stopHere int
if len(allGroups) <= 8 {
stopHere = len(allGroups)
} else {
stopHere = 8
}
var lowest = allGroups[0].UserCount
for i := 0; i < stopHere; i++ {
slots[i] = i
if allGroups[i].UserCount < lowest {
lowest = allGroups[i].UserCount
}
}
var findNewLowest = func() {
for _, slot := range slots {
if allGroups[slot].UserCount < lowest {
lowest = allGroups[slot].UserCount
}
}
}
for i := 8; i < len(allGroups); i++ {
if allGroups[i].UserCount > lowest {
for ii, slot := range slots {
if allGroups[i].UserCount > slot {
slots[ii] = i
lowest = allGroups[i].UserCount
findNewLowest()
break
}
}
}
}
tList.groupList = slots
}
err = tList.Tick() err = tList.Tick()
if err != nil { if err != nil {
return nil, err return nil, err
} }
AddScheduledHalfSecondTask(tList.Tick) AddScheduledHalfSecondTask(tList.Tick)
//AddScheduledSecondTask(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second //AddScheduledSecondTask(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second
return tList, nil return tList, nil
} }
// TODO: Add support for groups other than the guest group
func (tList *DefaultTopicList) Tick() error { func (tList *DefaultTopicList) Tick() error {
var oddLists = make(map[int]*TopicListHolder) var oddLists = make(map[int]*TopicListHolder)
var evenLists = make(map[int]*TopicListHolder) var evenLists = make(map[int]*TopicListHolder)
var addList = func(gid int, topicList []*TopicsRow, forumList []Forum, paginator Paginator) { var addList = func(gid int, holder *TopicListHolder) {
if gid%2 == 0 { if gid%2 == 0 {
evenLists[gid] = &TopicListHolder{topicList, forumList, paginator} evenLists[gid] = holder
} else { } else {
oddLists[gid] = &TopicListHolder{topicList, forumList, paginator} oddLists[gid] = holder
} }
} }
guestGroup, err := Groups.Get(GuestUser.Group) var canSeeHolders = make(map[string]*TopicListHolder)
if err != nil { for name, canSee := range tList.permTree.Load().(map[string][]int) {
return err topicList, forumList, paginator, err := tList.GetListByCanSee(canSee, 1)
}
topicList, forumList, paginator, err := tList.getListByGroup(guestGroup, 1)
if err != nil {
return err
}
addList(guestGroup.ID, topicList, forumList, paginator)
for _, gid := range tList.groupList {
group, err := Groups.Get(gid) // TODO: Bulk load the groups?
if err != nil { if err != nil {
return err return err
} }
if group.UserCount == 0 { canSeeHolders[name] = &TopicListHolder{topicList, forumList, paginator}
}
allGroups, err := Groups.GetAll()
if err != nil {
return err
}
for _, group := range allGroups {
// ? - Move the user count check to instance initialisation? Might require more book-keeping, particularly when a user moves into a zero user group
if group.UserCount == 0 && group.ID != GuestUser.Group {
continue continue
} }
var canSee = make([]byte, len(group.CanSee))
topicList, forumList, paginator, err := tList.getListByGroup(group, 1) for i, item := range group.CanSee {
if err != nil { canSee[i] = byte(item)
return err
} }
addList(group.ID, topicList, forumList, paginator) addList(group.ID, canSeeHolders[string(canSee)])
} }
tList.oddLock.Lock() tList.oddLock.Lock()
@ -142,6 +106,28 @@ func (tList *DefaultTopicList) Tick() error {
return nil return nil
} }
func (tList *DefaultTopicList) RebuildPermTree() error {
// TODO: Do something more efficient than this
allGroups, err := Groups.GetAll()
if err != nil {
return err
}
var permTree = make(map[string][]int) // [string(canSee)]canSee
for _, group := range allGroups {
var canSee = make([]byte, len(group.CanSee))
for i, item := range group.CanSee {
canSee[i] = byte(item)
}
var canSeeInt = make([]int, len(canSee))
copy(canSeeInt, group.CanSee)
permTree[string(canSee)] = canSeeInt
}
tList.permTree.Store(permTree)
return nil
}
func (tList *DefaultTopicList) GetListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) { func (tList *DefaultTopicList) GetListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) {
// TODO: Cache the first three pages not just the first along with all the topics on this beaten track // TODO: Cache the first three pages not just the first along with all the topics on this beaten track
if page == 1 { if page == 1 {
@ -161,13 +147,11 @@ func (tList *DefaultTopicList) GetListByGroup(group *Group, page int) (topicList
} }
} }
return tList.getListByGroup(group, page) // TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
return tList.GetListByCanSee(group.CanSee, page)
} }
func (tList *DefaultTopicList) getListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) { func (tList *DefaultTopicList) GetListByCanSee(canSee []int, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) {
// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
canSee := group.CanSee
// We need a list of the visible forums for Quick Topic // We need a list of the visible forums for Quick Topic
// ? - Would it be useful, if we could post in social groups from /topics/? // ? - Would it be useful, if we could post in social groups from /topics/?
for _, fid := range canSee { for _, fid := range canSee {
@ -252,11 +236,12 @@ func (tList *DefaultTopicList) getList(page int, argList []interface{}, qlist st
} }
topicItem.Link = BuildTopicURL(NameToSlug(topicItem.Title), topicItem.ID) topicItem.Link = BuildTopicURL(NameToSlug(topicItem.Title), topicItem.ID)
// TODO: Pass forum to something like topicItem.Forum and use that instead of these two properties? Could be more flexible.
forum := Forums.DirtyGet(topicItem.ParentID) forum := Forums.DirtyGet(topicItem.ParentID)
topicItem.ForumName = forum.Name topicItem.ForumName = forum.Name
topicItem.ForumLink = forum.Link topicItem.ForumLink = forum.Link
//topicItem.CreatedAt = RelativeTime(topicItem.CreatedAt) //topicItem.RelativeCreatedAt = RelativeTime(topicItem.CreatedAt)
topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt) topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt)
// TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/ // TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/

View File

@ -53,6 +53,28 @@ type User struct {
TempGroup int TempGroup int
} }
func (user *User) WebSockets() *WsJSONUser {
var groupID = user.Group
if user.TempGroup != 0 {
groupID = user.TempGroup
}
// TODO: Do we want to leak the user's permissions? Users will probably be able to see their status from the group tags, but still
return &WsJSONUser{user.ID, user.Link, user.Name, groupID, user.IsMod, user.Avatar, user.Level, user.Score, user.Liked}
}
// Use struct tags to avoid having to define this? It really depends on the circumstances, sometimes we want the whole thing, sometimes... not.
type WsJSONUser struct {
ID int
Link string
Name string
Group int // Be sure to mask with TempGroup
IsMod bool
Avatar string
Level int
Score int
Liked int
}
type UserStmts struct { type UserStmts struct {
activate *sql.Stmt activate *sql.Stmt
changeGroup *sql.Stmt changeGroup *sql.Stmt

View File

@ -10,6 +10,7 @@ package common
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -28,54 +29,218 @@ type WSUser struct {
User *User User *User
} }
type WSHub struct { // TODO: Make this an interface?
type WsHubImpl struct {
// TODO: Shard this map
OnlineUsers map[int]*WSUser OnlineUsers map[int]*WSUser
OnlineGuests map[*WSUser]bool OnlineGuests map[*WSUser]bool
GuestLock sync.RWMutex GuestLock sync.RWMutex
UserLock sync.RWMutex UserLock sync.RWMutex
lastTick time.Time
lastTopicList []*TopicsRow
} }
// TODO: Disable WebSockets on high load? Add a Control Panel interface for disabling it? // TODO: Disable WebSockets on high load? Add a Control Panel interface for disabling it?
var EnableWebsockets = true // Put this in caps for consistency with the other constants? var EnableWebsockets = true // Put this in caps for consistency with the other constants?
var WsHub WSHub // TODO: Rename this to WebSockets?
var WsHub WsHubImpl
var wsUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024} var wsUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
var errWsNouser = errors.New("This user isn't connected via WebSockets") var errWsNouser = errors.New("This user isn't connected via WebSockets")
func init() { func init() {
adminStatsWatchers = make(map[*WSUser]bool) adminStatsWatchers = make(map[*WSUser]bool)
WsHub = WSHub{ topicListWatchers = make(map[*WSUser]bool)
// TODO: Do we really want to initialise this here instead of in main.go / general_test.go like the other things?
WsHub = WsHubImpl{
OnlineUsers: make(map[int]*WSUser), OnlineUsers: make(map[int]*WSUser),
OnlineGuests: make(map[*WSUser]bool), OnlineGuests: make(map[*WSUser]bool),
} }
} }
func (hub *WSHub) GuestCount() int { func (hub *WsHubImpl) Start() {
//fmt.Println("running hub.Start")
if Config.DisableLiveTopicList {
return
}
hub.lastTick = time.Now()
AddScheduledSecondTask(hub.Tick)
}
type WsTopicList struct {
Topics []*WsTopicsRow
}
// This Tick is seperate from the admin one, as we want to process that in parallel with this due to the blocking calls to gopsutil
func (hub *WsHubImpl) Tick() error {
//fmt.Println("running hub.Tick")
// Don't waste CPU time if nothing has happened
// TODO: Get a topic list method which strips stickies?
tList, _, _, err := TopicList.GetList(1)
if err != nil {
hub.lastTick = time.Now()
return err // TODO: Do we get ErrNoRows here?
}
defer func() {
hub.lastTick = time.Now()
hub.lastTopicList = tList
}()
if len(tList) == 0 {
return nil
}
//fmt.Println("checking for changes")
// TODO: Optimise this by only sniffing the top non-sticky
if len(tList) == len(hub.lastTopicList) {
var hasItem = false
for j, tItem := range tList {
if !tItem.Sticky {
if tItem.ID != hub.lastTopicList[j].ID {
hasItem = true
}
}
}
if !hasItem {
return nil
}
}
// TODO: Implement this for guests too? Should be able to optimise it far better there due to them sharing the same permission set
// TODO: Be less aggressive with the locking, maybe use an array of sorts instead of hitting the main map every-time
topicListMutex.RLock()
if len(topicListWatchers) == 0 {
//fmt.Println("no watchers")
topicListMutex.RUnlock()
return nil
}
//fmt.Println("found changes")
// Copy these over so we close this loop as fast as possible so we can release the read lock, especially if the group gets are backed by calls to the database
var groupIDs = make(map[int]bool)
var currentWatchers = make([]*WSUser, len(topicListWatchers))
var i = 0
for wsUser, _ := range topicListWatchers {
currentWatchers[i] = wsUser
groupIDs[wsUser.User.Group] = true
i++
}
topicListMutex.RUnlock()
var groups = make(map[int]*Group)
var canSeeMap = make(map[string][]int)
for groupID, _ := range groupIDs {
group, err := Groups.Get(groupID)
if err != nil {
// TODO: Do we really want to halt all pushes for what is possibly just one user?
return err
}
groups[group.ID] = group
var canSee = make([]byte, len(group.CanSee))
for i, item := range group.CanSee {
canSee[i] = byte(item)
}
canSeeMap[string(canSee)] = group.CanSee
}
var canSeeRenders = make(map[string][]byte)
for name, canSee := range canSeeMap {
topicList, forumList, _, err := TopicList.GetListByCanSee(canSee, 1)
if err != nil {
return err // TODO: Do we get ErrNoRows here?
}
if len(topicList) == 0 {
continue
}
_ = forumList // Might use this later after we get the base feature working
//fmt.Println("canSeeItem")
if topicList[0].Sticky {
var lastSticky = 0
for i, row := range topicList {
if !row.Sticky {
lastSticky = i
break
}
}
if lastSticky == 0 {
continue
}
//fmt.Println("lastSticky: ", lastSticky)
//fmt.Println("before topicList: ", topicList)
topicList = topicList[lastSticky:]
//fmt.Println("after topicList: ", topicList)
}
// TODO: Compare to previous tick to eliminate unnecessary work and data
var wsTopicList = make([]*WsTopicsRow, len(topicList))
for i, topicRow := range topicList {
wsTopicList[i] = topicRow.WebSockets()
}
outBytes, err := json.Marshal(&WsTopicList{wsTopicList})
if err != nil {
return err
}
canSeeRenders[name] = outBytes
}
// TODO: Use MessagePack for additional speed?
//fmt.Println("writing to the clients")
for _, wsUser := range currentWatchers {
group := groups[wsUser.User.Group]
var canSee = make([]byte, len(group.CanSee))
for i, item := range group.CanSee {
canSee[i] = byte(item)
}
w, err := wsUser.conn.NextWriter(websocket.TextMessage)
if err != nil {
//fmt.Printf("werr for #%d: %s\n", wsUser.User.ID, err)
topicListMutex.Lock()
delete(topicListWatchers, wsUser)
topicListMutex.Unlock()
continue
}
//fmt.Println("writing to user #", wsUser.User.ID)
outBytes := canSeeRenders[string(canSee)]
//fmt.Println("outBytes: ", string(outBytes))
w.Write(outBytes)
w.Close()
}
return nil
}
func (hub *WsHubImpl) GuestCount() int {
defer hub.GuestLock.RUnlock() defer hub.GuestLock.RUnlock()
hub.GuestLock.RLock() hub.GuestLock.RLock()
return len(hub.OnlineGuests) return len(hub.OnlineGuests)
} }
func (hub *WSHub) UserCount() int { func (hub *WsHubImpl) UserCount() int {
defer hub.UserLock.RUnlock() defer hub.UserLock.RUnlock()
hub.UserLock.RLock() hub.UserLock.RLock()
return len(hub.OnlineUsers) return len(hub.OnlineUsers)
} }
func (hub *WSHub) broadcastMessage(msg string) error { func (hub *WsHubImpl) broadcastMessage(msg string) error {
hub.UserLock.RLock() hub.UserLock.RLock()
defer hub.UserLock.RUnlock()
for _, wsUser := range hub.OnlineUsers { for _, wsUser := range hub.OnlineUsers {
w, err := wsUser.conn.NextWriter(websocket.TextMessage) w, err := wsUser.conn.NextWriter(websocket.TextMessage)
if err != nil { if err != nil {
return err return err
} }
_, _ = w.Write([]byte(msg)) _, _ = w.Write([]byte(msg))
w.Close()
} }
hub.UserLock.RUnlock()
return nil return nil
} }
func (hub *WSHub) pushMessage(targetUser int, msg string) error { func (hub *WsHubImpl) pushMessage(targetUser int, msg string) error {
hub.UserLock.RLock() hub.UserLock.RLock()
wsUser, ok := hub.OnlineUsers[targetUser] wsUser, ok := hub.OnlineUsers[targetUser]
hub.UserLock.RUnlock() hub.UserLock.RUnlock()
@ -93,7 +258,7 @@ func (hub *WSHub) pushMessage(targetUser int, msg string) error {
return nil return nil
} }
func (hub *WSHub) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { func (hub *WsHubImpl) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
//log.Print("In pushAlert") //log.Print("In pushAlert")
hub.UserLock.RLock() hub.UserLock.RLock()
wsUser, ok := hub.OnlineUsers[targetUser] wsUser, ok := hub.OnlineUsers[targetUser]
@ -119,7 +284,7 @@ func (hub *WSHub) pushAlert(targetUser int, asid int, event string, elementType
return nil return nil
} }
func (hub *WSHub) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { func (hub *WsHubImpl) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
var wsUsers []*WSUser var wsUsers []*WSUser
hub.UserLock.RLock() hub.UserLock.RLock()
// We don't want to keep a lock on this for too long, so we'll accept some nil pointers // We don't want to keep a lock on this for too long, so we'll accept some nil pointers
@ -193,6 +358,7 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr
delete(WsHub.OnlineGuests, wsUser) delete(WsHub.OnlineGuests, wsUser)
WsHub.GuestLock.Unlock() WsHub.GuestLock.Unlock()
} else { } else {
// TODO: Make sure the admin is removed from the admin stats list in the case that an error happens
WsHub.UserLock.Lock() WsHub.UserLock.Lock()
delete(WsHub.OnlineUsers, user.ID) delete(WsHub.OnlineUsers, user.ID)
WsHub.UserLock.Unlock() WsHub.UserLock.Unlock()
@ -229,26 +395,17 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr
return nil return nil
} }
// TODO: Use a map instead of a switch to make this more modular?
func wsPageResponses(wsUser *WSUser, page []byte) { func wsPageResponses(wsUser *WSUser, page []byte) {
//fmt.Println("entering page: ", string(page))
switch string(page) { switch string(page) {
// Live Topic List is an experimental feature
// TODO: Optimise this to reduce the amount of contention
case "/topics/":
topicListMutex.Lock()
topicListWatchers[wsUser] = true
topicListMutex.Unlock()
case "/panel/": case "/panel/":
//log.Print("/panel/ WS Route")
/*w, err := wsUser.conn.NextWriter(websocket.TextMessage)
if err != nil {
//log.Print(err.Error())
return
}
log.Print(WsHub.online_users)
uonline := WsHub.UserCount()
gonline := WsHub.GuestCount()
totonline := uonline + gonline
w.Write([]byte("set #dash-totonline " + strconv.Itoa(totonline) + " online\r"))
w.Write([]byte("set #dash-gonline " + strconv.Itoa(gonline) + " guests online\r"))
w.Write([]byte("set #dash-uonline " + strconv.Itoa(uonline) + " users online\r"))
w.Close()*/
// Listen for changes and inform the admins... // Listen for changes and inform the admins...
adminStatsMutex.Lock() adminStatsMutex.Lock()
watchers := len(adminStatsWatchers) watchers := len(adminStatsWatchers)
@ -260,8 +417,15 @@ func wsPageResponses(wsUser *WSUser, page []byte) {
} }
} }
// TODO: Use a map instead of a switch to make this more modular?
func wsLeavePage(wsUser *WSUser, page []byte) { func wsLeavePage(wsUser *WSUser, page []byte) {
//fmt.Println("leaving page: ", string(page))
switch string(page) { switch string(page) {
// Live Topic List is an experimental feature
case "/topics/":
topicListMutex.Lock()
delete(topicListWatchers, wsUser)
topicListMutex.Unlock()
case "/panel/": case "/panel/":
adminStatsMutex.Lock() adminStatsMutex.Lock()
delete(adminStatsWatchers, wsUser) delete(adminStatsWatchers, wsUser)
@ -269,6 +433,10 @@ func wsLeavePage(wsUser *WSUser, page []byte) {
} }
} }
// TODO: Abstract this
// TODO: Use odd-even sharding
var topicListWatchers map[*WSUser]bool
var topicListMutex sync.RWMutex
var adminStatsWatchers map[*WSUser]bool var adminStatsWatchers map[*WSUser]bool
var adminStatsMutex sync.RWMutex var adminStatsMutex sync.RWMutex
@ -385,20 +553,18 @@ AdminStatLoop:
} }
} }
adminStatsMutex.RLock() // Acquire a write lock for now, so we can handle the delete() case below and the read one simultaneously
watchers := adminStatsWatchers // TODO: Stop taking a write lock here if it isn't necessary
adminStatsMutex.RUnlock() adminStatsMutex.Lock()
for watcher := range adminStatsWatchers {
for watcher := range watchers {
w, err := watcher.conn.NextWriter(websocket.TextMessage) w, err := watcher.conn.NextWriter(websocket.TextMessage)
if err != nil { if err != nil {
adminStatsMutex.Lock()
delete(adminStatsWatchers, watcher) delete(adminStatsWatchers, watcher)
adminStatsMutex.Unlock()
continue continue
} }
// nolint // nolint
// TODO: Use JSON for this to make things more portable and easier to convert to MessagePack, if need be?
if !noStatUpdates { if !noStatUpdates {
w.Write([]byte("set #dash-totonline <span>" + strconv.Itoa(totonline) + totunit + " online</span>\r")) w.Write([]byte("set #dash-totonline <span>" + strconv.Itoa(totonline) + totunit + " online</span>\r"))
w.Write([]byte("set #dash-gonline <span>" + strconv.Itoa(gonline) + gunit + " guests online</span>\r")) w.Write([]byte("set #dash-gonline <span>" + strconv.Itoa(gonline) + gunit + " guests online</span>\r"))
@ -421,6 +587,7 @@ AdminStatLoop:
w.Close() w.Close()
} }
adminStatsMutex.Unlock()
lastUonline = uonline lastUonline = uonline
lastGonline = gonline lastGonline = gonline

File diff suppressed because it is too large Load Diff

View File

@ -285,20 +285,20 @@
"topics_likes_suffix":"likes", "topics_likes_suffix":"likes",
"topics_last":"Last", "topics_last":"Last",
"topics_starter":"Starter", "topics_starter":"Starter",
"topic_like_count_suffix":" likes", "topic.like_count_suffix":" likes",
"topic_plus":"+", "topic.plus":"+",
"topic_plus_one":"+1", "topic.plus_one":"+1",
"topic_gap_up":" up", "topic.gap_up":" up",
"topic_level":"Level", "topic.level":"Level",
"topic_edit_button_text":"Edit", "topic.edit_button_text":"Edit",
"topic_delete_button_text":"Delete", "topic.delete_button_text":"Delete",
"topic_ip_button_text":"IP", "topic.ip_button_text":"IP",
"topic_lock_button_text":"Lock", "topic.lock_button_text":"Lock",
"topic_unlock_button_text":"Unlock", "topic.unlock_button_text":"Unlock",
"topic_pin_button_text":"Pin", "topic.pin_button_text":"Pin",
"topic_unpin_button_text":"Unpin", "topic.unpin_button_text":"Unpin",
"topic_report_button_text":"Report", "topic.report_button_text":"Report",
"topic_flag_button_text":"Flag", "topic.flag_button_text":"Flag",
"panel_rank_admins":"Admins", "panel_rank_admins":"Admins",
"panel_rank_mods":"Mods", "panel_rank_mods":"Mods",
@ -413,16 +413,16 @@
"create_topic_create_topic_button":"Create Topic", "create_topic_create_topic_button":"Create Topic",
"create_topic_add_file_button":"Add File", "create_topic_add_file_button":"Add File",
"quick_topic_aria":"Quick Topic Form", "quick_topic.aria":"Quick Topic Form",
"quick_topic_avatar_tooltip":"Your Avatar", "quick_topic.avatar_tooltip":"Your Avatar",
"quick_topic_avatar_alt":"Your Avatar", "quick_topic.avatar_alt":"Your Avatar",
"quick_topic_whatsup":"What's up?", "quick_topic.whatsup":"What's up?",
"quick_topic_content_placeholder":"Insert post here", "quick_topic.content_placeholder":"Insert post here",
"quick_topic_add_poll_option":"Add new poll option", "quick_topic.add_poll_option":"Add new poll option",
"quick_topic_create_topic_button":"Create Topic", "quick_topic.create_topic_button":"Create Topic",
"quick_topic_add_poll_button":"Add Poll", "quick_topic.add_poll_button":"Add Poll",
"quick_topic_add_file_button":"Add File", "quick_topic.add_file_button":"Add File",
"quick_topic_cancel_button":"Cancel", "quick_topic.cancel_button":"Cancel",
"topic_list_create_topic_tooltip":"Create Topic", "topic_list_create_topic_tooltip":"Create Topic",
"topic_list_create_topic_aria":"Create a topic", "topic_list_create_topic_aria":"Create a topic",
@ -435,8 +435,8 @@
"topic_list_moderate_run":"Run", "topic_list_moderate_run":"Run",
"topic_list_move_head":"Move these topics to?", "topic_list_move_head":"Move these topics to?",
"topic_list_move_button":"Move Topics", "topic_list_move_button":"Move Topics",
"status_closed_tooltip":"Status: Closed", "status.closed_tooltip":"Status: Closed",
"status_pinned_tooltip":"Status: Pinned", "status.pinned_tooltip":"Status: Pinned",
"topics_head":"All Topics", "topics_head":"All Topics",
"topics_locked_tooltip":"You don't have the permissions needed to create a topic", "topics_locked_tooltip":"You don't have the permissions needed to create a topic",
@ -456,68 +456,68 @@
"forums_none":"None", "forums_none":"None",
"forums_no_forums":"You don't have access to any forums.", "forums_no_forums":"You don't have access to any forums.",
"topic_opening_post_aria":"The opening post for this topic", "topic.opening_post_aria":"The opening post for this topic",
"topic_status_closed_aria":"This topic has been locked", "topic.status_closed_aria":"This topic has been locked",
"topic_title_input_aria":"Topic Title Input", "topic.title_input_aria":"Topic Title Input",
"topic_update_button":"Update", "topic.update_button":"Update",
"topic_userinfo_aria":"The information on the poster", "topic.userinfo_aria":"The information on the poster",
"topic_poll_aria":"The main poll for this topic", "topic.poll_aria":"The main poll for this topic",
"topic_poll_vote":"Vote", "topic.poll_vote":"Vote",
"topic_poll_results":"Results", "topic.poll_results":"Results",
"topic_poll_cancel":"Cancel", "topic.poll_cancel":"Cancel",
"topic_post_controls_aria":"Controls and Author Information", "topic.post_controls_aria":"Controls and Author Information",
"topic_unlike_tooltip":"Unlike", "topic.unlike_tooltip":"Unlike",
"topic_unlike_aria":"Unlike this topic", "topic.unlike_aria":"Unlike this topic",
"topic_like_tooltip":"Like", "topic.like_tooltip":"Like",
"topic_like_aria":"Like this topic", "topic.like_aria":"Like this topic",
"topic_edit_tooltip":"Edit Topic", "topic.edit_tooltip":"Edit Topic",
"topic_edit_aria":"Edit this topic", "topic.edit_aria":"Edit this topic",
"topic_delete_tooltip":"Delete Topic", "topic.delete_tooltip":"Delete Topic",
"topic_delete_aria":"Delete this topic", "topic.delete_aria":"Delete this topic",
"topic_unlock_tooltip":"Unlock Topic", "topic.unlock_tooltip":"Unlock Topic",
"topic_unlock_aria":"Unlock this topic", "topic.unlock_aria":"Unlock this topic",
"topic_lock_tooltip":"Lock Topic", "topic.lock_tooltip":"Lock Topic",
"topic_lock_aria":"Lock this topic", "topic.lock_aria":"Lock this topic",
"topic_unpin_tooltip":"Unpin Topic", "topic.unpin_tooltip":"Unpin Topic",
"topic_unpin_aria":"Unpin this topic", "topic.unpin_aria":"Unpin this topic",
"topic_pin_tooltip":"Pin Topic", "topic.pin_tooltip":"Pin Topic",
"topic_pin_aria":"Pin this topic", "topic.pin_aria":"Pin this topic",
"topic_ip_tooltip":"View IP", "topic.ip_tooltip":"View IP",
"topic_ip_full_tooltip":"IP Address", "topic.ip_full_tooltip":"IP Address",
"topic_ip_full_aria":"This user's IP Address", "topic.ip_full_aria":"This user's IP Address",
"topic_flag_tooltip":"Flag this topic", "topic.flag_tooltip":"Flag this topic",
"topic_flag_aria":"Flag this topic", "topic.flag_aria":"Flag this topic",
"topic_report_tooltip":"Report this topic", "topic.report_tooltip":"Report this topic",
"topic_report_aria":"Report this topic", "topic.report_aria":"Report this topic",
"topic_like_count_aria":"The number of likes on this topic", "topic.like_count_aria":"The number of likes on this topic",
"topic_like_count_tooltip":"Like Count", "topic.like_count_tooltip":"Like Count",
"topic_level_aria":"The poster's level", "topic.level_aria":"The poster's level",
"topic_level_tooltip":"Level", "topic.level_tooltip":"Level",
"topic_current_page_aria":"The current page for this topic", "topic.current_page_aria":"The current page for this topic",
"topic_post_like_tooltip":"Like this", "topic.post_like_tooltip":"Like this",
"topic_post_like_aria":"Like this post", "topic.post_like_aria":"Like this post",
"topic_post_unlike_tooltip":"Unlike this", "topic.post_unlike_tooltip":"Unlike this",
"topic_post_unlike_aria":"Unlike this post", "topic.post_unlike_aria":"Unlike this post",
"topic_post_edit_tooltip":"Edit Reply", "topic.post_edit_tooltip":"Edit Reply",
"topic_post_edit_aria":"Edit this post", "topic.post_edit_aria":"Edit this post",
"topic_post_delete_tooltip":"Delete Reply", "topic.post_delete_tooltip":"Delete Reply",
"topic_post_delete_aria":"Delete this post", "topic.post_delete_aria":"Delete this post",
"topic_post_ip_tooltip":"View IP", "topic.post_ip_tooltip":"View IP",
"topic_post_flag_tooltip":"Flag this reply", "topic.post_flag_tooltip":"Flag this reply",
"topic_post_flag_aria":"Flag this reply", "topic.post_flag_aria":"Flag this reply",
"topic_post_like_count_tooltip":"Like Count", "topic.post_like_count_tooltip":"Like Count",
"topic_post_level_aria":"The poster's level", "topic.post_level_aria":"The poster's level",
"topic_post_level_tooltip":"Level", "topic.post_level_tooltip":"Level",
"topic_reply_aria":"The quick reply form", "topic.reply_aria":"The quick reply form",
"topic_reply_content":"Insert reply here", "topic.reply_content":"Insert reply here",
"topic_reply_content_alt":"What do you think?", "topic.reply_content_alt":"What do you think?",
"topic_reply_add_poll_option":"Add new poll option", "topic.reply_add_poll_option":"Add new poll option",
"topic_reply_button":"Create Reply", "topic.reply_button":"Create Reply",
"topic_reply_add_poll_button":"Add Poll", "topic.reply_add_poll_button":"Add Poll",
"topic_reply_add_file_button":"Add File", "topic.reply_add_file_button":"Add File",
"topic_level_prefix":"Level ", "topic.level_prefix":"Level ",
"topic_your_information":"Your information", "topic.your_information":"Your information",
"paginator_less_than":"&lt;", "paginator_less_than":"&lt;",
"paginator_greater_than":"&gt;", "paginator_greater_than":"&gt;",

View File

@ -442,6 +442,9 @@ func main() {
log.Fatal("Received a signal to shutdown: ", sig) log.Fatal("Received a signal to shutdown: ", sig)
}() }()
// Start up the WebSocket ticks
common.WsHub.Start()
//if profiling { //if profiling {
// pprof.StopCPUProfile() // pprof.StopCPUProfile()
//} //}

View File

@ -1,5 +1,7 @@
'use strict'; 'use strict';
var formVars = {}; var formVars = {};
var tmplInits = {};
var tmplPhrases = [];
var alertList = []; var alertList = [];
var alertCount = 0; var alertCount = 0;
var conn; var conn;
@ -72,19 +74,21 @@ function loadAlerts(menuAlerts)
for(var i in data.msgs) { for(var i in data.msgs) {
var msg = data.msgs[i]; var msg = data.msgs[i];
var mmsg = msg.msg; var mmsg = msg.msg;
if("sub" in msg) { if("sub" in msg) {
for(var i = 0; i < msg.sub.length; i++) { for(var i = 0; i < msg.sub.length; i++) {
mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]); mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]);
//console.log("Sub #" + i + ":",msg.sub[i]); //console.log("Sub #" + i + ":",msg.sub[i]);
} }
} }
alist += Template_alert({
let aItem = Template_alert({
ASID: msg.asid || 0, ASID: msg.asid || 0,
Path: msg.path, Path: msg.path,
Avatar: msg.avatar || "", Avatar: msg.avatar || "",
Message: mmsg Message: mmsg
}) })
alist += aItem;
alertList.push(aItem);
//console.log(msg); //console.log(msg);
//console.log(mmsg); //console.log(mmsg);
} }
@ -138,64 +142,97 @@ function SplitN(data,ch,n) {
return out; return out;
} }
function runWebSockets() { function wsAlertEvent(data) {
if(window.location.protocol == "https:") var msg = data.msg;
conn = new WebSocket("wss://" + document.location.host + "/ws/"); if("sub" in data) {
else conn = new WebSocket("ws://" + document.location.host + "/ws/"); for(var i = 0; i < data.sub.length; i++) {
msg = msg.replace("\{"+i+"\}", data.sub[i]);
}
}
conn.onopen = function() { let aItem = Template_alert({
ASID: data.asid || 0,
Path: data.path,
Avatar: data.avatar || "",
Message: msg
})
alertList.push(aItem);
if(alertList.length > 8) alertList.shift();
//console.log("post alertList",alertList);
alertCount++;
var alist = "";
for (var i = 0; i < alertList.length; i++) alist += alertList[i];
//console.log(alist);
// TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts");
var alertListNode = generalAlerts.getElementsByClassName("alertList")[0];
var alertCounterNode = generalAlerts.getElementsByClassName("alert_counter")[0];
alertListNode.innerHTML = alist;
alertCounterNode.textContent = alertCount;
// TODO: Add some sort of notification queue to avoid flooding the end-user with notices?
// TODO: Use the site name instead of "Something Happened"
if(Notification.permission === "granted") {
var n = new Notification("Something Happened",{
body: msg,
icon: data.avatar,
});
setTimeout(n.close.bind(n), 8000);
}
bindToAlerts();
}
function runWebSockets() {
if(window.location.protocol == "https:") {
conn = new WebSocket("wss://" + document.location.host + "/ws/");
} else conn = new WebSocket("ws://" + document.location.host + "/ws/");
conn.onopen = () => {
console.log("The WebSockets connection was opened"); console.log("The WebSockets connection was opened");
conn.send("page " + document.location.pathname + '\r'); conn.send("page " + document.location.pathname + '\r');
// TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on // TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on
Notification.requestPermission(); if(loggedIn) {
Notification.requestPermission();
}
} }
conn.onclose = function() { conn.onclose = () => {
conn = false; conn = false;
console.log("The WebSockets connection was closed"); console.log("The WebSockets connection was closed");
} }
conn.onmessage = function(event) { conn.onmessage = (event) => {
//console.log("WSMessage:", event.data); //console.log("WSMessage:", event.data);
if(event.data[0] == "{") { if(event.data[0] == "{") {
console.log("json message");
let data = "";
try { try {
var data = JSON.parse(event.data); data = JSON.parse(event.data);
} catch(err) { } catch(err) {
console.log(err); console.log(err);
return;
} }
// TODO: Fix the data races in this code
if ("msg" in data) { if ("msg" in data) {
var msg = data.msg wsAlertEvent(data);
if("sub" in data) } else if("Topics" in data) {
for(var i = 0; i < data.sub.length; i++) console.log("topic in data");
msg = msg.replace("\{"+i+"\}", data.sub[i]); console.log("data:", data);
let topic = data.Topics[0];
if("avatar" in data) alertList.push("<div class='alertItem withAvatar' style='background-image:url(\""+data.avatar+"\");'><a class='text' data-asid='"+data.asid+"' href=\""+data.path+"\">"+msg+"</a></div>"); if(topic === undefined){
else alertList.push("<div class='alertItem'><a href=\""+data.path+"\" class='text'>"+msg+"</a></div>"); console.log("empty topic list");
if(alertList.length > 8) alertList.shift(); return;
//console.log("post alertList",alertList);
alertCount++;
var alist = ""
for (var i = 0; i < alertList.length; i++) alist += alertList[i];
//console.log(alist);
// TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts");
var alertListNode = generalAlerts.getElementsByClassName("alertList")[0];
var alertCounterNode = generalAlerts.getElementsByClassName("alert_counter")[0];
alertListNode.innerHTML = alist;
alertCounterNode.textContent = alertCount;
// TODO: Add some sort of notification queue to avoid flooding the end-user with notices?
// TODO: Use the site name instead of "Something Happened"
if(Notification.permission === "granted") {
var n = new Notification("Something Happened",{
body: msg,
icon: data.avatar,
});
setTimeout(n.close.bind(n), 8000);
} }
let renTopic = Template_topics_topic(topic);
bindToAlerts(); let node = $(renTopic);
node.addClass("new_item");
console.log("Prepending to topic list");
$(".topic_list").prepend(node);
} else {
console.log("unknown message");
console.log(data);
} }
} }
@ -217,6 +254,11 @@ function runWebSockets() {
} }
} }
// Temporary hack for templates
function len(item) {
return item.length;
}
function loadScript(name, callback) { function loadScript(name, callback) {
let url = "//" +siteURL+"/static/"+name let url = "//" +siteURL+"/static/"+name
$.getScript(url) $.getScript(url)
@ -231,16 +273,49 @@ function loadScript(name, callback) {
}); });
} }
function DoNothingButPassBack(item) {
return item;
}
function fetchPhrases() {
fetch("//" +siteURL+"/api/phrases/?query=status")
.then((resp) => resp.json())
.then((data) => {
Object.keys(tmplInits).forEach((key) => {
let phrases = [];
let tmplInit = tmplInits[key];
for(let phraseName of tmplInit) {
phrases.push(data[phraseName]);
}
console.log("Adding phrases");
console.log("key:",key);
console.log("phrases:",phrases);
tmplPhrases[key] = phrases;
})
});
}
$(document).ready(function(){ $(document).ready(function(){
runHook("start_init"); runHook("start_init");
loadScript("template_alert.js",() => { if(loggedIn) {
let toLoad = 1;
loadScript("template_topics_topic.js", () => {
console.log("Loaded template_topics_topic.js");
toLoad--;
if(toLoad===0) fetchPhrases();
});
}
// We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :(
loadScript("template_alert.js", () => {
console.log("Loaded template_alert.js"); console.log("Loaded template_alert.js");
alertsInitted = true; alertsInitted = true;
var alertMenuList = document.getElementsByClassName("menu_alerts"); var alertMenuList = document.getElementsByClassName("menu_alerts");
for(var i = 0; i < alertMenuList.length; i++) { for(var i = 0; i < alertMenuList.length; i++) {
loadAlerts(alertMenuList[i]); loadAlerts(alertMenuList[i]);
} }
}) });
if(window["WebSocket"]) runWebSockets(); if(window["WebSocket"]) runWebSockets();
else conn = false; else conn = false;

View File

@ -447,14 +447,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO: Cover more suspicious strings and at a lower layer than this // TODO: Cover more suspicious strings and at a lower layer than this
for _, char := range req.URL.Path { for _, char := range req.URL.Path {
if char != '&' && !(char > 44 && char < 58) && char != '=' && char != '?' && !(char > 64 && char < 91) && char != '\\' && char != '_' && !(char > 96 && char < 123) { if char != '&' && !(char > 44 && char < 58) && char != '=' && char != '?' && !(char > 64 && char < 91) && char != '\\' && char != '_' && !(char > 96 && char < 123) {
router.SuspiciousRequest(req,"") router.SuspiciousRequest(req,"Bad char in path")
break break
} }
} }
lowerPath := strings.ToLower(req.URL.Path) lowerPath := strings.ToLower(req.URL.Path)
// TODO: Flag any requests which has a dot with anything but a number after that // TODO: Flag any requests which has a dot with anything but a number after that
if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") { if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") {
router.SuspiciousRequest(req,"") router.SuspiciousRequest(req,"Bad snippet in path")
} }
// Indirect the default route onto a different one // Indirect the default route onto a different one
@ -528,7 +528,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO: Test this // TODO: Test this
items = items[:0] items = items[:0]
indices = indices[:0] indices = indices[:0]
router.SuspiciousRequest(req,"") router.SuspiciousRequest(req,"Illegal char in UA")
router.requestLogger.Print("UA Buffer: ", buffer) router.requestLogger.Print("UA Buffer: ", buffer)
router.requestLogger.Print("UA Buffer String: ", string(buffer)) router.requestLogger.Print("UA Buffer String: ", string(buffer))
break break

View File

@ -2,7 +2,6 @@ package main
// TODO: How should we handle *HeaderLite and *Header? // TODO: How should we handle *HeaderLite and *Header?
func routes() { func routes() {
addRoute(View("routeAPI", "/api/"))
addRoute(View("routes.Overview", "/overview/")) addRoute(View("routes.Overview", "/overview/"))
addRoute(View("routes.CustomPage", "/pages/", "extraData")) addRoute(View("routes.CustomPage", "/pages/", "extraData"))
addRoute(View("routes.ForumList", "/forums/" /*,"&forums"*/)) addRoute(View("routes.ForumList", "/forums/" /*,"&forums"*/))
@ -12,6 +11,12 @@ func routes() {
View("routes.ShowAttachment", "/attachs/", "extraData").Before("ParseForm"), View("routes.ShowAttachment", "/attachs/", "extraData").Before("ParseForm"),
) )
apiGroup := newRouteGroup("/api/",
View("routeAPI", "/api/"),
View("routeAPIPhrases", "/api/phrases/"), // TODO: Be careful with exposing the panel phrases here
)
addRouteGroup(apiGroup)
// TODO: Reduce the number of Befores. With a new method, perhaps? // TODO: Reduce the number of Befores. With a new method, perhaps?
reportGroup := newRouteGroup("/report/", reportGroup := newRouteGroup("/report/",
Action("routes.ReportSubmit", "/report/submit/", "extraData"), Action("routes.ReportSubmit", "/report/submit/", "extraData"),

View File

@ -7,8 +7,11 @@
package main package main
import ( import (
"encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"unicode"
"./common" "./common"
) )
@ -95,3 +98,91 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
} }
return nil return nil
} }
// TODO: Be careful with exposing the panel phrases here, maybe move them into a different namespace? We also need to educate the admin that phrases aren't necessarily secret
func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
w.Header().Set("Content-Type", "application/json")
err := r.ParseForm()
if err != nil {
return common.PreErrorJS("Bad Form", w, r)
}
query := r.FormValue("query")
if query == "" {
return common.PreErrorJS("No query provided", w, r)
}
var negations []string
var positives []string
queryBits := strings.Split(query, ",")
for _, queryBit := range queryBits {
queryBit = strings.TrimSpace(queryBit)
if queryBit[0] == '!' && len(queryBit) > 1 {
queryBit = strings.TrimPrefix(queryBit, "!")
for _, char := range queryBit {
if !unicode.IsLetter(char) && char != '-' && char != '_' {
return common.PreErrorJS("No symbols allowed, only - and _", w, r)
}
}
negations = append(negations, queryBit)
} else {
for _, char := range queryBit {
if !unicode.IsLetter(char) && char != '-' && char != '_' {
return common.PreErrorJS("No symbols allowed, only - and _", w, r)
}
}
positives = append(positives, queryBit)
}
}
if len(positives) == 0 {
return common.PreErrorJS("You haven't requested any phrases", w, r)
}
var phrases map[string]string
// A little optimisation to avoid copying entries from one map to the other, if we don't have to mutate it
if len(positives) > 1 {
phrases = make(map[string]string)
for _, positive := range positives {
// ! Constrain it to topic and status phrases for now
if !strings.HasPrefix(positive, "topic") && !strings.HasPrefix(positive, "status") {
return common.PreErrorJS("Not implemented!", w, r)
}
pPhrases, ok := common.GetTmplPhrasesByPrefix(positive)
if !ok {
return common.PreErrorJS("No such prefix", w, r)
}
for name, phrase := range pPhrases {
phrases[name] = phrase
}
}
} else {
// ! Constrain it to topic and status phrases for now
if !strings.HasPrefix(positives[0], "topic") && !strings.HasPrefix(positives[0], "status") {
return common.PreErrorJS("Not implemented!", w, r)
}
pPhrases, ok := common.GetTmplPhrasesByPrefix(positives[0])
if !ok {
return common.PreErrorJS("No such prefix", w, r)
}
phrases = pPhrases
}
for _, negation := range negations {
for name, _ := range phrases {
if strings.HasPrefix(name, negation) {
delete(phrases, name)
}
}
}
// TODO: Cache the output of this, especially for things like topic, so we don't have to waste more time than we need on this
jsonBytes, err := json.Marshal(phrases)
if err != nil {
return common.InternalError(err, w, r)
}
w.Write(jsonBytes)
return nil
}

View File

@ -81,7 +81,7 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid st
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt) topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
common.RunVhook("forum_trow_assign", &topicItem, &forum) common.RunVhookNoreturn("forum_trow_assign", &topicItem, &forum)
topicList = append(topicList, &topicItem) topicList = append(topicList, &topicItem)
reqUserList[topicItem.CreatedBy] = true reqUserList[topicItem.CreatedBy] = true
reqUserList[topicItem.LastReplyBy] = true reqUserList[topicItem.LastReplyBy] = true

View File

@ -88,7 +88,7 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User) commo
} }
replyLines = strings.Count(replyContent, "\n") replyLines = strings.Count(replyContent, "\n")
if group.IsMod || group.IsAdmin { if group.IsMod {
replyClassName = common.Config.StaffCSS replyClassName = common.Config.StaffCSS
} else { } else {
replyClassName = "" replyClassName = ""

View File

@ -8,10 +8,7 @@ type HTTPSRedirect struct {
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
dest := "https://" + req.Host + req.URL.Path dest := "https://" + req.Host + req.URL.String()
if len(req.URL.RawQuery) > 0 {
dest += "?" + req.URL.RawQuery
}
http.Redirect(w, req, dest, http.StatusTemporaryRedirect) http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
} }

View File

@ -84,7 +84,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit
} }
topic.Tag = postGroup.Tag topic.Tag = postGroup.Tag
if postGroup.IsMod || postGroup.IsAdmin { if postGroup.IsMod {
topic.ClassName = common.Config.StaffCSS topic.ClassName = common.Config.StaffCSS
} }
topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt) topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt)
@ -146,7 +146,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
if postGroup.IsMod || postGroup.IsAdmin { if postGroup.IsMod {
replyItem.ClassName = common.Config.StaffCSS replyItem.ClassName = common.Config.StaffCSS
} else { } else {
replyItem.ClassName = "" replyItem.ClassName = ""
@ -185,7 +185,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit
likedQueryList = append(likedQueryList, replyItem.ID) likedQueryList = append(likedQueryList, replyItem.ID)
} }
common.RunVhook("topic_reply_row_assign", &tpage, &replyItem) common.RunVhookNoreturn("topic_reply_row_assign", &tpage, &replyItem)
// TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis? // TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis?
tpage.ItemList = append(tpage.ItemList, replyItem) tpage.ItemList = append(tpage.ItemList, replyItem)
} }
@ -261,7 +261,7 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, user common.User, sfid
// Lock this to the forum being linked? // Lock this to the forum being linked?
// Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile) // Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile)
var strictmode bool var strictmode bool
common.RunVhook("topic_create_pre_loop", w, r, fid, &header, &user, &strictmode) common.RunVhookNoreturn("topic_create_pre_loop", w, r, fid, &header, &user, &strictmode)
// TODO: Re-add support for plugin_guilds // TODO: Re-add support for plugin_guilds
var forumList []common.Forum var forumList []common.Forum

View File

@ -2,7 +2,9 @@
rem TODO: Make these deletes a little less noisy rem TODO: Make these deletes a little less noisy
del "template_*.go" del "template_*.go"
del "gen_*.go" del "gen_*.go"
del "tmpl_client/template_*.go" cd tmpl_client
del "template_*.go"
cd ..
del "gosora.exe" del "gosora.exe"
echo Generating the dynamic code echo Generating the dynamic code

View File

@ -2,7 +2,9 @@
rem TODO: Make these deletes a little less noisy rem TODO: Make these deletes a little less noisy
del "template_*.go" del "template_*.go"
del "gen_*.go" del "gen_*.go"
del "tmpl_client/template_*.go" cd tmpl_client
del "template_*.go"
cd ..
del "gosora.exe" del "gosora.exe"
echo Generating the dynamic code echo Generating the dynamic code

View File

@ -2,7 +2,9 @@
rem TODO: Make these deletes a little less noisy rem TODO: Make these deletes a little less noisy
del "template_*.go" del "template_*.go"
del "gen_*.go" del "gen_*.go"
del "tmpl_client/template_*.go" cd tmpl_client
del "template_*.go"
cd ..
del "gosora.exe" del "gosora.exe"
echo Generating the dynamic code echo Generating the dynamic code

View File

@ -2,7 +2,9 @@
rem TODO: Make these deletes a little less noisy rem TODO: Make these deletes a little less noisy
del "template_*.go" del "template_*.go"
del "gen_*.go" del "gen_*.go"
del "tmpl_client/template_*.go" cd tmpl_client
del "template_*.go"
cd ..
del "gosora.exe" del "gosora.exe"
echo Generating the dynamic code echo Generating the dynamic code

View File

@ -2,7 +2,9 @@
rem TODO: Make these deletes a little less noisy rem TODO: Make these deletes a little less noisy
del "template_*.go" del "template_*.go"
del "gen_*.go" del "gen_*.go"
del "tmpl_client/template_*.go" cd tmpl_client
del "template_*.go"
cd ..
del "gosora.exe" del "gosora.exe"
echo Generating the dynamic code echo Generating the dynamic code

View File

@ -6,8 +6,6 @@
<div class="coldyn_block"> <div class="coldyn_block">
<div id="dash_left" class="coldyn_item"> <div id="dash_left" class="coldyn_item">
<div class="rowitem"> <div class="rowitem">
<span id="dash_saved">Saved</span>
<!--<span id="dash_username">{{.CurrentUser.Name}}</span>-->
<span id="dash_username"> <span id="dash_username">
<form id="dash_username_form" action="/user/edit/username/submit/?session={{.CurrentUser.Session}}" method="post"></form> <form id="dash_username_form" action="/user/edit/username/submit/?session={{.CurrentUser.Session}}" method="post"></form>
<input form="dash_username_form" name="account-new-username" value="{{.CurrentUser.Name}}" /> <input form="dash_username_form" name="account-new-username" value="{{.CurrentUser.Name}}" />

View File

@ -39,21 +39,21 @@
</form> </form>
</div> </div>
{{if .CurrentUser.Perms.CreateTopic}} {{if .CurrentUser.Perms.CreateTopic}}
<div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="{{lang "quick_topic_aria"}}"> <div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="{{lang "quick_topic.aria"}}">
<form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form> <form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
<img class="little_row_avatar" src="{{.CurrentUser.Avatar}}" height="64" alt="{{lang "quick_topic_avatar_alt"}}" title="{{lang "quick_topic_avatar_tooltip"}}" /> <img class="little_row_avatar" src="{{.CurrentUser.Avatar}}" height="64" alt="{{lang "quick_topic.avatar_alt"}}" title="{{lang "quick_topic.avatar_tooltip"}}" />
<input form="quick_post_form" id="topic_board_input" name="topic-board" value="{{.Forum.ID}}" type="hidden"> <input form="quick_post_form" id="topic_board_input" name="topic-board" value="{{.Forum.ID}}" type="hidden">
<div class="main_form"> <div class="main_form">
<div class="topic_meta"> <div class="topic_meta">
<div class="formrow topic_name_row real_first_child"> <div class="formrow topic_name_row real_first_child">
<div class="formitem"> <div class="formitem">
<input form="quick_post_form" name="topic-name" placeholder="{{lang "quick_topic_whatsup"}}" required> <input form="quick_post_form" name="topic-name" placeholder="{{lang "quick_topic.whatsup"}}" required>
</div> </div>
</div> </div>
</div> </div>
<div class="formrow topic_content_row"> <div class="formrow topic_content_row">
<div class="formitem"> <div class="formitem">
<textarea form="quick_post_form" id="input_content" name="topic-content" placeholder="{{lang "quick_topic_content_placeholder"}}" required></textarea> <textarea form="quick_post_form" id="input_content" name="topic-content" placeholder="{{lang "quick_topic.content_placeholder"}}" required></textarea>
</div> </div>
</div> </div>
<div class="formrow poll_content_row auto_hide"> <div class="formrow poll_content_row auto_hide">
@ -63,13 +63,13 @@
</div> </div>
<div class="formrow quick_button_row"> <div class="formrow quick_button_row">
<div class="formitem"> <div class="formitem">
<button form="quick_post_form" name="topic-button" class="formbutton">{{lang "quick_topic_create_topic_button"}}</button> <button form="quick_post_form" name="topic-button" class="formbutton">{{lang "quick_topic.create_topic_button"}}</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">{{lang "quick_topic_add_poll_button"}}</button> <button form="quick_post_form" class="formbutton" id="add_poll_button">{{lang "quick_topic.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" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">{{lang "quick_topic_add_file_button"}}</label> <label for="upload_files" class="formbutton add_file_button">{{lang "quick_topic.add_file_button"}}</label>
<div id="upload_file_dock"></div>{{end}} <div id="upload_file_dock"></div>{{end}}
<button class="formbutton close_form">{{lang "quick_topic_cancel_button"}}</button> <button class="formbutton close_form">{{lang "quick_topic.cancel_button"}}</button>
</div> </div>
</div> </div>
</div> </div>
@ -85,8 +85,8 @@
<a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a> <a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a>
<br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a> <br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a>
{{/** TODO: Avoid the double '|' when both .IsClosed and .Sticky are set to true. We could probably do this with CSS **/}} {{/** TODO: Avoid the double '|' when both .IsClosed and .Sticky are set to true. We could probably do this with CSS **/}}
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status_closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}} {{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status.closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}} {{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status.pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span> </span>
{{/** TODO: Phase this out of Cosora and remove it **/}} {{/** TODO: Phase this out of Cosora and remove it **/}}
<div class="topic_inner_right rowsmall"> <div class="topic_inner_right rowsmall">

View File

@ -13,6 +13,7 @@
{{end}} {{end}}
<script type="text/javascript"> <script type="text/javascript">
var session = "{{.CurrentUser.Session}}"; var session = "{{.CurrentUser.Session}}";
var loggedIn = {{.CurrentUser.Loggedin}};
var siteURL = "{{.Header.Site.URL}}"; var siteURL = "{{.Header.Site.URL}}";
var maxRequestSize = "{{.Header.Site.MaxRequestSize}}"; var maxRequestSize = "{{.Header.Site.MaxRequestSize}}";
</script> </script>

View File

@ -11,21 +11,21 @@
<main> <main>
<div {{scope "topic_title_block"}} class="rowblock rowhead topic_block" aria-label="{{lang "topic_opening_post_aria"}}"> <div {{scope "topic_title_block"}} class="rowblock rowhead topic_block" aria-label="{{lang "topic.opening_post_aria"}}">
<div class="rowitem topic_item{{if .Topic.Sticky}} topic_sticky_head{{else if .Topic.IsClosed}} topic_closed_head{{end}}"> <div class="rowitem topic_item{{if .Topic.Sticky}} topic_sticky_head{{else if .Topic.IsClosed}} topic_closed_head{{end}}">
<h1 class='topic_name hide_on_edit' title='{{.Topic.Title}}'>{{.Topic.Title}}</h1> <h1 class='topic_name hide_on_edit' title='{{.Topic.Title}}'>{{.Topic.Title}}</h1>
{{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit' title='{{lang "status_closed_tooltip"}}' aria-label='{{lang "topic_status_closed_aria"}}'>&#x1F512;&#xFE0E</span>{{end}} {{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit' title='{{lang "status.closed_tooltip"}}' aria-label='{{lang "topic.status_closed_aria"}}'>&#x1F512;&#xFE0E</span>{{end}}
{{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}} {{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}}
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
{{if .CurrentUser.Perms.EditTopic}} {{if .CurrentUser.Perms.EditTopic}}
<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}} {{if .Poll.ID}}
<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(/static/{{.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(/static/{{.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" style="margin:0;padding:0;"> <div class="topic_content user_content" style="margin:0;padding:0;">
{{range .Poll.QuickOptions}} {{range .Poll.QuickOptions}}
@ -38,9 +38,9 @@
</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">
@ -50,77 +50,48 @@
</article> </article>
{{end}} {{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(/static/{{.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(/static/{{.Header.Theme.Name}}/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;">
<p class="hide_on_edit topic_content user_content" itemprop="text" style="margin:0;padding:0;">{{.Topic.ContentHTML}}</p> <p class="hide_on_edit topic_content user_content" itemprop="text" style="margin:0;padding:0;">{{.Topic.ContentHTML}}</p>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea> <textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
<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"}}">
<a href="{{.Topic.UserLink}}" class="username real_username" rel="author">{{.Topic.CreatedByName}}</a>&nbsp;&nbsp; <a href="{{.Topic.UserLink}}" class="username real_username" rel="author">{{.Topic.CreatedByName}}</a>&nbsp;&nbsp;
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="mod_button"{{if .Topic.Liked}} title="{{lang "topic_unlike_tooltip"}}" aria-label="{{lang "topic_unlike_aria"}}"{{else}} title="{{lang "topic_like_tooltip"}}" aria-label="{{lang "topic_like_aria"}}"{{end}} style="color:#202020;"> {{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="mod_button"{{if .Topic.Liked}} title="{{lang "topic.unlike_tooltip"}}" aria-label="{{lang "topic.unlike_aria"}}"{{else}} title="{{lang "topic.like_tooltip"}}" aria-label="{{lang "topic.like_aria"}}"{{end}} style="color:#202020;">
<button class="username like_label {{if .Topic.Liked}}remove_like{{else}}add_like{{end}}"></button></a>{{end}} <button class="username like_label {{if .Topic.Liked}}remove_like{{else}}add_like{{end}}"></button></a>{{end}}
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
{{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="{{lang "topic_edit_tooltip"}}" aria-label="{{lang "topic_edit_aria"}}"><button class="username edit_label"></button></a>{{end}} {{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="{{lang "topic.edit_tooltip"}}" aria-label="{{lang "topic.edit_aria"}}"><button class="username edit_label"></button></a>{{end}}
{{end}} {{end}}
{{if .CurrentUser.Perms.DeleteTopic}}<a href='/topic/delete/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic_delete_tooltip"}}" aria-label="{{lang "topic_delete_aria"}}"><button class="username trash_label"></button></a>{{end}} {{if .CurrentUser.Perms.DeleteTopic}}<a href='/topic/delete/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic.delete_tooltip"}}" aria-label="{{lang "topic.delete_aria"}}"><button class="username trash_label"></button></a>{{end}}
{{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' style="font-weight:normal;" title="{{lang "topic_unlock_tooltip"}}" aria-label="{{lang "topic_unlock_aria"}}"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic_lock_tooltip"}}" aria-label="{{lang "topic_lock_aria"}}"><button class="username lock_label"></button></a>{{end}}{{end}} {{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' style="font-weight:normal;" title="{{lang "topic.unlock_tooltip"}}" aria-label="{{lang "topic.unlock_aria"}}"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic.lock_tooltip"}}" aria-label="{{lang "topic.lock_aria"}}"><button class="username lock_label"></button></a>{{end}}{{end}}
{{if .CurrentUser.Perms.PinTopic}}{{if .Topic.Sticky}}<a class="mod_button" href='/topic/unstick/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' style="font-weight:normal;" title="{{lang "topic_unpin_tooltip"}}" aria-label="{{lang "topic_unpin_aria"}}"><button class="username unpin_label"></button></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic_pin_tooltip"}}" aria-label="{{lang "topic_pin_aria"}}"><button class="username pin_label"></button></a>{{end}}{{end}} {{if .CurrentUser.Perms.PinTopic}}{{if .Topic.Sticky}}<a class="mod_button" href='/topic/unstick/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' style="font-weight:normal;" title="{{lang "topic.unpin_tooltip"}}" aria-label="{{lang "topic.unpin_aria"}}"><button class="username unpin_label"></button></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="mod_button" style="font-weight:normal;" title="{{lang "topic.pin_tooltip"}}" aria-label="{{lang "topic.pin_aria"}}"><button class="username pin_label"></button></a>{{end}}{{end}}
{{if .CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.Topic.IPAddress}}' style="font-weight:normal;" title="{{lang "topic_ip_tooltip"}}" aria-label="The poster's IP is {{.Topic.IPAddress}}"><button class="username ip_label"></button></a>{{end}} {{if .CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.Topic.IPAddress}}' style="font-weight:normal;" title="{{lang "topic.ip_tooltip"}}" aria-label="The poster's IP is {{.Topic.IPAddress}}"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="mod_button report_item" style="font-weight:normal;" title="{{lang "topic_flag_tooltip"}}" aria-label="{{lang "topic_flag_aria"}}" rel="nofollow"><button class="username flag_label"></button></a> <a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="mod_button report_item" style="font-weight:normal;" title="{{lang "topic.flag_tooltip"}}" aria-label="{{lang "topic.flag_aria"}}" rel="nofollow"><button class="username flag_label"></button></a>
<a class="username hide_on_micro like_count" aria-label="{{lang "topic_like_count_aria"}}">{{.Topic.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="{{lang "topic_like_count_tooltip"}}"></a> <a class="username hide_on_micro like_count" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="{{lang "topic.like_count_tooltip"}}"></a>
{{if .Topic.Tag}}<a class="username hide_on_micro user_tag">{{.Topic.Tag}}</a>{{else}}<a class="username hide_on_micro level" aria-label="{{lang "topic_level_aria"}}">{{.Topic.Level}}</a><a class="username hide_on_micro level_label" style="float:right;" title="{{lang "topic_level_tooltip"}}"></a>{{end}} {{if .Topic.Tag}}<a class="username hide_on_micro user_tag">{{.Topic.Tag}}</a>{{else}}<a class="username hide_on_micro level" aria-label="{{lang "topic.level_aria"}}">{{.Topic.Level}}</a><a class="username hide_on_micro level_label" style="float:right;" title="{{lang "topic.level_tooltip"}}"></a>{{end}}
</span> </span>
</div> </div>
</article> </article>
<div class="rowblock post_container" aria-label="{{lang "topic_current_page_aria"}}" style="overflow: hidden;">{{range .ItemList}}{{if .ActionType}} {{template "topic_posts.html" . }}
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item action_item">
<span class="action_icon" style="font-size: 18px;padding-right: 5px;">{{.ActionIcon}}</span>
<span itemprop="text">{{.ActionType}}</span>
</article>
{{else}}
<article {{scope "post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item {{.ClassName}}" style="background-image: url({{.Avatar}}), url(/static/{{$.Header.Theme.Name}}/post-avatar-bg.jpg);background-position: 0px {{if le .ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;">
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<p class="editable_block user_content" itemprop="text" style="margin:0;padding:0;">{{.ContentHtml}}</p>
<span class="controls{{if .LikeCount}} has_likes{{end}}">
<a href="{{.UserLink}}" class="username real_username" rel="author">{{.CreatedByName}}</a>&nbsp;&nbsp;
{{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic_post_like_tooltip"}}" aria-label="{{lang "topic_post_like_aria"}}" style="color:#202020;"><button class="username like_label remove_like"></button></a>{{else}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic_post_unlike_tooltip"}}" aria-label="{{lang "topic_post_unlike_aria"}}" style="color:#202020;"><button class="username like_label add_like"></button></a>{{end}}{{end}}
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic_post_edit_tooltip"}}" aria-label="{{lang "topic_post_edit_aria"}}"><button class="username edit_item edit_label"></button></a>{{end}}
{{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic_post_delete_tooltip"}}" aria-label="{{lang "topic_post_delete_aria"}}"><button class="username delete_item trash_label"></button></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' style="font-weight:normal;" title="{{lang "topic_post_ip_tooltip"}}" aria-label="The poster's IP is {{.IPAddress}}"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="{{lang "topic_post_flag_tooltip"}}" aria-label="{{lang "topic_post_flag_aria"}}" rel="nofollow"><button class="username report_item flag_label"></button></a>
<a class="username hide_on_micro like_count">{{.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="{{lang "topic_post_like_count_tooltip"}}"></a>
{{if .Tag}}<a class="username hide_on_micro user_tag">{{.Tag}}</a>{{else}}<a class="username hide_on_micro level" aria-label="{{lang "topic_post_level_aria"}}">{{.Level}}</a><a class="username hide_on_micro level_label" style="float:right;" title="{{lang "topic_post_level_tooltip"}}"></a>{{end}}
</span>
</article>
{{end}}{{end}}</div>
{{if .CurrentUser.Perms.CreateReply}} {{if .CurrentUser.Perms.CreateReply}}
{{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/?session={{.CurrentUser.Session}}" method="post"></form> <form id="quick_post_form" enctype="multipart/form-data" action="/reply/create/?session={{.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="reply-content" placeholder="{{lang "topic_reply_content"}}" required></textarea> <textarea id="input_content" form="quick_post_form" name="reply-content" placeholder="{{lang "topic.reply_content"}}" required></textarea>
</div> </div>
</div> </div>
<div class="formrow poll_content_row auto_hide"> <div class="formrow poll_content_row auto_hide">
@ -128,17 +99,17 @@
<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"}}" /> <input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "topic.reply_add_poll_option"}}" />
</div> </div>
</div> </div>
</div> </div>
<div class="formrow quick_button_row"> <div class="formrow quick_button_row">
<div class="formitem"> <div class="formitem">
<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" style="display: none;" />
<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>
</div> </div>

View File

@ -8,17 +8,17 @@
<main> <main>
<div {{scope "topic_title_block"}} class="rowblock rowhead topic_block" aria-label="{{lang "topic_opening_post_aria"}}"> <div {{scope "topic_title_block"}} class="rowblock rowhead topic_block" aria-label="{{lang "topic.opening_post_aria"}}">
<form action='/topic/edit/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' method="post"> <form action='/topic/edit/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' method="post">
<div class="rowitem topic_item{{if .Topic.Sticky}} topic_sticky_head{{else if .Topic.IsClosed}} topic_closed_head{{end}}"> <div class="rowitem topic_item{{if .Topic.Sticky}} topic_sticky_head{{else if .Topic.IsClosed}} topic_closed_head{{end}}">
<h1 class='topic_name hide_on_edit' title='{{.Topic.Title}}'>{{.Topic.Title}}</h1> <h1 class='topic_name hide_on_edit' title='{{.Topic.Title}}'>{{.Topic.Title}}</h1>
{{/** TODO: Inline this CSS **/}} {{/** TODO: Inline this CSS **/}}
{{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit' title='{{lang "status_closed_tooltip"}}' aria-label='{{lang "topic_status_closed_aria"}}' style="font-weight:normal;float: right;position:relative;top:-5px;">&#x1F512;&#xFE0E</span>{{end}} {{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit' title='{{lang "status.closed_tooltip"}}' aria-label='{{lang "topic.status_closed_aria"}}' style="font-weight:normal;float: right;position:relative;top:-5px;">&#x1F512;&#xFE0E</span>{{end}}
{{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}} {{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}}
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
{{if .CurrentUser.Perms.EditTopic}} {{if .CurrentUser.Perms.EditTopic}}
<input class='show_on_edit topic_name_input' name="topic_name" value='{{.Topic.Title}}' type="text" aria-label="{{lang "topic_title_input_aria"}}" /> <input class='show_on_edit topic_name_input' name="topic_name" value='{{.Topic.Title}}' type="text" aria-label="{{lang "topic.title_input_aria"}}" />
<button name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic_update_button"}}</button> <button name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic.update_button"}}</button>
{{end}} {{end}}
{{end}} {{end}}
</div> </div>
@ -29,10 +29,10 @@
{{if .Poll.ID}} {{if .Poll.ID}}
<form id="poll_{{.Poll.ID}}_form" action="/poll/vote/{{.Poll.ID}}?session={{.CurrentUser.Session}}" method="post"></form> <form id="poll_{{.Poll.ID}}_form" action="/poll/vote/{{.Poll.ID}}?session={{.CurrentUser.Session}}" method="post"></form>
<article class="rowitem passive deletable_block editable_parent post_item poll_item top_post hide_on_edit"> <article class="rowitem passive deletable_block editable_parent post_item poll_item top_post hide_on_edit">
<div class="userinfo" aria-label="{{lang "topic_userinfo_aria"}}"> <div class="userinfo" aria-label="{{lang "topic.userinfo_aria"}}">
<div class="avatar_item" style="background-image: url({{.Topic.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div> <div class="avatar_item" style="background-image: url({{.Topic.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
<a href="{{.Topic.UserLink}}" class="the_name" rel="author">{{.Topic.CreatedByName}}</a> <a href="{{.Topic.UserLink}}" class="the_name" rel="author">{{.Topic.CreatedByName}}</a>
{{if .Topic.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Topic.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic_level_prefix"}}{{.Topic.Level}}</div><div class="tag_post"></div></div>{{end}} {{if .Topic.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Topic.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic.level_prefix"}}{{.Topic.Level}}</div><div class="tag_post"></div></div>{{end}}
</div> </div>
<div id="poll_voter_{{.Poll.ID}}" class="content_container poll_voter"> <div id="poll_voter_{{.Poll.ID}}" class="content_container poll_voter">
<div class="topic_content user_content"> <div class="topic_content user_content">
@ -46,9 +46,9 @@
</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> </div>
@ -57,91 +57,56 @@
</div> </div>
</article> </article>
{{end}} {{end}}
<article {{scope "opening_post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item top_post" 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" aria-label="{{lang "topic.opening_post_aria"}}">
<div class="userinfo" aria-label="{{lang "topic_userinfo_aria"}}"> <div class="userinfo" aria-label="{{lang "topic.userinfo_aria"}}">
<div class="avatar_item" style="background-image: url({{.Topic.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div> <div class="avatar_item" style="background-image: url({{.Topic.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
<a href="{{.Topic.UserLink}}" class="the_name" rel="author">{{.Topic.CreatedByName}}</a> <a href="{{.Topic.UserLink}}" class="the_name" rel="author">{{.Topic.CreatedByName}}</a>
{{if .Topic.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Topic.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic_level_prefix"}}{{.Topic.Level}}</div><div class="tag_post"></div></div>{{end}} {{if .Topic.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Topic.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic.level_prefix"}}{{.Topic.Level}}</div><div class="tag_post"></div></div>{{end}}
</div> </div>
<div class="content_container"> <div class="content_container">
<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>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea> <textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
<div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}"> <div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}">
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Loggedin}}
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="action_button like_item {{if .Topic.Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic_like_aria"}}" data-action="like"></a>{{end}} {{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="action_button like_item {{if .Topic.Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic.like_aria"}}" data-action="like"></a>{{end}}
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
{{if .CurrentUser.Perms.EditTopic}}<a href="/topic/edit/{{.Topic.ID}}" class="action_button open_edit" aria-label="{{lang "topic_edit_aria"}}" data-action="edit"></a>{{end}} {{if .CurrentUser.Perms.EditTopic}}<a href="/topic/edit/{{.Topic.ID}}" class="action_button open_edit" aria-label="{{lang "topic.edit_aria"}}" data-action="edit"></a>{{end}}
{{end}} {{end}}
{{if .CurrentUser.Perms.DeleteTopic}}<a href="/topic/delete/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="action_button delete_item" aria-label="{{lang "topic_delete_aria"}}" data-action="delete"></a>{{end}} {{if .CurrentUser.Perms.DeleteTopic}}<a href="/topic/delete/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="action_button delete_item" aria-label="{{lang "topic.delete_aria"}}" data-action="delete"></a>{{end}}
{{if .CurrentUser.Perms.CloseTopic}} {{if .CurrentUser.Perms.CloseTopic}}
{{if .Topic.IsClosed}}<a href='/topic/unlock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="action_button unlock_item" data-action="unlock" aria-label="{{lang "topic_unlock_aria"}}"></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="action_button lock_item" data-action="lock" aria-label="{{lang "topic_lock_aria"}}"></a>{{end}}{{end}} {{if .Topic.IsClosed}}<a href='/topic/unlock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="action_button unlock_item" data-action="unlock" aria-label="{{lang "topic.unlock_aria"}}"></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="action_button lock_item" data-action="lock" aria-label="{{lang "topic.lock_aria"}}"></a>{{end}}{{end}}
{{if .CurrentUser.Perms.PinTopic}} {{if .CurrentUser.Perms.PinTopic}}
{{if .Topic.Sticky}}<a href='/topic/unstick/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="action_button unpin_item" data-action="unpin" aria-label="{{lang "topic_unpin_aria"}}"></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="action_button pin_item" data-action="pin" aria-label="{{lang "topic_pin_aria"}}"></a>{{end}}{{end}} {{if .Topic.Sticky}}<a href='/topic/unstick/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="action_button unpin_item" data-action="unpin" aria-label="{{lang "topic.unpin_aria"}}"></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}' class="action_button pin_item" data-action="pin" aria-label="{{lang "topic.pin_aria"}}"></a>{{end}}{{end}}
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IPAddress}}" 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={{.Topic.IPAddress}}" 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/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="action_button report_item" aria-label="{{lang "topic_report_aria"}}" data-action="report"></a> <a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" 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 class="action_button_right"> <div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic_like_count_aria"}}">{{.Topic.LikeCount}}</a> <a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile">{{.Topic.RelativeCreatedAt}}</a> <a class="action_button created_at hide_on_mobile">{{.Topic.RelativeCreatedAt}}</a>
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IPAddress}}" title="{{lang "topic_ip_full_tooltip"}}" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.Topic.IPAddress}}</a>{{end}} {{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IPAddress}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.Topic.IPAddress}}</a>{{end}}
</div> </div>
</div> </div>
</div><div style="clear:both;"></div> </div><div style="clear:both;"></div>
</article> </article>
{{template "topic_alt_posts.html" . }}
{{range .ItemList}} </div>
<article {{scope "post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item {{if .ActionType}}action_item{{end}}">
<div class="userinfo" aria-label="{{lang "topic_userinfo_aria"}}">
<div class="avatar_item" style="background-image: url({{.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
<a href="{{.UserLink}}" class="the_name" rel="author">{{.CreatedByName}}</a>
{{if .Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic_level_prefix"}}{{.Level}}</div><div class="tag_post"></div></div>{{end}}
</div>
<div class="content_container" {{if .ActionType}}style="margin-left: 0px;"{{end}}>
{{if .ActionType}}
<span class="action_icon" style="font-size: 18px;padding-right: 5px;" aria-hidden="true">{{.ActionIcon}}</span>
<span itemprop="text">{{.ActionType}}</span>
{{else}}
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<div class="editable_block user_content" itemprop="text">{{.ContentHtml}}</div>
<div class="controls button_container{{if .LikeCount}} has_likes{{end}}">
{{if $.CurrentUser.Loggedin}}
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button like_item {{if .Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic_post_like_aria"}}" data-action="like"></a>{{end}}
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button edit_item" aria-label="{{lang "topic_post_edit_aria"}}" data-action="edit"></a>{{end}}
{{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.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={{.IPAddress}}" 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}}?session={{$.CurrentUser.Session}}&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 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">{{.RelativeCreatedAt}}</a>
{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}
</div>
</div>
{{end}}
</div>
<div style="clear:both;"></div>
</article>
{{end}}</div>
{{if .CurrentUser.Perms.CreateReply}} {{if .CurrentUser.Perms.CreateReply}}
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
<div class="rowblock topic_reply_container"> <div class="rowblock topic_reply_container">
<div class="userinfo" aria-label="{{lang "topic_your_information"}}"> <div class="userinfo" aria-label="{{lang "topic.your_information"}}">
<div class="avatar_item" style="background-image: url({{.CurrentUser.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div> <div class="avatar_item" style="background-image: url({{.CurrentUser.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
<a href="{{.CurrentUser.Link}}" class="the_name" rel="author">{{.CurrentUser.Name}}</a> <a href="{{.CurrentUser.Link}}" class="the_name" rel="author">{{.CurrentUser.Name}}</a>
{{if .CurrentUser.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.CurrentUser.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic_level_prefix"}}{{.CurrentUser.Level}}</div><div class="tag_post"></div></div>{{end}} {{if .CurrentUser.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.CurrentUser.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic.level_prefix"}}{{.CurrentUser.Level}}</div><div class="tag_post"></div></div>{{end}}
</div> </div>
<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/?session={{.CurrentUser.Session}}" method="post"></form> <form id="quick_post_form" enctype="multipart/form-data" action="/reply/create/?session={{.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="reply-content" placeholder="{{lang "topic_reply_content_alt"}}" required></textarea> <textarea id="input_content" form="quick_post_form" name="reply-content" placeholder="{{lang "topic.reply_content_alt"}}" required></textarea>
</div> </div>
</div> </div>
<div class="formrow poll_content_row auto_hide"> <div class="formrow poll_content_row auto_hide">
@ -149,17 +114,17 @@
<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"}}" /> <input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "topic.reply_add_poll_option"}}" />
</div> </div>
</div> </div>
</div> </div>
<div class="formrow quick_button_row"> <div class="formrow quick_button_row">
<div class="formitem"> <div class="formitem">
<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" style="display: none;" />
<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>
</div> </div>

View File

@ -0,0 +1,34 @@
{{range .ItemList}}<article {{scope "post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item {{if .ActionType}}action_item{{end}}">
<div class="userinfo" aria-label="{{lang "topic.userinfo_aria"}}">
<div class="avatar_item" style="background-image: url({{.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
<a href="{{.UserLink}}" class="the_name" rel="author">{{.CreatedByName}}</a>
{{if .Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic.level_prefix"}}{{.Level}}</div><div class="tag_post"></div></div>{{end}}
</div>
<div class="content_container" {{if .ActionType}}style="margin-left: 0px;"{{end}}>
{{if .ActionType}}
<span class="action_icon" style="font-size: 18px;padding-right: 5px;" aria-hidden="true">{{.ActionIcon}}</span>
<span itemprop="text">{{.ActionType}}</span>
{{else}}
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<div class="editable_block user_content" itemprop="text">{{.ContentHtml}}</div>
<div class="controls button_container{{if .LikeCount}} has_likes{{end}}">
{{if $.CurrentUser.Loggedin}}
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button like_item {{if .Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic.post_like_aria"}}" data-action="like"></a>{{end}}
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button edit_item" aria-label="{{lang "topic.post_edit_aria"}}" data-action="edit"></a>{{end}}
{{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.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={{.IPAddress}}" 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}}?session={{$.CurrentUser.Session}}&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 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">{{.RelativeCreatedAt}}</a>
{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}
</div>
</div>
{{end}}
</div>
<div style="clear:both;"></div>
</article>{{end}}

View File

@ -0,0 +1,32 @@
<div class="rowblock post_container" aria-label="{{lang "topic.current_page_aria"}}" style="overflow: hidden;">{{range .ItemList}}
{{if .ActionType}}
<article {{scope "post_action"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item action_item">
<span class="action_icon" style="font-size: 18px;padding-right: 5px;">{{.ActionIcon}}</span>
<span itemprop="text">{{.ActionType}}</span>
</article>
{{else}}
<article {{scope "post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item {{.ClassName}}" style="background-image: url({{.Avatar}}), url(/static/{{$.Header.Theme.Name}}/post-avatar-bg.jpg);background-position: 0px {{if le .ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;">
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<p class="editable_block user_content" itemprop="text" style="margin:0;padding:0;">{{.ContentHtml}}</p>
<span class="controls{{if .LikeCount}} has_likes{{end}}">
<a href="{{.UserLink}}" class="username real_username" rel="author">{{.CreatedByName}}</a>&nbsp;&nbsp;
{{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_like_tooltip"}}" aria-label="{{lang "topic.post_like_aria"}}" style="color:#202020;"><button class="username like_label remove_like"></button></a>{{else}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_unlike_tooltip"}}" aria-label="{{lang "topic.post_unlike_aria"}}" style="color:#202020;"><button class="username like_label add_like"></button></a>{{end}}{{end}}
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_edit_tooltip"}}" aria-label="{{lang "topic.post_edit_aria"}}"><button class="username edit_item edit_label"></button></a>{{end}}
{{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic.post_delete_tooltip"}}" aria-label="{{lang "topic.post_delete_aria"}}"><button class="username delete_item trash_label"></button></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' style="font-weight:normal;" title="{{lang "topic.post_ip_tooltip"}}" aria-label="The poster's IP is {{.IPAddress}}"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="{{lang "topic.post_flag_tooltip"}}" aria-label="{{lang "topic.post_flag_aria"}}" rel="nofollow"><button class="username report_item flag_label"></button></a>
<a class="username hide_on_micro like_count">{{.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="{{lang "topic.post_like_count_tooltip"}}"></a>
{{if .Tag}}<a class="username hide_on_micro user_tag">{{.Tag}}</a>{{else}}<a class="username hide_on_micro level" aria-label="{{lang "topic.post_level_aria"}}">{{.Level}}</a><a class="username hide_on_micro level_label" style="float:right;" title="{{lang "topic.post_level_tooltip"}}"></a>{{end}}
</span>
</article>
{{end}}
{{end}}</div>

View File

@ -55,10 +55,10 @@
</div> </div>
</form> </form>
</div> </div>
<div class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="{{lang "quick_topic_aria"}}"> <div class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="{{lang "quick_topic.aria"}}">
<form name="topic_create_form_form" id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form> <form name="topic_create_form_form" id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form>
<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" />
<img class="little_row_avatar" src="{{.CurrentUser.Avatar}}" height="64" alt="{{lang "quick_topic_avatar_alt"}}" title="{{lang "quick_topic_avatar_tooltip"}}" /> <img class="little_row_avatar" src="{{.CurrentUser.Avatar}}" height="64" alt="{{lang "quick_topic.avatar_alt"}}" title="{{lang "quick_topic.avatar_tooltip"}}" />
<div class="main_form"> <div class="main_form">
<div class="topic_meta"> <div class="topic_meta">
<div class="formrow topic_board_row real_first_child"> <div class="formrow topic_board_row real_first_child">
@ -68,13 +68,13 @@
</div> </div>
<div class="formrow topic_name_row"> <div class="formrow topic_name_row">
<div class="formitem"> <div class="formitem">
<input form="quick_post_form" name="topic-name" placeholder="{{lang "quick_topic_whatsup"}}" required> <input form="quick_post_form" name="topic-name" placeholder="{{lang "quick_topic.whatsup"}}" required>
</div> </div>
</div> </div>
</div> </div>
<div class="formrow topic_content_row"> <div class="formrow topic_content_row">
<div class="formitem"> <div class="formitem">
<textarea form="quick_post_form" id="input_content" name="topic-content" placeholder="{{lang "quick_topic_content_placeholder"}}" required></textarea> <textarea form="quick_post_form" id="input_content" name="topic-content" placeholder="{{lang "quick_topic.content_placeholder"}}" required></textarea>
</div> </div>
</div> </div>
<div class="formrow poll_content_row auto_hide"> <div class="formrow poll_content_row auto_hide">
@ -82,19 +82,19 @@
<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 "quick_topic_add_poll_option"}}" /> <input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "quick_topic.add_poll_option"}}" />
</div> </div>
</div> </div>
</div> </div>
<div class="formrow quick_button_row"> <div class="formrow quick_button_row">
<div class="formitem"> <div class="formitem">
<button form="quick_post_form" class="formbutton">{{lang "quick_topic_create_topic_button"}}</button> <button form="quick_post_form" class="formbutton">{{lang "quick_topic.create_topic_button"}}</button>
<button form="quick_post_form" class="formbutton" id="add_poll_button">{{lang "quick_topic_add_poll_button"}}</button> <button form="quick_post_form" class="formbutton" id="add_poll_button">{{lang "quick_topic.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" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">{{lang "quick_topic_add_file_button"}}</label> <label for="upload_files" class="formbutton add_file_button">{{lang "quick_topic.add_file_button"}}</label>
<div id="upload_file_dock"></div>{{end}} <div id="upload_file_dock"></div>{{end}}
<button class="formbutton close_form">{{lang "quick_topic_cancel_button"}}</button> <button class="formbutton close_form">{{lang "quick_topic.cancel_button"}}</button>
</div> </div>
</div> </div>
</div> </div>
@ -102,39 +102,7 @@
{{end}} {{end}}
{{end}} {{end}}
<div id="topic_list" class="rowblock topic_list" aria-label="{{lang "topics_list_aria"}}"> <div id="topic_list" class="rowblock topic_list" aria-label="{{lang "topics_list_aria"}}">
{{range .TopicList}}<div class="topic_row" data-tid="{{.ID}}"> {{range .TopicList}}{{template "topics_topic.html" . }}{{else}}<div class="rowitem passive rowmsg">{{lang "topics_no_topics"}}{{if .CurrentUser.Perms.CreateTopic}} <a href="/topics/create/">{{lang "topics_start_one"}}</a>{{end}}</div>{{end}}
<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}">
<span class="selector"></span>
<a href="{{.Creator.Link}}"><img src="{{.Creator.Avatar}}" height="64" alt="{{.Creator.Name}}'s Avatar" title="{{.Creator.Name}}'s Avatar" /></a>
<span class="topic_inner_left">
<a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a> {{if .ForumName}}<a class="rowsmall parent_forum" href="{{.ForumLink}}" title="{{.ForumName}}">{{.ForumName}}</a>{{end}}
<br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a>
{{/** TODO: Avoid the double '|' when both .IsClosed and .Sticky are set to true. We could probably do this with CSS **/}}
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status_closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span>
{{/** TODO: Phase this out of Cosora and remove it **/}}
<div class="topic_inner_right rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div>
<div class="topic_middle">
<div class="topic_middle_inside rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}} topic_closed{{end}}">
<div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.Avatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span>
</span>
</div>
</div>
</div>{{else}}<div class="rowitem passive rowmsg">{{lang "topics_no_topics"}}{{if .CurrentUser.Perms.CreateTopic}} <a href="/topics/create/">{{lang "topics_start_one"}}</a>{{end}}</div>{{end}}
</div> </div>
{{if gt .LastPage 1}} {{if gt .LastPage 1}}

View File

@ -0,0 +1,33 @@
<div class="topic_row" data-tid="{{.ID}}">
<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}">
<span class="selector"></span>
<a href="{{.Creator.Link}}"><img src="{{.Creator.Avatar}}" height="64" alt="{{.Creator.Name}}'s Avatar" title="{{.Creator.Name}}'s Avatar" /></a>
<span class="topic_inner_left">
<a class="rowtopic" href="{{.Link}}" itemprop="itemListElement" title="{{.Title}}"><span>{{.Title}}</span></a> {{if .ForumName}}<a class="rowsmall parent_forum" href="{{.ForumLink}}" title="{{.ForumName}}">{{.ForumName}}</a>{{end}}
<br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a>
{{/** TODO: Avoid the double '|' when both .IsClosed and .Sticky are set to true. We could probably do this with CSS **/}}
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status.closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status.pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span>
{{/** TODO: Phase this out of Cosora and remove it **/}}
<div class="topic_inner_right rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div>
<div class="topic_middle">
<div class="topic_middle_inside rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}} topic_closed{{end}}">
<div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.Avatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span>
</span>
</div>
</div>
</div>

View File

@ -15,15 +15,6 @@
height: 184px; height: 184px;
position: relative; position: relative;
} }
#dash_saved {
text-transform: uppercase;
font-size: 11px;
color: green;
position: absolute;
right: 8px;
top: 8px;
display: none;
}
.dash_security, .account_soon { .dash_security, .account_soon {
text-transform: uppercase; text-transform: uppercase;
font-size: 11px; font-size: 11px;

View File

@ -694,6 +694,13 @@ textarea {
border: 1px solid var(--element-border-color); border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color); border-bottom: 2px solid var(--element-border-color);
} }
.topic_row.new_item .topic_left, .topic_row.new_item .topic_right {
background-color: rgb(239, 255, 255);
border: 1px solid rgb(187, 217, 217);
border-bottom: 2px solid rgb(187, 217, 217);
border-left: none;
border-right: none;
}
.topic_middle { .topic_middle {
display: none; display: none;
} }
@ -1008,7 +1015,7 @@ textarea {
display: block; display: block;
} }
.like_count:after { .like_count:after {
content: "{{index .Phrases "topic_like_count_suffix"}}"; content: "{{index .Phrases "topic.like_count_suffix"}}";
margin-right: 6px; margin-right: 6px;
} }
@ -1036,31 +1043,31 @@ textarea {
} }
.add_like:before, .remove_like:before { .add_like:before, .remove_like:before {
content: "{{index .Phrases "topic_plus_one"}}"; content: "{{index .Phrases "topic.plus_one"}}";
} }
.button_container .open_edit:after, .edit_item:after{ .button_container .open_edit:after, .edit_item:after{
content: "{{index .Phrases "topic_edit_button_text"}}"; content: "{{index .Phrases "topic.edit_button_text"}}";
} }
.delete_item:after { .delete_item:after {
content: "{{index .Phrases "topic_delete_button_text"}}"; content: "{{index .Phrases "topic.delete_button_text"}}";
} }
.ip_item_button:after { .ip_item_button:after {
content: "{{index .Phrases "topic_ip_button_text"}}"; content: "{{index .Phrases "topic.ip_button_text"}}";
} }
.lock_item:after { .lock_item:after {
content: "{{index .Phrases "topic_lock_button_text"}}"; content: "{{index .Phrases "topic.lock_button_text"}}";
} }
.unlock_item:after { .unlock_item:after {
content: "{{index .Phrases "topic_unlock_button_text"}}"; content: "{{index .Phrases "topic.unlock_button_text"}}";
} }
.pin_item:after { .pin_item:after {
content: "{{index .Phrases "topic_pin_button_text"}}"; content: "{{index .Phrases "topic.pin_button_text"}}";
} }
.unpin_item:after { .unpin_item:after {
content: "{{index .Phrases "topic_unpin_button_text"}}"; content: "{{index .Phrases "topic.unpin_button_text"}}";
} }
.report_item:after { .report_item:after {
content: "{{index .Phrases "topic_report_button_text"}}"; content: "{{index .Phrases "topic.report_button_text"}}";
} }
#ip_search_container .rowlist .rowitem { #ip_search_container .rowlist .rowitem {
@ -1245,29 +1252,6 @@ textarea {
border-top: 1px solid var(--element-border-color) !important; border-top: 1px solid var(--element-border-color) !important;
} }
/*.colstack_item .formrow {
display: flex;
}
.colstack_right .formrow {
padding-left: 16px;
padding-right: 16px;
padding-bottom: 4px;
border-right: 1px solid var(--element-border-color);
}
.colstack_right .formrow:first-child {
padding-top: 16px;
}
.colstack_right .formrow .formlabel {
padding-top: 5px;
}
.colstack_right .formrow:last-child {
padding-bottom: 16px;
}
.colstack_item:not(#profile_right_lane) .formrow .formlabel {
width: 40%;
margin-right: 12px;
white-space: nowrap;
}*/
.formitem:only-child { .formitem:only-child {
width: 100%; width: 100%;
display: flex; display: flex;
@ -1748,7 +1732,7 @@ textarea {
content: ""; content: "";
} }
.like_count:before { .like_count:before {
content: "{{index .Phrases "topic_plus"}}"; content: "{{index .Phrases "topic.plus"}}";
font-weight: normal; font-weight: normal;
} }
.created_at { .created_at {

View File

@ -13,9 +13,6 @@
width: 240px; width: 240px;
position: relative; position: relative;
} }
#dash_saved {
display: none;
}
#dash_username { #dash_username {
display: flex; display: flex;
} }

View File

@ -4,22 +4,6 @@
--third-dark-background: #333333; --third-dark-background: #333333;
} }
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 400;
src: url("../fontawesome-5.0.13/webfonts/fa-regular-400.eot");
src: url("../fontawesome-5.0.13/webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../fontawesome-5.0.13/webfonts/fa-regular-400.woff2") format("woff2"), url("../fontawesome-5.0.13/webfonts/fa-regular-400.woff") format("woff"), url("../fontawesome-5.0.13/webfonts/fa-regular-400.ttf") format("truetype"), url("../fontawesome-5.0.13/webfonts/fa-regular-400.svg#fontawesome") format("svg");
}
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 900;
src: url("../fontawesome-5.0.13/webfonts/fa-solid-900.eot");
src: url("../fontawesome-5.0.13/webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../fontawesome-5.0.13/webfonts/fa-solid-900.woff2") format("woff2"), url("../fontawesome-5.0.13/webfonts/fa-solid-900.woff") format("woff"), url("../fontawesome-5.0.13/webfonts/fa-solid-900.ttf") format("truetype"), url("../fontawesome-5.0.13/webfonts/fa-solid-900.svg#fontawesome") format("svg");
}
* { * {
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -1,9 +1,6 @@
#account_dashboard .colstack_right .coldyn_block { #account_dashboard .colstack_right .coldyn_block {
display: flex; display: flex;
} }
#dash_saved {
display: none;
}
#dash_left { #dash_left {
padding: 18px; padding: 18px;
padding-right: 0px; padding-right: 0px;

View File

@ -290,34 +290,34 @@ a {
} }
.like_label:before { .like_label:before {
content: "{{index .Phrases "topic_plus_one"}}"; content: "{{index .Phrases "topic.plus_one"}}";
} }
.edit_label:before { .edit_label:before {
content: "{{index .Phrases "topic_edit_button_text"}}"; content: "{{index .Phrases "topic.edit_button_text"}}";
} }
.trash_label:before { .trash_label:before {
content: "{{index .Phrases "topic_delete_button_text"}}"; content: "{{index .Phrases "topic.delete_button_text"}}";
} }
.pin_label:before { .pin_label:before {
content: "{{index .Phrases "topic_pin_button_text"}}"; content: "{{index .Phrases "topic.pin_button_text"}}";
} }
.lock_label:before { .lock_label:before {
content: "{{index .Phrases "topic_lock_button_text"}}"; content: "{{index .Phrases "topic.lock_button_text"}}";
} }
.unlock_label:before { .unlock_label:before {
content: "{{index .Phrases "topic_unlock_button_text"}}"; content: "{{index .Phrases "topic.unlock_button_text"}}";
} }
.unpin_label:before { .unpin_label:before {
content: "{{index .Phrases "topic_unpin_button_text"}}"; content: "{{index .Phrases "topic.unpin_button_text"}}";
} }
.ip_label:before { .ip_label:before {
content: "{{index .Phrases "topic_ip_button_text"}}"; content: "{{index .Phrases "topic.ip_button_text"}}";
} }
.flag_label:before { .flag_label:before {
content: "{{index .Phrases "topic_flag_button_text"}}"; content: "{{index .Phrases "topic.flag_button_text"}}";
} }
.level_label:before { .level_label:before {
content: "{{index .Phrases "topic_level"}}"; content: "{{index .Phrases "topic.level"}}";
} }
.like_count_label, .like_count { .like_count_label, .like_count {
@ -896,7 +896,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
font-weight: normal; font-weight: normal;
} }
#profile_left_pane .report_item:after { #profile_left_pane .report_item:after {
content: "{{index .Phrases "topic_report_button_text"}}"; content: "{{index .Phrases "topic.report_button_text"}}";
} }
#profile_left_lane .profileName { #profile_left_lane .profileName {
font-size: 18px; font-size: 18px;

View File

@ -1,4 +1,4 @@
.sidebar, #dash_saved { .sidebar {
display: none; display: none;
} }
#account_dashboard .colstack_right .coldyn_block { #account_dashboard .colstack_right .coldyn_block {

View File

@ -715,7 +715,7 @@ button.username {
content: "😀"; content: "😀";
} }
.like_count:after { .like_count:after {
content: "{{index .Phrases "topic_gap_up"}}"; content: "{{index .Phrases "topic.gap_up"}}";
} }
.edit_label:before { .edit_label:before {
content: "🖊️"; content: "🖊️";
@ -763,7 +763,7 @@ button.username {
font-weight: normal; font-weight: normal;
} }
#profile_left_pane .report_item:after { #profile_left_pane .report_item:after {
content: "{{index .Phrases "topic_report_button_text"}}"; content: "{{index .Phrases "topic.report_button_text"}}";
} }
#profile_right_lane { #profile_right_lane {
width: calc(100% - 230px); width: calc(100% - 230px);
@ -959,28 +959,28 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
} }
.add_like:before, .remove_like:before { .add_like:before, .remove_like:before {
content: "{{index .Phrases "topic_plus_one"}}"; content: "{{index .Phrases "topic.plus_one"}}";
} }
.button_container .open_edit:after, .edit_item:after { .button_container .open_edit:after, .edit_item:after {
content: "{{index .Phrases "topic_edit_button_text"}}"; content: "{{index .Phrases "topic.edit_button_text"}}";
} }
.delete_item:after { .delete_item:after {
content: "{{index .Phrases "topic_delete_button_text"}}"; content: "{{index .Phrases "topic.delete_button_text"}}";
} }
.lock_item:after { .lock_item:after {
content: "{{index .Phrases "topic_lock_button_text"}}"; content: "{{index .Phrases "topic.lock_button_text"}}";
} }
.unlock_item:after { .unlock_item:after {
content: "{{index .Phrases "topic_unlock_button_text"}}"; content: "{{index .Phrases "topic.unlock_button_text"}}";
} }
.pin_item:after { .pin_item:after {
content: "{{index .Phrases "topic_pin_button_text"}}"; content: "{{index .Phrases "topic.pin_button_text"}}";
} }
.unpin_item:after { .unpin_item:after {
content: "{{index .Phrases "topic_unpin_button_text"}}"; content: "{{index .Phrases "topic.unpin_button_text"}}";
} }
.report_item:after { .report_item:after {
content: "{{index .Phrases "topic_report_button_text"}}"; content: "{{index .Phrases "topic.report_button_text"}}";
} }
.footer { .footer {

View File

@ -1,4 +1,4 @@
.sidebar, #dash_saved { .sidebar {
display: none; display: none;
} }

View File

@ -895,7 +895,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
font-size: 18px; font-size: 18px;
} }
#profile_left_lane .report_item:after { #profile_left_lane .report_item:after {
content: "{{index .Phrases "topic_report_button_text"}}"; content: "{{index .Phrases "topic.report_button_text"}}";
} }
#profile_right_lane { #profile_right_lane {
width: calc(100% - 245px); width: calc(100% - 245px);