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:
parent
163d417831
commit
7be011a30d
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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++
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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 + "]}"
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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/
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
765
gen_router.go
765
gen_router.go
File diff suppressed because it is too large
Load Diff
@ -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":"<",
|
"paginator_less_than":"<",
|
||||||
"paginator_greater_than":">",
|
"paginator_greater_than":">",
|
||||||
|
3
main.go
3
main.go
@ -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()
|
||||||
//}
|
//}
|
||||||
|
165
public/global.js
165
public/global.js
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"),
|
||||||
|
91
routes.go
91
routes.go
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 = ""
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
4
run.bat
4
run.bat
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}}" />
|
||||||
|
@ -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"}}"> | 🔒︎</span>{{end}}
|
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status.closed_tooltip"}}"> | 🔒︎</span>{{end}}
|
||||||
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | 📍︎</span>{{end}}
|
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status.pinned_tooltip"}}"> | 📍︎</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">
|
||||||
|
@ -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>
|
||||||
|
@ -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"}}'>🔒︎</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"}}'>🔒︎</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>
|
<a href="{{.Topic.UserLink}}" class="username real_username" rel="author">{{.Topic.CreatedByName}}</a>
|
||||||
{{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>
|
|
||||||
{{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>
|
||||||
|
@ -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;">🔒︎</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;">🔒︎</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;"> </div>
|
<div class="avatar_item" style="background-image: url({{.Topic.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;"> </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;"> </div>
|
<div class="avatar_item" style="background-image: url({{.Topic.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;"> </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;"> </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;"> </div>
|
<div class="avatar_item" style="background-image: url({{.CurrentUser.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;"> </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>
|
||||||
|
34
templates/topic_alt_posts.html
Normal file
34
templates/topic_alt_posts.html
Normal 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;"> </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}}
|
32
templates/topic_posts.html
Normal file
32
templates/topic_posts.html
Normal 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>
|
||||||
|
{{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>
|
@ -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"}}"> | 🔒︎</span>{{end}}
|
|
||||||
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | 📍︎</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}}
|
||||||
|
33
templates/topics_topic.html
Normal file
33
templates/topics_topic.html
Normal 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"}}"> | 🔒︎</span>{{end}}
|
||||||
|
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status.pinned_tooltip"}}"> | 📍︎</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>
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -13,9 +13,6 @@
|
|||||||
width: 240px;
|
width: 240px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
#dash_saved {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#dash_username {
|
#dash_username {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.sidebar, #dash_saved {
|
.sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#account_dashboard .colstack_right .coldyn_block {
|
#account_dashboard .colstack_right .coldyn_block {
|
||||||
|
@ -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 {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.sidebar, #dash_saved {
|
.sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user