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

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

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

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

View File

@ -2,7 +2,9 @@
rem TODO: Make these deletes a little less noisy
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

View File

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

View File

@ -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")

View File

@ -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 {

View File

@ -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
}

View File

@ -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++

View File

@ -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

View File

@ -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) {

View File

@ -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 + "]}"
}

View File

@ -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
}

View File

@ -149,7 +149,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
stats.Reports = 0 // TODO: Do the report count. Only show open threads?
// 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
}

View File

@ -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

View File

@ -122,25 +122,15 @@ var Template_ip_search_handle = func(pi IPSearchPage, w io.Writer) error {
return Templates.ExecuteTemplate(w, mapping+".html", pi)
}
// ? - 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
}

View File

@ -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())

View File

@ -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
}

View File

@ -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()

View File

@ -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/

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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":"&lt;",
"paginator_greater_than":"&gt;",

View File

@ -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()
//}

View File

@ -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;

View File

@ -447,14 +447,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO: Cover more suspicious strings and at a lower layer than this
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

View File

@ -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"),

View File

@ -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
}

View File

@ -81,7 +81,7 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid st
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
topicItem.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

View File

@ -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 = ""

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}}" />

View File

@ -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"}}"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
{{if .IsClosed}}<span class="rowsmall topic_status_e topic_status_closed" title="{{lang "status.closed_tooltip"}}"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status.pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span>
{{/** TODO: Phase this out of Cosora and remove it **/}}
<div class="topic_inner_right rowsmall">

View File

@ -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>

View File

