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
|
||||
del "template_*.go"
|
||||
del "gen_*.go"
|
||||
del "tmpl_client/template_*.go"
|
||||
cd tmpl_client
|
||||
del "template_*.go"
|
||||
cd ..
|
||||
del "gosora.exe"
|
||||
|
||||
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{} {
|
||||
hook := Vhooks[name]
|
||||
if hook != nil {
|
||||
|
|
|
@ -3,7 +3,9 @@ package common
|
|||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
//"errors"
|
||||
|
@ -40,6 +42,9 @@ func (list SFileList) JSTmplInit() error {
|
|||
DebugLog("Initialising the client side templates")
|
||||
var fragMap = make(map[string][][]byte)
|
||||
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)
|
||||
return filepath.Walk("./tmpl_client", func(path string, f os.FileInfo, err error) error {
|
||||
if f.IsDir() {
|
||||
|
@ -56,18 +61,27 @@ func (list SFileList) JSTmplInit() error {
|
|||
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 {
|
||||
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 {
|
||||
return errors.New("no template function found")
|
||||
}
|
||||
data = data[startIndex-len([]byte("func Template")):]
|
||||
data = replace(data, "func ", "function ")
|
||||
data = replace(data, " error {\n", " {\nlet out = \"\"\n")
|
||||
spaceIndex, hasSpace := skipUntilIfExists(data, 10, ' ')
|
||||
spaceIndex, hasSpace := skipUntilIfExists(data, funcIndex, ' ')
|
||||
if !hasSpace {
|
||||
return errors.New("no spaces found after the template function name")
|
||||
}
|
||||
|
@ -75,13 +89,16 @@ func (list SFileList) JSTmplInit() error {
|
|||
if !hasBrace {
|
||||
return errors.New("no right brace found after the template function name")
|
||||
}
|
||||
//fmt.Println("spaceIndex: ", spaceIndex)
|
||||
//fmt.Println("endBrace: ", endBrace)
|
||||
//fmt.Println("string(data[spaceIndex:endBrace]): ", string(data[spaceIndex:endBrace]))
|
||||
fmt.Println("spaceIndex: ", spaceIndex)
|
||||
fmt.Println("endBrace: ", endBrace)
|
||||
fmt.Println("string(data[spaceIndex:endBrace]): ", string(data[spaceIndex:endBrace]))
|
||||
|
||||
preLen := len(data)
|
||||
data = replace(data, string(data[spaceIndex:endBrace]), "")
|
||||
data = replace(data, "))\n", "\n")
|
||||
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) {
|
||||
out = "["
|
||||
|
@ -99,6 +116,9 @@ func (list SFileList) JSTmplInit() error {
|
|||
var each = func(phrase string, handle func(index int)) {
|
||||
//fmt.Println("find each '" + phrase + "'")
|
||||
var index = endBrace
|
||||
if index < 0 {
|
||||
panic("index under zero: " + strconv.Itoa(index))
|
||||
}
|
||||
var foundIt bool
|
||||
for {
|
||||
//fmt.Println("in index: ", index)
|
||||
|
@ -145,9 +165,24 @@ func (list SFileList) JSTmplInit() error {
|
|||
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(", "out += ")
|
||||
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, "return nil", "return out")
|
||||
data = replace(data, " )", ")")
|
||||
|
@ -155,19 +190,25 @@ func (list SFileList) JSTmplInit() error {
|
|||
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, `,
|
||||
});
|
||||
}`, "\n\t];")
|
||||
data = replace(data, `=
|
||||
}`, "= []")
|
||||
|
||||
path = strings.TrimPrefix(path, "tmpl_client/")
|
||||
tmplName := strings.TrimSuffix(path, ".go")
|
||||
fragset, ok := fragMap[strings.TrimPrefix(tmplName, "template_")]
|
||||
fragset, ok := fragMap[shortName]
|
||||
if !ok {
|
||||
DebugLog("tmplName: ", tmplName)
|
||||
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 {
|
||||
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 = replace(data, "\n;", "\n")
|
||||
|
|
|
@ -174,20 +174,30 @@ func PermmapToQuery(permmap map[string]*ForumPerms, fid int) error {
|
|||
if err != nil {
|
||||
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 {
|
||||
tx, err := qgen.Builder.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = ReplaceForumPermsForGroupTx(tx, gid, presetSet, permSets)
|
||||
if err != nil {
|
||||
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 {
|
||||
|
|
|
@ -284,6 +284,10 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
|
|||
if err != nil {
|
||||
return gid, err
|
||||
}
|
||||
err = TopicList.RebuildPermTree()
|
||||
if err != nil {
|
||||
return gid, err
|
||||
}
|
||||
|
||||
return gid, nil
|
||||
}
|
||||
|
|
|
@ -204,7 +204,8 @@ func skipAllUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, h
|
|||
expectIndex := 0
|
||||
//fmt.Printf("tmplData: %+v\n", string(tmplData))
|
||||
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] {
|
||||
//fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex)
|
||||
expectIndex++
|
||||
|
|
|
@ -13,8 +13,8 @@ func NewNullUserCache() *NullUserCache {
|
|||
func (mus *NullUserCache) Get(id int) (*User, error) {
|
||||
return nil, ErrNoRows
|
||||
}
|
||||
func (mus *NullUserCache) BulkGet(_ []int) (list []*User) {
|
||||
return nil
|
||||
func (mus *NullUserCache) BulkGet(ids []int) (list []*User) {
|
||||
return make([]*User, len(ids))
|
||||
}
|
||||
func (mus *NullUserCache) GetUnsafe(id int) (*User, error) {
|
||||
return nil, ErrNoRows
|
||||
|
|
|
@ -13,7 +13,7 @@ type Header struct {
|
|||
Title string
|
||||
NoticeList []string
|
||||
Scripts []string
|
||||
//PreloadScripts []string
|
||||
//Preload []string
|
||||
Stylesheets []string
|
||||
Widgets PageWidgets
|
||||
Site *site
|
||||
|
@ -32,8 +32,8 @@ func (header *Header) AddScript(name string) {
|
|||
header.Scripts = append(header.Scripts, name)
|
||||
}
|
||||
|
||||
/*func (header *Header) PreloadScript(name string) {
|
||||
header.PreloadScripts = append(header.PreloadScripts, name)
|
||||
/*func (header *Header) Preload(name string) {
|
||||
header.Preload = append(header.Preload, name)
|
||||
}*/
|
||||
|
||||
func (header *Header) AddSheet(name string) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
*
|
||||
* Gosora Phrase System
|
||||
* Copyright Azareal 2017 - 2018
|
||||
* Copyright Azareal 2017 - 2019
|
||||
*
|
||||
*/
|
||||
package common
|
||||
|
@ -13,6 +13,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"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
|
||||
type LanguagePack struct {
|
||||
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.
|
||||
Levels LevelPhrases
|
||||
GlobalPerms map[string]string
|
||||
LocalPerms map[string]string
|
||||
SettingPhrases map[string]string
|
||||
PermPresets map[string]string
|
||||
Accounts map[string]string // TODO: Apply these phrases in the software proper
|
||||
UserAgents map[string]string
|
||||
OperatingSystems map[string]string
|
||||
HumanLanguages map[string]string
|
||||
Errors map[string]map[string]string // map[category]map[name]value
|
||||
NoticePhrases map[string]string
|
||||
PageTitles map[string]string
|
||||
TmplPhrases map[string]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.
|
||||
Levels LevelPhrases
|
||||
GlobalPerms map[string]string
|
||||
LocalPerms map[string]string
|
||||
SettingPhrases map[string]string
|
||||
PermPresets map[string]string
|
||||
Accounts map[string]string // TODO: Apply these phrases in the software proper
|
||||
UserAgents map[string]string
|
||||
OperatingSystems map[string]string
|
||||
HumanLanguages map[string]string
|
||||
Errors map[string]map[string]string // map[category]map[name]value
|
||||
NoticePhrases map[string]string
|
||||
PageTitles map[string]string
|
||||
TmplPhrases map[string]string
|
||||
TmplPhrasesPrefixes map[string]map[string]string // [prefix][name]phrase
|
||||
|
||||
TmplIndicesToPhrases [][][]byte // [tmplID][index]phrase
|
||||
}
|
||||
|
@ -81,6 +83,17 @@ func InitPhrases() error {
|
|||
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))
|
||||
for tmplID, phraseNames := range langTmplIndicesToNames {
|
||||
var phraseSet = make([][]byte, len(phraseNames))
|
||||
|
@ -233,6 +246,11 @@ func GetTmplPhrases() map[string]string {
|
|||
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 {
|
||||
return "{lang." + prefix + "[" + suffix + "]}"
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"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 {
|
||||
Get(id int) (*Poll, error)
|
||||
GetUnsafe(id int) (*Poll, error)
|
||||
|
@ -20,6 +21,7 @@ type PollCache interface {
|
|||
GetCapacity() int
|
||||
}
|
||||
|
||||
// MemoryPollCache stores and pulls polls out of the current process' memory
|
||||
type MemoryPollCache struct {
|
||||
items map[int]*Poll
|
||||
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) {
|
||||
mus.RLock()
|
||||
item, ok := mus.items[id]
|
||||
|
@ -46,6 +49,7 @@ func (mus *MemoryPollCache) Get(id int) (*Poll, error) {
|
|||
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) {
|
||||
list = make([]*Poll, len(ids))
|
||||
mus.RLock()
|
||||
|
@ -56,6 +60,7 @@ func (mus *MemoryPollCache) BulkGet(ids []int) (list []*Poll) {
|
|||
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) {
|
||||
item, ok := mus.items[id]
|
||||
if ok {
|
||||
|
@ -64,6 +69,7 @@ func (mus *MemoryPollCache) GetUnsafe(id int) (*Poll, error) {
|
|||
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 {
|
||||
mus.Lock()
|
||||
user, ok := mus.items[item.ID]
|
||||
|
@ -81,17 +87,21 @@ func (mus *MemoryPollCache) Set(item *Poll) error {
|
|||
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 {
|
||||
mus.Lock()
|
||||
if int(mus.length) >= mus.capacity {
|
||||
mus.Unlock()
|
||||
return ErrStoreCapacityOverflow
|
||||
}
|
||||
mus.Lock()
|
||||
mus.items[item.ID] = item
|
||||
mus.length = int64(len(mus.items))
|
||||
mus.Unlock()
|
||||
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 {
|
||||
if int(mus.length) >= mus.capacity {
|
||||
return ErrStoreCapacityOverflow
|
||||
|
@ -101,6 +111,7 @@ func (mus *MemoryPollCache) AddUnsafe(item *Poll) error {
|
|||
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 {
|
||||
mus.Lock()
|
||||
_, ok := mus.items[id]
|
||||
|
@ -114,6 +125,7 @@ func (mus *MemoryPollCache) Remove(id int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// RemoveUnsafe is the unsafe version of Remove. THIS METHOD IS NOT THREAD-SAFE.
|
||||
func (mus *MemoryPollCache) RemoveUnsafe(id int) error {
|
||||
_, ok := mus.items[id]
|
||||
if !ok {
|
||||
|
@ -124,6 +136,7 @@ func (mus *MemoryPollCache) RemoveUnsafe(id int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Flush removes all the polls from the cache, useful for tests.
|
||||
func (mus *MemoryPollCache) Flush() {
|
||||
mus.Lock()
|
||||
mus.items = make(map[int]*Poll)
|
||||
|
@ -132,19 +145,23 @@ func (mus *MemoryPollCache) Flush() {
|
|||
}
|
||||
|
||||
// ! 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 {
|
||||
return int(mus.length)
|
||||
}
|
||||
|
||||
// SetCapacity sets the maximum number of polls which this cache can hold
|
||||
func (mus *MemoryPollCache) SetCapacity(capacity int) {
|
||||
// Ints are moved in a single instruction, so this should be thread-safe
|
||||
mus.capacity = capacity
|
||||
}
|
||||
|
||||
// GetCapacity returns the maximum number of polls this cache can hold
|
||||
func (mus *MemoryPollCache) GetCapacity() int {
|
||||
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 {
|
||||
}
|
||||
|
||||
|
@ -153,50 +170,38 @@ func NewNullPollCache() *NullPollCache {
|
|||
return &NullPollCache{}
|
||||
}
|
||||
|
||||
// nolint
|
||||
func (mus *NullPollCache) Get(id int) (*Poll, error) {
|
||||
return nil, ErrNoRows
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) BulkGet(ids []int) (list []*Poll) {
|
||||
return list
|
||||
return make([]*Poll, len(ids))
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) GetUnsafe(id int) (*Poll, error) {
|
||||
return nil, ErrNoRows
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) Set(_ *Poll) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) Add(item *Poll) error {
|
||||
_ = item
|
||||
func (mus *NullPollCache) Add(_ *Poll) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) AddUnsafe(item *Poll) error {
|
||||
_ = item
|
||||
func (mus *NullPollCache) AddUnsafe(_ *Poll) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) Remove(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) RemoveUnsafe(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) Flush() {
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) Length() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) SetCapacity(_ int) {
|
||||
}
|
||||
|
||||
func (mus *NullPollCache) GetCapacity() int {
|
||||
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?
|
||||
|
||||
// TODO: Remove this as it might be counter-productive
|
||||
pusher, ok := w.(http.Pusher)
|
||||
/*pusher, ok := w.(http.Pusher)
|
||||
if ok {
|
||||
pusher.Push("/static/"+theme.Name+"/main.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)
|
||||
}
|
||||
// TODO: Push avatars?
|
||||
}
|
||||
}*/
|
||||
|
||||
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 {
|
||||
pusher.Push("/static/"+theme.Name+"/main.css", 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)
|
||||
}
|
||||
// TODO: Push avatars?
|
||||
}
|
||||
}*/
|
||||
|
||||
return header, nil
|
||||
}
|
||||
|
|
|
@ -81,6 +81,8 @@ type config struct {
|
|||
BuildSlugs bool // TODO: Make this a setting?
|
||||
ServerCount int
|
||||
|
||||
DisableLiveTopicList bool
|
||||
|
||||
Noavatar string // ? - Move this into the settings table?
|
||||
ItemsPerPage int // ? - Move this into the settings table?
|
||||
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)
|
||||
}
|
||||
|
||||
// ? - Add template hooks?
|
||||
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
|
||||
func tmplInitUsers() (User, User, User) {
|
||||
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?
|
||||
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}
|
||||
return user, user2, user3
|
||||
}
|
||||
|
||||
func tmplInitHeaders(user User, user2 User, user3 User) (*Header, *Header, *Header) {
|
||||
header := &Header{
|
||||
Site: Site,
|
||||
Settings: SettingBox.Load().(SettingMap),
|
||||
|
@ -163,9 +153,32 @@ func CompileTemplates() error {
|
|||
*header3 = *header
|
||||
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")
|
||||
|
||||
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{
|
||||
PollOption{0, "Nothing"},
|
||||
PollOption{1, "Something"},
|
||||
|
@ -213,7 +226,7 @@ func CompileTemplates() error {
|
|||
}
|
||||
|
||||
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"
|
||||
topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, Paginator{[]int{1}, 1, 1}}
|
||||
topicListTmpl, err := c.Compile("topics.html", "templates/", "common.TopicListPage", topicListPage, varList)
|
||||
|
@ -310,9 +323,11 @@ func CompileJSTemplates() error {
|
|||
log.Print("Compiling the JS templates")
|
||||
var config tmpl.CTemplateConfig
|
||||
config.Minify = Config.MinifyTemplates
|
||||
config.Debug = Dev.DebugMode
|
||||
config.SuperDebug = Dev.TemplateDebug
|
||||
config.SkipHandles = true
|
||||
config.SkipInitBlock = true
|
||||
config.SkipTmplPtrMap = true
|
||||
config.SkipInitBlock = false
|
||||
config.PackageName = "tmpl"
|
||||
|
||||
c := tmpl.NewCTemplateSet()
|
||||
|
@ -321,6 +336,11 @@ func CompileJSTemplates() error {
|
|||
"io": "io",
|
||||
"../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)
|
||||
|
||||
// TODO: Check what sort of path is sent exactly and use it here
|
||||
|
@ -330,6 +350,39 @@ func CompileJSTemplates() error {
|
|||
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 wg sync.WaitGroup
|
||||
var writeTemplate = func(name string, content string) {
|
||||
|
@ -348,6 +401,9 @@ func CompileJSTemplates() error {
|
|||
}()
|
||||
}
|
||||
writeTemplate("alert", alertTmpl)
|
||||
writeTemplate("topics_topic", topicListItemTmpl)
|
||||
writeTemplate("topic_posts", topicIDTmpl)
|
||||
writeTemplate("topic_alt_posts", topicIDAltTmpl)
|
||||
writeTemplateList(c, &wg, dirPrefix)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -28,12 +28,13 @@ type VarItemReflect struct {
|
|||
}
|
||||
|
||||
type CTemplateConfig struct {
|
||||
Minify bool
|
||||
Debug bool
|
||||
SuperDebug bool
|
||||
SkipHandles bool
|
||||
SkipInitBlock bool
|
||||
PackageName string
|
||||
Minify bool
|
||||
Debug bool
|
||||
SuperDebug bool
|
||||
SkipHandles bool
|
||||
SkipTmplPtrMap bool
|
||||
SkipInitBlock bool
|
||||
PackageName string
|
||||
}
|
||||
|
||||
// nolint
|
||||
|
@ -58,6 +59,7 @@ type CTemplateSet struct {
|
|||
//tempVars map[string]string
|
||||
config CTemplateConfig
|
||||
baseImportMap map[string]string
|
||||
buildTags string
|
||||
expectsInt interface{}
|
||||
}
|
||||
|
||||
|
@ -103,6 +105,10 @@ func (c *CTemplateSet) SetBaseImportMap(importMap map[string]string) {
|
|||
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) {
|
||||
if c.config.Debug {
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
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.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n"
|
||||
if !c.config.SkipTmplPtrMap {
|
||||
fout += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n"
|
||||
}
|
||||
if len(c.langIndexToName) > 0 {
|
||||
fout += "\t" + fname + "_tmpl_phrase_id = common.RegisterTmplPhraseNames([]string{\n"
|
||||
for _, name := range c.langIndexToName {
|
||||
|
@ -805,6 +818,7 @@ func (c *CTemplateSet) compileIfVarsub(varname string, varholder string, templat
|
|||
out += ".(" + cur.Type().Name() + ")"
|
||||
}
|
||||
if !cur.IsValid() {
|
||||
fmt.Println("cur: ", cur)
|
||||
panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?")
|
||||
}
|
||||
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)
|
||||
case func(Page, io.Writer) error:
|
||||
return tmplO(pi.(Page), w)
|
||||
case string:
|
||||
case nil, string:
|
||||
mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[template]
|
||||
if !ok {
|
||||
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
|
||||
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]
|
||||
if ok {
|
||||
//fmt.Println("tmpl: ", tmpl)
|
||||
//fmt.Println("exiting at Themes[theme].TmplPtr[template]")
|
||||
return tmpl
|
||||
}
|
||||
|
||||
tmpl, ok = TmplPtrMap[template]
|
||||
if ok {
|
||||
//fmt.Println("exiting at TmplPtrMap[template]")
|
||||
return tmpl
|
||||
}
|
||||
//fmt.Println("just passing back the template name")
|
||||
return template
|
||||
}
|
||||
|
||||
|
|
|
@ -81,14 +81,15 @@ type TopicUser struct {
|
|||
}
|
||||
|
||||
type TopicsRow struct {
|
||||
ID int
|
||||
Link string
|
||||
Title string
|
||||
Content string
|
||||
CreatedBy int
|
||||
IsClosed bool
|
||||
Sticky bool
|
||||
CreatedAt string
|
||||
ID int
|
||||
Link string
|
||||
Title string
|
||||
Content string
|
||||
CreatedBy int
|
||||
IsClosed bool
|
||||
Sticky bool
|
||||
CreatedAt time.Time
|
||||
//RelativeCreatedAt string
|
||||
LastReplyAt time.Time
|
||||
RelativeLastReplyAt string
|
||||
LastReplyBy int
|
||||
|
@ -109,6 +110,31 @@ type TopicsRow struct {
|
|||
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 {
|
||||
addRepliesToTopic *sql.Stmt
|
||||
lock *sql.Stmt
|
||||
|
@ -302,6 +328,7 @@ func (topic *Topic) Copy() Topic {
|
|||
return *topic
|
||||
}
|
||||
|
||||
// TODO: Load LastReplyAt?
|
||||
func TopicByReplyID(rid int) (*Topic, error) {
|
||||
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)
|
||||
|
@ -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: Load LastReplyAt everywhere in here?
|
||||
func GetTopicUser(tid int) (TopicUser, error) {
|
||||
tcache := Topics.GetCache()
|
||||
ucache := Users.GetCache()
|
||||
|
|
|
@ -3,6 +3,7 @@ package common
|
|||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"../query_gen/lib"
|
||||
)
|
||||
|
@ -16,119 +17,82 @@ type TopicListHolder struct {
|
|||
}
|
||||
|
||||
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)
|
||||
GetList(page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error)
|
||||
|
||||
RebuildPermTree() error
|
||||
}
|
||||
|
||||
type DefaultTopicList struct {
|
||||
// TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group?
|
||||
oddGroups map[int]*TopicListHolder
|
||||
evenGroups map[int]*TopicListHolder
|
||||
oddLock 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) {
|
||||
tList := &DefaultTopicList{
|
||||
oddGroups: make(map[int]*TopicListHolder),
|
||||
evenGroups: make(map[int]*TopicListHolder),
|
||||
}
|
||||
|
||||
var slots = make([]int, 8) // Only cache the topic list for eight groups
|
||||
|
||||
// TODO: Do something more efficient than this
|
||||
allGroups, err := Groups.GetAll()
|
||||
err := tList.RebuildPermTree()
|
||||
if err != nil {
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
AddScheduledHalfSecondTask(tList.Tick)
|
||||
//AddScheduledSecondTask(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second
|
||||
return tList, nil
|
||||
}
|
||||
|
||||
// TODO: Add support for groups other than the guest group
|
||||
func (tList *DefaultTopicList) Tick() error {
|
||||
var oddLists = 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 {
|
||||
evenLists[gid] = &TopicListHolder{topicList, forumList, paginator}
|
||||
evenLists[gid] = holder
|
||||
} else {
|
||||
oddLists[gid] = &TopicListHolder{topicList, forumList, paginator}
|
||||
oddLists[gid] = holder
|
||||
}
|
||||
}
|
||||
|
||||
guestGroup, err := Groups.Get(GuestUser.Group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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?
|
||||
var canSeeHolders = make(map[string]*TopicListHolder)
|
||||
for name, canSee := range tList.permTree.Load().(map[string][]int) {
|
||||
topicList, forumList, paginator, err := tList.GetListByCanSee(canSee, 1)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
topicList, forumList, paginator, err := tList.getListByGroup(group, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
var canSee = make([]byte, len(group.CanSee))
|
||||
for i, item := range group.CanSee {
|
||||
canSee[i] = byte(item)
|
||||
}
|
||||
addList(group.ID, topicList, forumList, paginator)
|
||||
addList(group.ID, canSeeHolders[string(canSee)])
|
||||
}
|
||||
|
||||
tList.oddLock.Lock()
|
||||
|
@ -142,6 +106,28 @@ func (tList *DefaultTopicList) Tick() error {
|
|||
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) {
|
||||
// TODO: Cache the first three pages not just the first along with all the topics on this beaten track
|
||||
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) {
|
||||
// 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
|
||||
|
||||
func (tList *DefaultTopicList) GetListByCanSee(canSee []int, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) {
|
||||
// We need a list of the visible forums for Quick Topic
|
||||
// ? - Would it be useful, if we could post in social groups from /topics/?
|
||||
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)
|
||||
// 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)
|
||||
topicItem.ForumName = forum.Name
|
||||
topicItem.ForumLink = forum.Link
|
||||
|
||||
//topicItem.CreatedAt = RelativeTime(topicItem.CreatedAt)
|
||||
//topicItem.RelativeCreatedAt = RelativeTime(topicItem.CreatedAt)
|
||||
topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt)
|
||||
|
||||
// TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/
|
||||
|
|
|
@ -53,6 +53,28 @@ type User struct {
|
|||
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 {
|
||||
activate *sql.Stmt
|
||||
changeGroup *sql.Stmt
|
||||
|
|
|
@ -10,6 +10,7 @@ package common
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -28,54 +29,218 @@ type WSUser struct {
|
|||
User *User
|
||||
}
|
||||
|
||||
type WSHub struct {
|
||||
// TODO: Make this an interface?
|
||||
type WsHubImpl struct {
|
||||
// TODO: Shard this map
|
||||
OnlineUsers map[int]*WSUser
|
||||
OnlineGuests map[*WSUser]bool
|
||||
GuestLock sync.RWMutex
|
||||
UserLock sync.RWMutex
|
||||
|
||||
lastTick time.Time
|
||||
lastTopicList []*TopicsRow
|
||||
}
|
||||
|
||||
// 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 WsHub WSHub
|
||||
// TODO: Rename this to WebSockets?
|
||||
var WsHub WsHubImpl
|
||||
var wsUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
|
||||
var errWsNouser = errors.New("This user isn't connected via WebSockets")
|
||||
|
||||
func init() {
|
||||
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),
|
||||
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()
|
||||
hub.GuestLock.RLock()
|
||||
return len(hub.OnlineGuests)
|
||||
}
|
||||
|
||||
func (hub *WSHub) UserCount() int {
|
||||
func (hub *WsHubImpl) UserCount() int {
|
||||
defer hub.UserLock.RUnlock()
|
||||
hub.UserLock.RLock()
|
||||
return len(hub.OnlineUsers)
|
||||
}
|
||||
|
||||
func (hub *WSHub) broadcastMessage(msg string) error {
|
||||
func (hub *WsHubImpl) broadcastMessage(msg string) error {
|
||||
hub.UserLock.RLock()
|
||||
defer hub.UserLock.RUnlock()
|
||||
for _, wsUser := range hub.OnlineUsers {
|
||||
w, err := wsUser.conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = w.Write([]byte(msg))
|
||||
w.Close()
|
||||
}
|
||||
hub.UserLock.RUnlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hub *WSHub) pushMessage(targetUser int, msg string) error {
|
||||
func (hub *WsHubImpl) pushMessage(targetUser int, msg string) error {
|
||||
hub.UserLock.RLock()
|
||||
wsUser, ok := hub.OnlineUsers[targetUser]
|
||||
hub.UserLock.RUnlock()
|
||||
|
@ -93,7 +258,7 @@ func (hub *WSHub) pushMessage(targetUser int, msg string) error {
|
|||
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")
|
||||
hub.UserLock.RLock()
|
||||
wsUser, ok := hub.OnlineUsers[targetUser]
|
||||
|
@ -119,7 +284,7 @@ func (hub *WSHub) pushAlert(targetUser int, asid int, event string, elementType
|
|||
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
|
||||
hub.UserLock.RLock()
|
||||
// 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)
|
||||
WsHub.GuestLock.Unlock()
|
||||
} else {
|
||||
// TODO: Make sure the admin is removed from the admin stats list in the case that an error happens
|
||||
WsHub.UserLock.Lock()
|
||||
delete(WsHub.OnlineUsers, user.ID)
|
||||
WsHub.UserLock.Unlock()
|
||||
|
@ -229,26 +395,17 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: Use a map instead of a switch to make this more modular?
|
||||
func wsPageResponses(wsUser *WSUser, page []byte) {
|
||||
//fmt.Println("entering page: ", 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/":
|
||||
//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...
|
||||
adminStatsMutex.Lock()
|
||||
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) {
|
||||
//fmt.Println("leaving page: ", string(page))
|
||||
switch string(page) {
|
||||
// Live Topic List is an experimental feature
|
||||
case "/topics/":
|
||||
topicListMutex.Lock()
|
||||
delete(topicListWatchers, wsUser)
|
||||
topicListMutex.Unlock()
|
||||
case "/panel/":
|
||||
adminStatsMutex.Lock()
|
||||
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 adminStatsMutex sync.RWMutex
|
||||
|
||||
|
@ -385,20 +553,18 @@ AdminStatLoop:
|
|||
}
|
||||
}
|
||||
|
||||
adminStatsMutex.RLock()
|
||||
watchers := adminStatsWatchers
|
||||
adminStatsMutex.RUnlock()
|
||||
|
||||
for watcher := range watchers {
|
||||
// Acquire a write lock for now, so we can handle the delete() case below and the read one simultaneously
|
||||
// TODO: Stop taking a write lock here if it isn't necessary
|
||||
adminStatsMutex.Lock()
|
||||
for watcher := range adminStatsWatchers {
|
||||
w, err := watcher.conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
adminStatsMutex.Lock()
|
||||
delete(adminStatsWatchers, watcher)
|
||||
adminStatsMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// nolint
|
||||
// TODO: Use JSON for this to make things more portable and easier to convert to MessagePack, if need be?
|
||||
if !noStatUpdates {
|
||||
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"))
|
||||
|
@ -421,6 +587,7 @@ AdminStatLoop:
|
|||
|
||||
w.Close()
|
||||
}
|
||||
adminStatsMutex.Unlock()
|
||||
|
||||
lastUonline = uonline
|
||||
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_last":"Last",
|
||||
"topics_starter":"Starter",
|
||||
"topic_like_count_suffix":" likes",
|
||||
"topic_plus":"+",
|
||||
"topic_plus_one":"+1",
|
||||
"topic_gap_up":" up",
|
||||
"topic_level":"Level",
|
||||
"topic_edit_button_text":"Edit",
|
||||
"topic_delete_button_text":"Delete",
|
||||
"topic_ip_button_text":"IP",
|
||||
"topic_lock_button_text":"Lock",
|
||||
"topic_unlock_button_text":"Unlock",
|
||||
"topic_pin_button_text":"Pin",
|
||||
"topic_unpin_button_text":"Unpin",
|
||||
"topic_report_button_text":"Report",
|
||||
"topic_flag_button_text":"Flag",
|
||||
"topic.like_count_suffix":" likes",
|
||||
"topic.plus":"+",
|
||||
"topic.plus_one":"+1",
|
||||
"topic.gap_up":" up",
|
||||
"topic.level":"Level",
|
||||
"topic.edit_button_text":"Edit",
|
||||
"topic.delete_button_text":"Delete",
|
||||
"topic.ip_button_text":"IP",
|
||||
"topic.lock_button_text":"Lock",
|
||||
"topic.unlock_button_text":"Unlock",
|
||||
"topic.pin_button_text":"Pin",
|
||||
"topic.unpin_button_text":"Unpin",
|
||||
"topic.report_button_text":"Report",
|
||||
"topic.flag_button_text":"Flag",
|
||||
|
||||
"panel_rank_admins":"Admins",
|
||||
"panel_rank_mods":"Mods",
|
||||
|
@ -413,16 +413,16 @@
|
|||
"create_topic_create_topic_button":"Create Topic",
|
||||
"create_topic_add_file_button":"Add File",
|
||||
|
||||
"quick_topic_aria":"Quick Topic Form",
|
||||
"quick_topic_avatar_tooltip":"Your Avatar",
|
||||
"quick_topic_avatar_alt":"Your Avatar",
|
||||
"quick_topic_whatsup":"What's up?",
|
||||
"quick_topic_content_placeholder":"Insert post here",
|
||||
"quick_topic_add_poll_option":"Add new poll option",
|
||||
"quick_topic_create_topic_button":"Create Topic",
|
||||
"quick_topic_add_poll_button":"Add Poll",
|
||||
"quick_topic_add_file_button":"Add File",
|
||||
"quick_topic_cancel_button":"Cancel",
|
||||
"quick_topic.aria":"Quick Topic Form",
|
||||
"quick_topic.avatar_tooltip":"Your Avatar",
|
||||
"quick_topic.avatar_alt":"Your Avatar",
|
||||
"quick_topic.whatsup":"What's up?",
|
||||
"quick_topic.content_placeholder":"Insert post here",
|
||||
"quick_topic.add_poll_option":"Add new poll option",
|
||||
"quick_topic.create_topic_button":"Create Topic",
|
||||
"quick_topic.add_poll_button":"Add Poll",
|
||||
"quick_topic.add_file_button":"Add File",
|
||||
"quick_topic.cancel_button":"Cancel",
|
||||
|
||||
"topic_list_create_topic_tooltip":"Create Topic",
|
||||
"topic_list_create_topic_aria":"Create a topic",
|
||||
|
@ -435,8 +435,8 @@
|
|||
"topic_list_moderate_run":"Run",
|
||||
"topic_list_move_head":"Move these topics to?",
|
||||
"topic_list_move_button":"Move Topics",
|
||||
"status_closed_tooltip":"Status: Closed",
|
||||
"status_pinned_tooltip":"Status: Pinned",
|
||||
"status.closed_tooltip":"Status: Closed",
|
||||
"status.pinned_tooltip":"Status: Pinned",
|
||||
|
||||
"topics_head":"All Topics",
|
||||
"topics_locked_tooltip":"You don't have the permissions needed to create a topic",
|
||||
|
@ -456,68 +456,68 @@
|
|||
"forums_none":"None",
|
||||
"forums_no_forums":"You don't have access to any forums.",
|
||||
|
||||
"topic_opening_post_aria":"The opening post for this topic",
|
||||
"topic_status_closed_aria":"This topic has been locked",
|
||||
"topic_title_input_aria":"Topic Title Input",
|
||||
"topic_update_button":"Update",
|
||||
"topic_userinfo_aria":"The information on the poster",
|
||||
"topic_poll_aria":"The main poll for this topic",
|
||||
"topic_poll_vote":"Vote",
|
||||
"topic_poll_results":"Results",
|
||||
"topic_poll_cancel":"Cancel",
|
||||
"topic_post_controls_aria":"Controls and Author Information",
|
||||
"topic_unlike_tooltip":"Unlike",
|
||||
"topic_unlike_aria":"Unlike this topic",
|
||||
"topic_like_tooltip":"Like",
|
||||
"topic_like_aria":"Like this topic",
|
||||
"topic_edit_tooltip":"Edit Topic",
|
||||
"topic_edit_aria":"Edit this topic",
|
||||
"topic_delete_tooltip":"Delete Topic",
|
||||
"topic_delete_aria":"Delete this topic",
|
||||
"topic_unlock_tooltip":"Unlock Topic",
|
||||
"topic_unlock_aria":"Unlock this topic",
|
||||
"topic_lock_tooltip":"Lock Topic",
|
||||
"topic_lock_aria":"Lock this topic",
|
||||
"topic_unpin_tooltip":"Unpin Topic",
|
||||
"topic_unpin_aria":"Unpin this topic",
|
||||
"topic_pin_tooltip":"Pin Topic",
|
||||
"topic_pin_aria":"Pin this topic",
|
||||
"topic_ip_tooltip":"View IP",
|
||||
"topic_ip_full_tooltip":"IP Address",
|
||||
"topic_ip_full_aria":"This user's IP Address",
|
||||
"topic_flag_tooltip":"Flag this topic",
|
||||
"topic_flag_aria":"Flag this topic",
|
||||
"topic_report_tooltip":"Report this topic",
|
||||
"topic_report_aria":"Report this topic",
|
||||
"topic_like_count_aria":"The number of likes on this topic",
|
||||
"topic_like_count_tooltip":"Like Count",
|
||||
"topic_level_aria":"The poster's level",
|
||||
"topic_level_tooltip":"Level",
|
||||
"topic_current_page_aria":"The current page for this topic",
|
||||
"topic_post_like_tooltip":"Like this",
|
||||
"topic_post_like_aria":"Like this post",
|
||||
"topic_post_unlike_tooltip":"Unlike this",
|
||||
"topic_post_unlike_aria":"Unlike this post",
|
||||
"topic_post_edit_tooltip":"Edit Reply",
|
||||
"topic_post_edit_aria":"Edit this post",
|
||||
"topic_post_delete_tooltip":"Delete Reply",
|
||||
"topic_post_delete_aria":"Delete this post",
|
||||
"topic_post_ip_tooltip":"View IP",
|
||||
"topic_post_flag_tooltip":"Flag this reply",
|
||||
"topic_post_flag_aria":"Flag this reply",
|
||||
"topic_post_like_count_tooltip":"Like Count",
|
||||
"topic_post_level_aria":"The poster's level",
|
||||
"topic_post_level_tooltip":"Level",
|
||||
"topic_reply_aria":"The quick reply form",
|
||||
"topic_reply_content":"Insert reply here",
|
||||
"topic_reply_content_alt":"What do you think?",
|
||||
"topic_reply_add_poll_option":"Add new poll option",
|
||||
"topic_reply_button":"Create Reply",
|
||||
"topic_reply_add_poll_button":"Add Poll",
|
||||
"topic_reply_add_file_button":"Add File",
|
||||
"topic.opening_post_aria":"The opening post for this topic",
|
||||
"topic.status_closed_aria":"This topic has been locked",
|
||||
"topic.title_input_aria":"Topic Title Input",
|
||||
"topic.update_button":"Update",
|
||||
"topic.userinfo_aria":"The information on the poster",
|
||||
"topic.poll_aria":"The main poll for this topic",
|
||||
"topic.poll_vote":"Vote",
|
||||
"topic.poll_results":"Results",
|
||||
"topic.poll_cancel":"Cancel",
|
||||
"topic.post_controls_aria":"Controls and Author Information",
|
||||
"topic.unlike_tooltip":"Unlike",
|
||||
"topic.unlike_aria":"Unlike this topic",
|
||||
"topic.like_tooltip":"Like",
|
||||
"topic.like_aria":"Like this topic",
|
||||
"topic.edit_tooltip":"Edit Topic",
|
||||
"topic.edit_aria":"Edit this topic",
|
||||
"topic.delete_tooltip":"Delete Topic",
|
||||
"topic.delete_aria":"Delete this topic",
|
||||
"topic.unlock_tooltip":"Unlock Topic",
|
||||
"topic.unlock_aria":"Unlock this topic",
|
||||
"topic.lock_tooltip":"Lock Topic",
|
||||
"topic.lock_aria":"Lock this topic",
|
||||
"topic.unpin_tooltip":"Unpin Topic",
|
||||
"topic.unpin_aria":"Unpin this topic",
|
||||
"topic.pin_tooltip":"Pin Topic",
|
||||
"topic.pin_aria":"Pin this topic",
|
||||
"topic.ip_tooltip":"View IP",
|
||||
"topic.ip_full_tooltip":"IP Address",
|
||||
"topic.ip_full_aria":"This user's IP Address",
|
||||
"topic.flag_tooltip":"Flag this topic",
|
||||
"topic.flag_aria":"Flag this topic",
|
||||
"topic.report_tooltip":"Report this topic",
|
||||
"topic.report_aria":"Report this topic",
|
||||
"topic.like_count_aria":"The number of likes on this topic",
|
||||
"topic.like_count_tooltip":"Like Count",
|
||||
"topic.level_aria":"The poster's level",
|
||||
"topic.level_tooltip":"Level",
|
||||
"topic.current_page_aria":"The current page for this topic",
|
||||
"topic.post_like_tooltip":"Like this",
|
||||
"topic.post_like_aria":"Like this post",
|
||||
"topic.post_unlike_tooltip":"Unlike this",
|
||||
"topic.post_unlike_aria":"Unlike this post",
|
||||
"topic.post_edit_tooltip":"Edit Reply",
|
||||
"topic.post_edit_aria":"Edit this post",
|
||||
"topic.post_delete_tooltip":"Delete Reply",
|
||||
"topic.post_delete_aria":"Delete this post",
|
||||
"topic.post_ip_tooltip":"View IP",
|
||||
"topic.post_flag_tooltip":"Flag this reply",
|
||||
"topic.post_flag_aria":"Flag this reply",
|
||||
"topic.post_like_count_tooltip":"Like Count",
|
||||
"topic.post_level_aria":"The poster's level",
|
||||
"topic.post_level_tooltip":"Level",
|
||||
"topic.reply_aria":"The quick reply form",
|
||||
"topic.reply_content":"Insert reply here",
|
||||
"topic.reply_content_alt":"What do you think?",
|
||||
"topic.reply_add_poll_option":"Add new poll option",
|
||||
"topic.reply_button":"Create Reply",
|
||||
"topic.reply_add_poll_button":"Add Poll",
|
||||
"topic.reply_add_file_button":"Add File",
|
||||
|
||||
"topic_level_prefix":"Level ",
|
||||
"topic_your_information":"Your information",
|
||||
"topic.level_prefix":"Level ",
|
||||
"topic.your_information":"Your information",
|
||||
|
||||
"paginator_less_than":"<",
|
||||
"paginator_greater_than":">",
|
||||
|
|
3
main.go
3
main.go
|
@ -442,6 +442,9 @@ func main() {
|
|||
log.Fatal("Received a signal to shutdown: ", sig)
|
||||
}()
|
||||
|
||||
// Start up the WebSocket ticks
|
||||
common.WsHub.Start()
|
||||
|
||||
//if profiling {
|
||||
// pprof.StopCPUProfile()
|
||||
//}
|
||||
|
|
165
public/global.js
165
public/global.js
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
var formVars = {};
|
||||
var tmplInits = {};
|
||||
var tmplPhrases = [];
|
||||
var alertList = [];
|
||||
var alertCount = 0;
|
||||
var conn;
|
||||
|
@ -72,19 +74,21 @@ function loadAlerts(menuAlerts)
|
|||
for(var i in data.msgs) {
|
||||
var msg = data.msgs[i];
|
||||
var mmsg = msg.msg;
|
||||
|
||||
if("sub" in msg) {
|
||||
for(var i = 0; i < msg.sub.length; i++) {
|
||||
mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]);
|
||||
//console.log("Sub #" + i + ":",msg.sub[i]);
|
||||
}
|
||||
}
|
||||
alist += Template_alert({
|
||||
|
||||
let aItem = Template_alert({
|
||||
ASID: msg.asid || 0,
|
||||
Path: msg.path,
|
||||
Avatar: msg.avatar || "",
|
||||
Message: mmsg
|
||||
})
|
||||
alist += aItem;
|
||||
alertList.push(aItem);
|
||||
//console.log(msg);
|
||||
//console.log(mmsg);
|
||||
}
|
||||
|
@ -138,64 +142,97 @@ function SplitN(data,ch,n) {
|
|||
return out;
|
||||
}
|
||||
|
||||
function runWebSockets() {
|
||||
if(window.location.protocol == "https:")
|
||||
conn = new WebSocket("wss://" + document.location.host + "/ws/");
|
||||
else conn = new WebSocket("ws://" + document.location.host + "/ws/");
|
||||
function wsAlertEvent(data) {
|
||||
var msg = data.msg;
|
||||
if("sub" in data) {
|
||||
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");
|
||||
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
|
||||
Notification.requestPermission();
|
||||
if(loggedIn) {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
}
|
||||
conn.onclose = function() {
|
||||
conn.onclose = () => {
|
||||
conn = false;
|
||||
console.log("The WebSockets connection was closed");
|
||||
}
|
||||
conn.onmessage = function(event) {
|
||||
conn.onmessage = (event) => {
|
||||
//console.log("WSMessage:", event.data);
|
||||
if(event.data[0] == "{") {
|
||||
console.log("json message");
|
||||
let data = "";
|
||||
try {
|
||||
var data = JSON.parse(event.data);
|
||||
data = JSON.parse(event.data);
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Fix the data races in this code
|
||||
if ("msg" in data) {
|
||||
var msg = data.msg
|
||||
if("sub" in data)
|
||||
for(var i = 0; i < data.sub.length; i++)
|
||||
msg = msg.replace("\{"+i+"\}", data.sub[i]);
|
||||
|
||||
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>");
|
||||
else alertList.push("<div class='alertItem'><a href=\""+data.path+"\" class='text'>"+msg+"</a></div>");
|
||||
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);
|
||||
wsAlertEvent(data);
|
||||
} else if("Topics" in data) {
|
||||
console.log("topic in data");
|
||||
console.log("data:", data);
|
||||
let topic = data.Topics[0];
|
||||
if(topic === undefined){
|
||||
console.log("empty topic list");
|
||||
return;
|
||||
}
|
||||
|
||||
bindToAlerts();
|
||||
let renTopic = Template_topics_topic(topic);
|
||||
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) {
|
||||
let url = "//" +siteURL+"/static/"+name
|
||||
$.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(){
|
||||
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");
|
||||
alertsInitted = true;
|
||||
var alertMenuList = document.getElementsByClassName("menu_alerts");
|
||||
for(var i = 0; i < alertMenuList.length; i++) {
|
||||
loadAlerts(alertMenuList[i]);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if(window["WebSocket"]) runWebSockets();
|
||||
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
|
||||
for _, char := range req.URL.Path {
|
||||
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
|
||||
}
|
||||
}
|
||||
lowerPath := strings.ToLower(req.URL.Path)
|
||||
// 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") {
|
||||
router.SuspiciousRequest(req,"")
|
||||
router.SuspiciousRequest(req,"Bad snippet in path")
|
||||
}
|
||||
|
||||
// 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
|
||||
items = items[: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 String: ", string(buffer))
|
||||
break
|
||||
|
|
|
@ -2,7 +2,6 @@ package main
|
|||
|
||||
// TODO: How should we handle *HeaderLite and *Header?
|
||||
func routes() {
|
||||
addRoute(View("routeAPI", "/api/"))
|
||||
addRoute(View("routes.Overview", "/overview/"))
|
||||
addRoute(View("routes.CustomPage", "/pages/", "extraData"))
|
||||
addRoute(View("routes.ForumList", "/forums/" /*,"&forums"*/))
|
||||
|
@ -12,6 +11,12 @@ func routes() {
|
|||
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?
|
||||
reportGroup := newRouteGroup("/report/",
|
||||
Action("routes.ReportSubmit", "/report/submit/", "extraData"),
|
||||
|
|
91
routes.go
91
routes.go
|
@ -7,8 +7,11 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"./common"
|
||||
)
|
||||
|
@ -95,3 +98,91 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
|
|||
}
|
||||
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.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
|
||||
|
||||
common.RunVhook("forum_trow_assign", &topicItem, &forum)
|
||||
common.RunVhookNoreturn("forum_trow_assign", &topicItem, &forum)
|
||||
topicList = append(topicList, &topicItem)
|
||||
reqUserList[topicItem.CreatedBy] = 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")
|
||||
if group.IsMod || group.IsAdmin {
|
||||
if group.IsMod {
|
||||
replyClassName = common.Config.StaffCSS
|
||||
} else {
|
||||
replyClassName = ""
|
||||
|
|
|
@ -8,10 +8,7 @@ type HTTPSRedirect struct {
|
|||
|
||||
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Connection", "close")
|
||||
dest := "https://" + req.Host + req.URL.Path
|
||||
if len(req.URL.RawQuery) > 0 {
|
||||
dest += "?" + req.URL.RawQuery
|
||||
}
|
||||
dest := "https://" + req.Host + req.URL.String()
|
||||
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
|
||||
if postGroup.IsMod || postGroup.IsAdmin {
|
||||
if postGroup.IsMod {
|
||||
topic.ClassName = common.Config.StaffCSS
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if postGroup.IsMod || postGroup.IsAdmin {
|
||||
if postGroup.IsMod {
|
||||
replyItem.ClassName = common.Config.StaffCSS
|
||||
} else {
|
||||
replyItem.ClassName = ""
|
||||
|
@ -185,7 +185,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit
|
|||
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?
|
||||
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?
|
||||
// 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
|
||||
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
|
||||
var forumList []common.Forum
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
rem TODO: Make these deletes a little less noisy
|
||||
del "template_*.go"
|
||||
del "gen_*.go"
|
||||
del "tmpl_client/template_*.go"
|
||||
cd tmpl_client
|
||||
del "template_*.go"
|
||||
cd ..
|
||||
del "gosora.exe"
|
||||
|
||||
echo Generating the dynamic code
|
||||
|
|
4
run.bat
4
run.bat
|
@ -2,7 +2,9 @@
|
|||
rem TODO: Make these deletes a little less noisy
|
||||
del "template_*.go"
|
||||
del "gen_*.go"
|
||||
del "tmpl_client/template_*.go"
|
||||
cd tmpl_client
|
||||
del "template_*.go"
|
||||
cd ..
|
||||
del "gosora.exe"
|
||||
|
||||
echo Generating the dynamic code
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
rem TODO: Make these deletes a little less noisy
|
||||
del "template_*.go"
|
||||
del "gen_*.go"
|
||||
del "tmpl_client/template_*.go"
|
||||
cd tmpl_client
|
||||
del "template_*.go"
|
||||
cd ..
|
||||
del "gosora.exe"
|
||||
|
||||
echo Generating the dynamic code
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
rem TODO: Make these deletes a little less noisy
|
||||
del "template_*.go"
|
||||
del "gen_*.go"
|
||||
del "tmpl_client/template_*.go"
|
||||
cd tmpl_client
|
||||
del "template_*.go"
|
||||
cd ..
|
||||
del "gosora.exe"
|
||||
|
||||
echo Generating the dynamic code
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
rem TODO: Make these deletes a little less noisy
|
||||
del "template_*.go"
|
||||
del "gen_*.go"
|
||||
del "tmpl_client/template_*.go"
|
||||
cd tmpl_client
|
||||
del "template_*.go"
|
||||
cd ..
|
||||
del "gosora.exe"
|
||||
|
||||
echo Generating the dynamic code
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
<div class="coldyn_block">
|
||||
<div id="dash_left" class="coldyn_item">
|
||||
<div class="rowitem">
|
||||
<span id="dash_saved">Saved</span>
|
||||
<!--<span id="dash_username">{{.CurrentUser.Name}}</span>-->
|
||||
<span id="dash_username">
|
||||
<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}}" />
|
||||
|
|
|
@ -39,21 +39,21 @@
|
|||
</form>
|
||||
</div>
|
||||
{{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>
|
||||
<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">
|
||||
<div class="main_form">
|
||||
<div class="topic_meta">
|
||||
<div class="formrow topic_name_row real_first_child">
|
||||
<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 class="formrow topic_content_row">
|
||||
<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 class="formrow poll_content_row auto_hide">
|
||||
|
@ -63,13 +63,13 @@
|
|||
</div>
|
||||
<div class="formrow quick_button_row">
|
||||
<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" class="formbutton" id="add_poll_button">{{lang "quick_topic_add_poll_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>
|
||||
{{if .CurrentUser.Perms.UploadFiles}}
|
||||
<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}}
|
||||
<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>
|
||||
|
@ -85,8 +85,8 @@
|
|||
<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>
|
||||
{{/** 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}}
|
||||
{{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">
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
{{end}}
|
||||
<script type="text/javascript">
|
||||
var session = "{{.CurrentUser.Session}}";
|
||||
var loggedIn = {{.CurrentUser.Loggedin}};
|
||||
var siteURL = "{{.Header.Site.URL}}";
|
||||
var maxRequestSize = "{{.Header.Site.MaxRequestSize}}";
|
||||
</script>
|
||||
|
|
|
@ -11,21 +11,21 @@
|
|||
|
||||
<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}}">
|
||||
<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 **/}}
|
||||
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
|
||||
{{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"}}" />
|
||||
<button form='edit_topic_form' name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic_update_button"}}</button>
|
||||
<input form='edit_topic_form' class='show_on_edit topic_name_input' name="topic_name" value='{{.Topic.Title}}' type="text" aria-label="{{lang "topic.title_input_aria"}}" />
|
||||
<button form='edit_topic_form' name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic.update_button"}}</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .Poll.ID}}
|
||||
<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="topic_content user_content" style="margin:0;padding:0;">
|
||||
{{range .Poll.QuickOptions}}
|
||||
|
@ -38,9 +38,9 @@
|
|||
</div>
|
||||
{{end}}
|
||||
<div class="poll_buttons">
|
||||
<button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">{{lang "topic_poll_vote"}}</button>
|
||||
<button class="poll_results_button" data-poll-id="{{.Poll.ID}}">{{lang "topic_poll_results"}}</button>
|
||||
<a href="#"><button class="poll_cancel_button">{{lang "topic_poll_cancel"}}</button></a>
|
||||
<button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">{{lang "topic.poll_vote"}}</button>
|
||||
<button class="poll_results_button" data-poll-id="{{.Poll.ID}}">{{lang "topic.poll_results"}}</button>
|
||||
<a href="#"><button class="poll_cancel_button">{{lang "topic.poll_cancel"}}</button></a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="poll_results_{{.Poll.ID}}" class="poll_results auto_hide">
|
||||
|
@ -50,77 +50,48 @@
|
|||
</article>
|
||||
{{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;">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
{{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}}
|
||||
|
||||
{{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}}
|
||||
|
||||
{{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.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>
|
||||
{{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}}
|
||||
<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>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="rowblock post_container" aria-label="{{lang "topic_current_page_aria"}}" style="overflow: hidden;">{{range .ItemList}}{{if .ActionType}}
|
||||
<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>
|
||||
{{template "topic_posts.html" . }}
|
||||
|
||||
{{if .CurrentUser.Perms.CreateReply}}
|
||||
{{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>
|
||||
<input form="quick_post_form" name="tid" value='{{.Topic.ID}}' type="hidden" />
|
||||
<input form="quick_post_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
|
||||
<div class="formrow real_first_child">
|
||||
<div class="formitem">
|
||||
<textarea id="input_content" form="quick_post_form" name="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 class="formrow poll_content_row auto_hide">
|
||||
|
@ -128,17 +99,17 @@
|
|||
<div class="pollinput" data-pollinput="0">
|
||||
<input type="checkbox" disabled />
|
||||
<label class="pollinputlabel"></label>
|
||||
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "topic_reply_add_poll_option"}}" />
|
||||
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "topic.reply_add_poll_option"}}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formrow quick_button_row">
|
||||
<div class="formitem">
|
||||
<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" name="reply-button" class="formbutton">{{lang "topic.reply_button"}}</button>
|
||||
<button form="quick_post_form" class="formbutton" id="add_poll_button">{{lang "topic.reply_add_poll_button"}}</button>
|
||||
{{if .CurrentUser.Perms.UploadFiles}}
|
||||
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
@ -8,17 +8,17 @@
|
|||
|
||||
<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">
|
||||
<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>
|
||||
{{/** 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 **/}}
|
||||
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
|
||||
{{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"}}" />
|
||||
<button name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic_update_button"}}</button>
|
||||
<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>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
@ -29,10 +29,10 @@
|
|||
{{if .Poll.ID}}
|
||||
<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">
|
||||
<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>
|
||||
<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 id="poll_voter_{{.Poll.ID}}" class="content_container poll_voter">
|
||||
<div class="topic_content user_content">
|
||||
|
@ -46,9 +46,9 @@
|
|||
</div>
|
||||
{{end}}
|
||||
<div class="poll_buttons">
|
||||
<button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">{{lang "topic_poll_vote"}}</button>
|
||||
<button class="poll_results_button" data-poll-id="{{.Poll.ID}}">{{lang "topic_poll_results"}}</button>
|
||||
<a href="#"><button class="poll_cancel_button">{{lang "topic_poll_cancel"}}</button></a>
|
||||
<button form="poll_{{.Poll.ID}}_form" class="poll_vote_button">{{lang "topic.poll_vote"}}</button>
|
||||
<button class="poll_results_button" data-poll-id="{{.Poll.ID}}">{{lang "topic.poll_results"}}</button>
|
||||
<a href="#"><button class="poll_cancel_button">{{lang "topic.poll_cancel"}}</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,91 +57,56 @@
|
|||
</div>
|
||||
</article>
|
||||
{{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"}}">
|
||||
<div class="userinfo" aria-label="{{lang "topic_userinfo_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="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>
|
||||
{{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 class="content_container">
|
||||
<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>
|
||||
<div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}">
|
||||
{{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 .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}}
|
||||
{{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 .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 .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}}
|
||||
<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>
|
||||
{{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}}
|
||||
<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>
|
||||
{{end}}
|
||||
<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>
|
||||
{{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 style="clear:both;"></div>
|
||||
</article>
|
||||
|
||||
{{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}}</div>
|
||||
{{template "topic_alt_posts.html" . }}
|
||||
</div>
|
||||
|
||||
{{if .CurrentUser.Perms.CreateReply}}
|
||||
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
|
||||
<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>
|
||||
<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 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>
|
||||
<input form="quick_post_form" name="tid" value='{{.Topic.ID}}' type="hidden" />
|
||||
<input form="quick_post_form" id="has_poll_input" name="has_poll" value="0" type="hidden" />
|
||||
<div class="formrow real_first_child">
|
||||
<div class="formitem">
|
||||
<textarea id="input_content" form="quick_post_form" name="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 class="formrow poll_content_row auto_hide">
|
||||
|
@ -149,17 +114,17 @@
|
|||
<div class="pollinput" data-pollinput="0">
|
||||
<input type="checkbox" disabled />
|
||||
<label class="pollinputlabel"></label>
|
||||
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "topic_reply_add_poll_option"}}" />
|
||||
<input form="quick_post_form" name="pollinputitem[0]" class="pollinputinput" type="text" placeholder="{{lang "topic.reply_add_poll_option"}}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formrow quick_button_row">
|
||||
<div class="formitem">
|
||||
<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" name="reply-button" class="formbutton">{{lang "topic.reply_button"}}</button>
|
||||
<button form="quick_post_form" class="formbutton" id="add_poll_button">{{lang "topic.reply_add_poll_button"}}</button>
|
||||
{{if .CurrentUser.Perms.UploadFiles}}
|
||||
<input name="upload_files" form="quick_post_form" id="upload_files" multiple type="file" style="display: none;" />
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
@ -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}}
|
|
@ -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>
|
||||
</form>
|
||||
</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>
|
||||
<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="topic_meta">
|
||||
<div class="formrow topic_board_row real_first_child">
|
||||
|
@ -68,13 +68,13 @@
|
|||
</div>
|
||||
<div class="formrow topic_name_row">
|
||||
<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 class="formrow topic_content_row">
|
||||
<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 class="formrow poll_content_row auto_hide">
|
||||
|
@ -82,19 +82,19 @@
|
|||
<div class="pollinput" data-pollinput="0">
|
||||
<input type="checkbox" disabled />
|
||||
<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 class="formrow quick_button_row">
|
||||
<div class="formitem">
|
||||
<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">{{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>
|
||||
{{if .CurrentUser.Perms.UploadFiles}}
|
||||
<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}}
|
||||
<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>
|
||||
|
@ -102,39 +102,7 @@
|
|||
{{end}}
|
||||
{{end}}
|
||||
<div id="topic_list" class="rowblock topic_list" aria-label="{{lang "topics_list_aria"}}">
|
||||
{{range .TopicList}}<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>{{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}}
|
||||
{{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>
|
||||
|
||||
{{if gt .LastPage 1}}
|
||||
|
|
|
@ -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;
|
||||
position: relative;
|
||||
}
|
||||
#dash_saved {
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
color: green;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
display: none;
|
||||
}
|
||||
.dash_security, .account_soon {
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
|
|
|
@ -694,6 +694,13 @@ textarea {
|
|||
border: 1px 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 {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1008,7 +1015,7 @@ textarea {
|
|||
display: block;
|
||||
}
|
||||
.like_count:after {
|
||||
content: "{{index .Phrases "topic_like_count_suffix"}}";
|
||||
content: "{{index .Phrases "topic.like_count_suffix"}}";
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
|
@ -1036,31 +1043,31 @@ textarea {
|
|||
}
|
||||
|
||||
.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{
|
||||
content: "{{index .Phrases "topic_edit_button_text"}}";
|
||||
content: "{{index .Phrases "topic.edit_button_text"}}";
|
||||
}
|
||||
.delete_item:after {
|
||||
content: "{{index .Phrases "topic_delete_button_text"}}";
|
||||
content: "{{index .Phrases "topic.delete_button_text"}}";
|
||||
}
|
||||
.ip_item_button:after {
|
||||
content: "{{index .Phrases "topic_ip_button_text"}}";
|
||||
content: "{{index .Phrases "topic.ip_button_text"}}";
|
||||
}
|
||||
.lock_item:after {
|
||||
content: "{{index .Phrases "topic_lock_button_text"}}";
|
||||
content: "{{index .Phrases "topic.lock_button_text"}}";
|
||||
}
|
||||
.unlock_item:after {
|
||||
content: "{{index .Phrases "topic_unlock_button_text"}}";
|
||||
content: "{{index .Phrases "topic.unlock_button_text"}}";
|
||||
}
|
||||
.pin_item:after {
|
||||
content: "{{index .Phrases "topic_pin_button_text"}}";
|
||||
content: "{{index .Phrases "topic.pin_button_text"}}";
|
||||
}
|
||||
.unpin_item:after {
|
||||
content: "{{index .Phrases "topic_unpin_button_text"}}";
|
||||
content: "{{index .Phrases "topic.unpin_button_text"}}";
|
||||
}
|
||||
.report_item:after {
|
||||
content: "{{index .Phrases "topic_report_button_text"}}";
|
||||
content: "{{index .Phrases "topic.report_button_text"}}";
|
||||
}
|
||||
|
||||
#ip_search_container .rowlist .rowitem {
|
||||
|
@ -1245,29 +1252,6 @@ textarea {
|
|||
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 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
@ -1748,7 +1732,7 @@ textarea {
|
|||
content: "";
|
||||
}
|
||||
.like_count:before {
|
||||
content: "{{index .Phrases "topic_plus"}}";
|
||||
content: "{{index .Phrases "topic.plus"}}";
|
||||
font-weight: normal;
|
||||
}
|
||||
.created_at {
|
||||
|
|
|
@ -13,9 +13,6 @@
|
|||
width: 240px;
|
||||
position: relative;
|
||||
}
|
||||
#dash_saved {
|
||||
display: none;
|
||||
}
|
||||
#dash_username {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -4,22 +4,6 @@
|
|||
--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;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
#account_dashboard .colstack_right .coldyn_block {
|
||||
display: flex;
|
||||
}
|
||||
#dash_saved {
|
||||
display: none;
|
||||
}
|
||||
#dash_left {
|
||||
padding: 18px;
|
||||
padding-right: 0px;
|
||||
|
|
|
@ -290,34 +290,34 @@ a {
|
|||
}
|
||||
|
||||
.like_label:before {
|
||||
content: "{{index .Phrases "topic_plus_one"}}";
|
||||
content: "{{index .Phrases "topic.plus_one"}}";
|
||||
}
|
||||
.edit_label:before {
|
||||
content: "{{index .Phrases "topic_edit_button_text"}}";
|
||||
content: "{{index .Phrases "topic.edit_button_text"}}";
|
||||
}
|
||||
.trash_label:before {
|
||||
content: "{{index .Phrases "topic_delete_button_text"}}";
|
||||
content: "{{index .Phrases "topic.delete_button_text"}}";
|
||||
}
|
||||
.pin_label:before {
|
||||
content: "{{index .Phrases "topic_pin_button_text"}}";
|
||||
content: "{{index .Phrases "topic.pin_button_text"}}";
|
||||
}
|
||||
.lock_label:before {
|
||||
content: "{{index .Phrases "topic_lock_button_text"}}";
|
||||
content: "{{index .Phrases "topic.lock_button_text"}}";
|
||||
}
|
||||
.unlock_label:before {
|
||||
content: "{{index .Phrases "topic_unlock_button_text"}}";
|
||||
content: "{{index .Phrases "topic.unlock_button_text"}}";
|
||||
}
|
||||
.unpin_label:before {
|
||||
content: "{{index .Phrases "topic_unpin_button_text"}}";
|
||||
content: "{{index .Phrases "topic.unpin_button_text"}}";
|
||||
}
|
||||
.ip_label:before {
|
||||
content: "{{index .Phrases "topic_ip_button_text"}}";
|
||||
content: "{{index .Phrases "topic.ip_button_text"}}";
|
||||
}
|
||||
.flag_label:before {
|
||||
content: "{{index .Phrases "topic_flag_button_text"}}";
|
||||
content: "{{index .Phrases "topic.flag_button_text"}}";
|
||||
}
|
||||
.level_label:before {
|
||||
content: "{{index .Phrases "topic_level"}}";
|
||||
content: "{{index .Phrases "topic.level"}}";
|
||||
}
|
||||
|
||||
.like_count_label, .like_count {
|
||||
|
@ -896,7 +896,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
|
|||
font-weight: normal;
|
||||
}
|
||||
#profile_left_pane .report_item:after {
|
||||
content: "{{index .Phrases "topic_report_button_text"}}";
|
||||
content: "{{index .Phrases "topic.report_button_text"}}";
|
||||
}
|
||||
#profile_left_lane .profileName {
|
||||
font-size: 18px;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.sidebar, #dash_saved {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
#account_dashboard .colstack_right .coldyn_block {
|
||||
|
|
|
@ -715,7 +715,7 @@ button.username {
|
|||
content: "😀";
|
||||
}
|
||||
.like_count:after {
|
||||
content: "{{index .Phrases "topic_gap_up"}}";
|
||||
content: "{{index .Phrases "topic.gap_up"}}";
|
||||
}
|
||||
.edit_label:before {
|
||||
content: "🖊️";
|
||||
|
@ -763,7 +763,7 @@ button.username {
|
|||
font-weight: normal;
|
||||
}
|
||||
#profile_left_pane .report_item:after {
|
||||
content: "{{index .Phrases "topic_report_button_text"}}";
|
||||
content: "{{index .Phrases "topic.report_button_text"}}";
|
||||
}
|
||||
#profile_right_lane {
|
||||
width: calc(100% - 230px);
|
||||
|
@ -959,28 +959,28 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
|
|||
}
|
||||
|
||||
.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 {
|
||||
content: "{{index .Phrases "topic_edit_button_text"}}";
|
||||
content: "{{index .Phrases "topic.edit_button_text"}}";
|
||||
}
|
||||
.delete_item:after {
|
||||
content: "{{index .Phrases "topic_delete_button_text"}}";
|
||||
content: "{{index .Phrases "topic.delete_button_text"}}";
|
||||
}
|
||||
.lock_item:after {
|
||||
content: "{{index .Phrases "topic_lock_button_text"}}";
|
||||
content: "{{index .Phrases "topic.lock_button_text"}}";
|
||||
}
|
||||
.unlock_item:after {
|
||||
content: "{{index .Phrases "topic_unlock_button_text"}}";
|
||||
content: "{{index .Phrases "topic.unlock_button_text"}}";
|
||||
}
|
||||
.pin_item:after {
|
||||
content: "{{index .Phrases "topic_pin_button_text"}}";
|
||||
content: "{{index .Phrases "topic.pin_button_text"}}";
|
||||
}
|
||||
.unpin_item:after {
|
||||
content: "{{index .Phrases "topic_unpin_button_text"}}";
|
||||
content: "{{index .Phrases "topic.unpin_button_text"}}";
|
||||
}
|
||||
.report_item:after {
|
||||
content: "{{index .Phrases "topic_report_button_text"}}";
|
||||
content: "{{index .Phrases "topic.report_button_text"}}";
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.sidebar, #dash_saved {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -895,7 +895,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel {
|
|||
font-size: 18px;
|
||||
}
|
||||
#profile_left_lane .report_item:after {
|
||||
content: "{{index .Phrases "topic_report_button_text"}}";
|
||||
content: "{{index .Phrases "topic.report_button_text"}}";
|
||||
}
|
||||
#profile_right_lane {
|
||||
width: calc(100% - 245px);
|
||||
|
|
Loading…
Reference in New Issue