@ -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"}}'>&#x1F512;&#xFE0E</span>{{end}}
{{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit' title='{{lang "status.closed_tooltip"}}' aria-label='{{lang "topic.status_closed_aria"}}'>&#x1F512;&#xFE0E</span>{{end}}
{{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}}
{{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>&nbsp;&nbsp;
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="mod_button"{{if .Topic.Liked}} title="{{lang "topic_unlike_tooltip"}}" aria-label="{{lang "topic_unlike_aria"}}"{{else}} title="{{lang "topic_like_tooltip"}}" aria-label="{{lang "topic_like_aria"}}"{{end}} style="color:#202020;">
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="mod_button"{{if .Topic.Liked}} title="{{lang "topic.unlike_tooltip"}}" aria-label="{{lang "topic.unlike_aria"}}"{{else}} title="{{lang "topic.like_tooltip"}}" aria-label="{{lang "topic.like_aria"}}"{{end}} style="color:#202020;">
<button class="username like_label {{if .Topic.Liked}}remove_like{{else}}add_like{{end}}"></button></a>{{end}}
{{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>&nbsp;&nbsp;
{{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic_post_like_tooltip"}}" aria-label="{{lang "topic_post_like_aria"}}" style="color:#202020;"><button class="username like_label remove_like"></button></a>{{else}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic_post_unlike_tooltip"}}" aria-label="{{lang "topic_post_unlike_aria"}}" style="color:#202020;"><button class="username like_label add_like"></button></a>{{end}}{{end}}
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic_post_edit_tooltip"}}" aria-label="{{lang "topic_post_edit_aria"}}"><button class="username edit_item edit_label"></button></a>{{end}}
{{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "topic_post_delete_tooltip"}}" aria-label="{{lang "topic_post_delete_aria"}}"><button class="username delete_item trash_label"></button></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' style="font-weight:normal;" title="{{lang "topic_post_ip_tooltip"}}" aria-label="The poster's IP is {{.IPAddress}}"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="{{lang "topic_post_flag_tooltip"}}" aria-label="{{lang "topic_post_flag_aria"}}" rel="nofollow"><button class="username report_item flag_label"></button></a>
<a class="username hide_on_micro like_count">{{.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="{{lang "topic_post_like_count_tooltip"}}"></a>
{{if .Tag}}<a class="username hide_on_micro user_tag">{{.Tag}}</a>{{else}}<a class="username hide_on_micro level" aria-label="{{lang "topic_post_level_aria"}}">{{.Level}}</a><a class="username hide_on_micro level_label" style="float:right;" title="{{lang "topic_post_level_tooltip"}}"></a>{{end}}
</span>
</article>
{{end}}{{end}}</div>
{{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>

View File

@ -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;">&#x1F512;&#xFE0E</span>{{end}}
{{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit' title='{{lang "status.closed_tooltip"}}' aria-label='{{lang "topic.status_closed_aria"}}' style="font-weight:normal;float: right;position:relative;top:-5px;">&#x1F512;&#xFE0E</span>{{end}}
{{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}}
{{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;">&nbsp;</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;">&nbsp;</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;">&nbsp;</div>
<a href="{{.UserLink}}" class="the_name" rel="author">{{.CreatedByName}}</a>
{{if .Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">{{lang "topic_level_prefix"}}{{.Level}}</div><div class="tag_post"></div></div>{{end}}
</div>
<div class="content_container" {{if .ActionType}}style="margin-left: 0px;"{{end}}>
{{if .ActionType}}
<span class="action_icon" style="font-size: 18px;padding-right: 5px;" aria-hidden="true">{{.ActionIcon}}</span>
<span itemprop="text">{{.ActionType}}</span>
{{else}}
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<div class="editable_block user_content" itemprop="text">{{.ContentHtml}}</div>
<div class="controls button_container{{if .LikeCount}} has_likes{{end}}">
{{if $.CurrentUser.Loggedin}}
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button like_item {{if .Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic_post_like_aria"}}" data-action="like"></a>{{end}}
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button edit_item" aria-label="{{lang "topic_post_edit_aria"}}" data-action="edit"></a>{{end}}
{{end}}
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button delete_item" aria-label="{{lang "topic_post_delete_aria"}}" data-action="delete"></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="{{lang "topic_ip_full_tooltip"}}" class="action_button ip_item_button hide_on_big" aria-label="{{lang "topic_ip_full_aria"}}" data-action="ip"></a>{{end}}
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="action_button report_item" aria-label="{{lang "topic_report_aria"}}" data-action="report"></a>
<a href="#" class="action_button button_menu"></a>
{{end}}
<div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic_post_like_count_tooltip"}}">{{.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile">{{.RelativeCreatedAt}}</a>
{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}
</div>
</div>
{{end}}
</div>
<div style="clear:both;"></div>
</article>
{{end}}</div>
{{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;">&nbsp;</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>

View File

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

View File

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

View File

@ -55,10 +55,10 @@
</div>
</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"}}"> | &#x1F512;&#xFE0E</span>{{end}}
{{if .Sticky}}<span class="rowsmall topic_status_e topic_status_sticky" title="{{lang "status_pinned_tooltip"}}"> | &#x1F4CD;&#xFE0E</span>{{end}}
</span>
{{/** TODO: Phase this out of Cosora and remove it **/}}
<div class="topic_inner_right rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div>
<div class="topic_middle">
<div class="topic_middle_inside rowsmall">
<span class="replyCount">{{.PostCount}}</span><br />
<span class="likeCount">{{.LikeCount}}</span>
</div>
</div>
<div class="rowitem topic_right passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}} topic_closed{{end}}">
<div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.Avatar}}" height="64" alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span>
<a href="{{.LastUser.Link}}" class="lastName" style="font-size: 14px;" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<span class="rowsmall lastReplyAt">{{.RelativeLastReplyAt}}</span>
</span>
</div>
</div>
</div>{{else}}<div class="rowitem passive rowmsg">{{lang "topics_no_topics"}}{{if .CurrentUser.Perms.CreateTopic}} <a href="/topics/create/">{{lang "topics_start_one"}}</a>{{end}}</div>{{end}}
{{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}}

View File

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

View File

@ -15,15 +15,6 @@
height: 184px;
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;

View File

@ -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 {

View File

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

View File

@ -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;
}

View File

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

View File

@ -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;

View File

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

View File

@ -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 {

View File

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

View File

@ -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);