diff --git a/common/extend.go b/common/extend.go index c681d251..221aa582 100644 --- a/common/extend.go +++ b/common/extend.go @@ -42,6 +42,15 @@ var VhookSkippable = map[string]func(...interface{}) (bool, RouteError){ //var vhookErrorable = map[string]func(...interface{}) (interface{}, RouteError){} +var taskHooks = map[string][]func() error{ + "before_half_second_tick": nil, + "after_half_second_tick": nil, + "before_second_tick": nil, + "after_second_tick": nil, + "before_fifteen_minute_tick": nil, + "after_fifteen_minute_tick": nil, +} + // Coming Soon: type Message interface { ID() int @@ -225,6 +234,7 @@ func NewPlugin(uname string, name string, author string, url string, settings st } // ? - Is this racey? +// TODO: Generate the cases in this switch func (plugin *Plugin) AddHook(name string, handler interface{}) { switch h := handler.(type) { case func(interface{}) interface{}: @@ -254,6 +264,15 @@ func (plugin *Plugin) AddHook(name string, handler interface{}) { PreRenderHooks[name] = append(PreRenderHooks[name], h) } plugin.Hooks[name] = len(PreRenderHooks[name]) + case func() error: // ! We might want a more generic name, as we might use this signature for things other than tasks hooks + if len(taskHooks[name]) == 0 { + var hookSlice []func() error + hookSlice = append(hookSlice, h) + taskHooks[name] = hookSlice + } else { + taskHooks[name] = append(taskHooks[name], h) + } + plugin.Hooks[name] = len(taskHooks[name]) case func(...interface{}) interface{}: Vhooks[name] = h plugin.Hooks[name] = 0 @@ -266,6 +285,7 @@ func (plugin *Plugin) AddHook(name string, handler interface{}) { } // ? - Is this racey? +// TODO: Generate the cases in this switch func (plugin *Plugin) RemoveHook(name string, handler interface{}) { switch handler.(type) { case func(interface{}) interface{}: @@ -295,6 +315,15 @@ func (plugin *Plugin) RemoveHook(name string, handler interface{}) { hook = append(hook[:key], hook[key+1:]...) } PreRenderHooks[name] = hook + case func() error: + key := plugin.Hooks[name] + hook := taskHooks[name] + if len(hook) == 1 { + hook = []func() error{} + } else { + hook = append(hook[:key], hook[key+1:]...) + } + taskHooks[name] = hook case func(...interface{}) interface{}: delete(Vhooks, name) case func(...interface{}) (bool, RouteError): @@ -340,7 +369,11 @@ func RunHookNoreturn(name string, data interface{}) { } func RunVhook(name string, data ...interface{}) interface{} { - return Vhooks[name](data...) + hook := Vhooks[name] + if hook != nil { + return hook(data...) + } + return nil } func RunVhookSkippable(name string, data ...interface{}) (bool, RouteError) { @@ -348,7 +381,29 @@ func RunVhookSkippable(name string, data ...interface{}) (bool, RouteError) { } func RunVhookNoreturn(name string, data ...interface{}) { - _ = Vhooks[name](data...) + hook := Vhooks[name] + if hook != nil { + _ = hook(data...) + } +} + +// TODO: Find a better way of doing this +func RunVhookNeedHook(name string, data ...interface{}) (ret interface{}, hasHook bool) { + hook := Vhooks[name] + if hook != nil { + return hook(data...), true + } + return nil, false +} + +func RunTaskHook(name string) error { + for _, hook := range taskHooks[name] { + err := hook() + if err != nil { + return err + } + } + return nil } // Trying to get a teeny bit of type-safety where-ever possible, especially for such a critical set of hooks @@ -360,14 +415,14 @@ func RunSshook(name string, data string) string { } func RunPreRenderHook(name string, w http.ResponseWriter, r *http.Request, user *User, data interface{}) (halt bool) { - // This hook runs on ALL pre_render hooks + // This hook runs on ALL PreRender hooks for _, hook := range PreRenderHooks["pre_render"] { if hook(w, r, user, data) { return true } } - // The actual pre_render hook + // The actual PreRender hook for _, hook := range PreRenderHooks[name] { if hook(w, r, user, data) { return true diff --git a/common/group_store.go b/common/group_store.go index 8a0b1761..06f1aac7 100644 --- a/common/group_store.go +++ b/common/group_store.go @@ -232,9 +232,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod var blankIntList []int var pluginPerms = make(map[string]bool) var pluginPermsBytes = []byte("{}") - if Vhooks["create_group_preappend"] != nil { - RunVhook("create_group_preappend", &pluginPerms, &pluginPermsBytes) - } + RunVhook("create_group_preappend", &pluginPerms, &pluginPermsBytes) // Generate the forum permissions based on the presets... fdata, err := Forums.GetAll() diff --git a/common/pages.go b/common/pages.go index 4576b17c..d7499be4 100644 --- a/common/pages.go +++ b/common/pages.go @@ -79,6 +79,7 @@ type ForumPage struct { Header *HeaderVars ItemList []*TopicsRow Forum *Forum + PageList []int Page int LastPage int } @@ -218,15 +219,15 @@ type PanelAnalyticsRoutePage struct { } type PanelAnalyticsAgentPage struct { - Title string - CurrentUser User - Header *HeaderVars - Stats PanelStats - Zone string - Agent string + Title string + CurrentUser User + Header *HeaderVars + Stats PanelStats + Zone string + Agent string FriendlyAgent string - PrimaryGraph PanelTimeGraph - TimeRange string + PrimaryGraph PanelTimeGraph + TimeRange string } type PanelThemesPage struct { diff --git a/common/poll_store.go b/common/poll_store.go index 0275d878..d5ac3f3e 100644 --- a/common/poll_store.go +++ b/common/poll_store.go @@ -3,6 +3,9 @@ package common import ( "database/sql" "encoding/json" + "errors" + "log" + "strconv" "../query_gen/lib" ) @@ -116,6 +119,92 @@ func (store *DefaultPollStore) Get(id int) (*Poll, error) { return poll, err } +// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts? +// TODO: ID of 0 should always error? +func (store *DefaultPollStore) BulkGetMap(ids []int) (list map[int]*Poll, err error) { + var idCount = len(ids) + list = make(map[int]*Poll) + if idCount == 0 { + return list, nil + } + + var stillHere []int + sliceList := store.cache.BulkGet(ids) + for i, sliceItem := range sliceList { + if sliceItem != nil { + list[sliceItem.ID] = sliceItem + } else { + stillHere = append(stillHere, ids[i]) + } + } + ids = stillHere + + // If every user is in the cache, then return immediately + if len(ids) == 0 { + return list, nil + } + + // TODO: Add a function for the qlist stuff + var qlist string + var pollIDList []interface{} + for _, id := range ids { + pollIDList = append(pollIDList, strconv.Itoa(id)) + qlist += "?," + } + qlist = qlist[0 : len(qlist)-1] + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("polls").Columns("pollID, parentID, parentTable, type, options, votes").Where("pollID IN(" + qlist + ")").Query(pollIDList...) + if err != nil { + return list, err + } + + for rows.Next() { + poll := &Poll{ID: 0} + var optionTxt []byte + err := rows.Scan(&poll.ID, &poll.ParentID, &poll.ParentTable, &poll.Type, &optionTxt, &poll.VoteCount) + if err != nil { + return list, err + } + + err = json.Unmarshal(optionTxt, &poll.Options) + if err != nil { + return list, err + } + poll.QuickOptions = store.unpackOptionsMap(poll.Options) + store.cache.Set(poll) + + list[poll.ID] = poll + } + + // Did we miss any polls? + if idCount > len(list) { + var sidList string + for _, id := range ids { + _, ok := list[id] + if !ok { + sidList += strconv.Itoa(id) + "," + } + } + + // We probably don't need this, but it might be useful in case of bugs in BulkCascadeGetMap + if sidList == "" { + if Dev.DebugMode { + log.Print("This data is sampled later in the BulkCascadeGetMap function, so it might miss the cached IDs") + log.Print("idCount", idCount) + log.Print("ids", ids) + log.Print("list", list) + } + return list, errors.New("We weren't able to find a poll, but we don't know which one") + } + sidList = sidList[0 : len(sidList)-1] + + err = errors.New("Unable to find the polls with the following IDs: " + sidList) + } + + return list, err +} + func (store *DefaultPollStore) Reload(id int) error { poll := &Poll{ID: id} var optionTxt []byte diff --git a/common/site.go b/common/site.go index b792f2ed..04f14b0c 100644 --- a/common/site.go +++ b/common/site.go @@ -67,7 +67,7 @@ type config struct { StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS DefaultForum int // The forum posts go in by default, this used to be covered by the Uncategorised Forum, but we want to replace it with a more robust solution. Make this a setting? MinifyTemplates bool - MultiServer bool + ServerCount int Noavatar string // ? - Move this into the settings table? ItemsPerPage int // ? - Move this into the settings table? @@ -104,6 +104,9 @@ func VerifyConfig() error { if !Forums.Exists(Config.DefaultForum) { return errors.New("Invalid default forum") } + if Config.ServerCount < 1 { + return errors.New("You can't have less than one server") + } return nil } diff --git a/common/tasks.go b/common/tasks.go index c7c616b6..aeaabde7 100644 --- a/common/tasks.go +++ b/common/tasks.go @@ -85,7 +85,13 @@ func HandleExpiredScheduledGroups() error { } // TODO: Use AddScheduledSecondTask +// TODO: Be a little more granular with the synchronisation func HandleServerSync() error { + // We don't want to run any unnecessary queries when there is nothing to synchronise + /*if Config.ServerCount > 1 { + return nil + }*/ + var lastUpdate time.Time err := taskStmts.getSync.QueryRow().Scan(&lastUpdate) if err != nil { @@ -93,7 +99,6 @@ func HandleServerSync() error { } if lastUpdate.After(lastSync) { - // TODO: A more granular sync err = Forums.LoadForums() if err != nil { log.Print("Unable to reload the forums") diff --git a/common/template_init.go b/common/template_init.go index 46aa4340..30a98dbe 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -162,7 +162,7 @@ func compileTemplates() error { //var topicList []TopicUser //topicList = append(topicList,TopicUser{1,"topic-title","Topic Title","The topic content.",1,false,false,"Date","Date",1,"","127.0.0.1",0,1,"classname","","admin-fred","Admin Fred",config.DefaultGroup,"",0,"","","","",58,false}) forumItem := BlankForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0) - forumPage := ForumPage{"General Forum", user, headerVars, topicsList, forumItem, 1, 1} + forumPage := ForumPage{"General Forum", user, headerVars, topicsList, forumItem, []int{1}, 1, 1} forumTmpl, err := c.Compile("forum.html", "templates/", "common.ForumPage", forumPage, varList) if err != nil { return err diff --git a/common/topic_list.go b/common/topic_list.go index f5eaac5e..53787933 100644 --- a/common/topic_list.go +++ b/common/topic_list.go @@ -122,6 +122,10 @@ func (tList *DefaultTopicList) Tick() error { if err != nil { return err } + if group.UserCount == 0 { + continue + } + topicList, forumList, pageList, page, lastPage, err := tList.getListByGroup(group, 1) if err != nil { return err @@ -258,9 +262,7 @@ func (tList *DefaultTopicList) getList(page int, argList []interface{}, qlist st topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt) // TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/ - if Vhooks["topics_topic_row_assign"] != nil { - RunVhook("topics_topic_row_assign", &topicItem, &forum) - } + RunVhook("topics_topic_row_assign", &topicItem, &forum) topicList = append(topicList, &topicItem) reqUserList[topicItem.CreatedBy] = true reqUserList[topicItem.LastReplyBy] = true diff --git a/common/user_store.go b/common/user_store.go index 494a461e..2fd8a514 100644 --- a/common/user_store.go +++ b/common/user_store.go @@ -128,14 +128,14 @@ func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err erro acc := qgen.Builder.Accumulator() rows, err := acc.Select("users").Columns("uid, name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group").Where("uid IN(" + qlist + ")").Query(uidList...) if err != nil { - return nil, err + return list, err } for rows.Next() { user := &User{Loggedin: true} err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) if err != nil { - return nil, err + return list, err } user.Init() diff --git a/config.go b/config.go index 5bb60486..3203f775 100644 --- a/config.go +++ b/config.go @@ -51,7 +51,7 @@ func init() { common.Config.StaffCSS = "staff_post" common.Config.DefaultForum = 2 common.Config.MinifyTemplates = true - common.Config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features + common.Config.ServerCount = 1 // Experimental: Enable Cross-Server Synchronisation and several other features //common.Config.Noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png" common.Config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png" diff --git a/install/install.go b/install/install.go index 13bbb01b..abe78bee 100644 --- a/install/install.go +++ b/install/install.go @@ -148,7 +148,7 @@ func init() { common.Config.StaffCSS = "staff_post" common.Config.DefaultForum = 2 common.Config.MinifyTemplates = true - common.Config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features + common.Config.ServerCount = 1 // Experimental: Enable Cross-Server Synchronisation and several other features //common.Config.Noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png" common.Config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png" diff --git a/main.go b/main.go index 2aa8a27f..104805eb 100644 --- a/main.go +++ b/main.go @@ -289,14 +289,20 @@ func main() { fifteenMinuteTicker := time.NewTicker(15 * time.Minute) //hourTicker := time.NewTicker(1 * time.Hour) go func() { + var runHook = func(name string) { + err := common.RunTaskHook(name) + if err != nil { + common.LogError(err) + } + } for { select { case <-halfSecondTicker.C: - // TODO: Add a plugin hook here + runHook("before_half_second_tick") runTasks(common.ScheduledHalfSecondTasks) - // TODO: Add a plugin hook here + runHook("after_half_second_tick") case <-secondTicker.C: - // TODO: Add a plugin hook here + runHook("before_second_tick") runTasks(common.ScheduledSecondTasks) // TODO: Stop hard-coding this @@ -317,16 +323,14 @@ func main() { // TODO: Alert the admin, if CPU usage, RAM usage, or the number of posts in the past second are too high // TODO: Clean-up alerts with no unread matches which are over two weeks old. Move this to a 24 hour task? // TODO: Rescan the static files for changes - - // TODO: Add a plugin hook here + runHook("after_second_tick") case <-fifteenMinuteTicker.C: - // TODO: Add a plugin hook here + runHook("before_fifteen_minute_tick") runTasks(common.ScheduledFifteenMinuteTasks) // TODO: Automatically lock topics, if they're really old, and the associated setting is enabled. // TODO: Publish scheduled posts. - - // TODO: Add a plugin hook here + runHook("after_fifteen_minute_tick") } // TODO: Handle the daily clean-up. diff --git a/member_routes.go b/member_routes.go index 6dd43aba..742f937e 100644 --- a/member_routes.go +++ b/member_routes.go @@ -405,10 +405,11 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user common.User, title = "Topic: " + title content = content + "\n\nOriginal Post: #tid-" + strconv.Itoa(itemID) } else { - if common.Vhooks["report_preassign"] != nil { - common.RunVhookNoreturn("report_preassign", &itemID, &itemType) + _, hasHook := common.RunVhookNeedHook("report_preassign", &itemID, &itemType) + if hasHook { return nil } + // Don't try to guess the type return common.LocalError("Unknown type", w, r, user) } diff --git a/routes.go b/routes.go index 4b658305..25d79803 100644 --- a/routes.go +++ b/routes.go @@ -97,9 +97,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid s topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt) - if common.Vhooks["forum_trow_assign"] != nil { - common.RunVhook("forum_trow_assign", &topicItem, &forum) - } + common.RunVhook("forum_trow_assign", &topicItem, &forum) topicList = append(topicList, &topicItem) reqUserList[topicItem.CreatedBy] = true reqUserList[topicItem.LastReplyBy] = true @@ -130,7 +128,8 @@ func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid s topicItem.LastUser = userList[topicItem.LastReplyBy] } - pi := common.ForumPage{forum.Name, user, headerVars, topicList, forum, page, lastPage} + pageList := common.Paginate(forum.TopicCount, common.Config.ItemsPerPage, 5) + pi := common.ForumPage{forum.Name, user, headerVars, topicList, forum, pageList, page, lastPage} if common.PreRenderHooks["pre_render_forum"] != nil { if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) { return nil diff --git a/routes/topic.go b/routes/topic.go index 50e7e4b5..037e55dd 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -37,8 +37,6 @@ func init() { var successJSONBytes = []byte(`{"success":"1"}`) func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit string) common.RouteError { - var err error - var replyList []common.ReplyUser page, _ := strconv.Atoi(r.FormValue("page")) // SEO URLs... @@ -110,82 +108,80 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit // Calculate the offset offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) - tpage := common.TopicPage{topic.Title, user, headerVars, replyList, topic, poll, page, lastPage} + tpage := common.TopicPage{topic.Title, user, headerVars, []common.ReplyUser{}, topic, poll, page, lastPage} - // Get the replies.. - rows, err := topicStmts.getReplies.Query(topic.ID, offset, common.Config.ItemsPerPage) - if err == sql.ErrNoRows { - return common.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - defer rows.Close() - - replyItem := common.ReplyUser{ClassName: ""} - for rows.Next() { - err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType) - if err != nil { + // Get the replies if we have any... + if topic.PostCount > 0 { + rows, err := topicStmts.getReplies.Query(topic.ID, offset, common.Config.ItemsPerPage) + if err == sql.ErrNoRows { + return common.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) + } else if err != nil { return common.InternalError(err, w, r) } + defer rows.Close() - replyItem.UserLink = common.BuildProfileURL(common.NameToSlug(replyItem.CreatedByName), replyItem.CreatedBy) - replyItem.ParentID = topic.ID - replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums") - replyItem.ContentLines = strings.Count(replyItem.Content, "\n") - - postGroup, err = common.Groups.Get(replyItem.Group) - if err != nil { - return common.InternalError(err, w, r) - } - - if postGroup.IsMod || postGroup.IsAdmin { - replyItem.ClassName = common.Config.StaffCSS - } else { - replyItem.ClassName = "" - } - - // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this? - replyItem.Avatar = common.BuildAvatar(replyItem.CreatedBy, replyItem.Avatar) - replyItem.Tag = postGroup.Tag - replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt) - - // We really shouldn't have inline HTML, we should do something about this... - if replyItem.ActionType != "" { - switch replyItem.ActionType { - case "lock": - replyItem.ActionType = "This topic has been locked by " + replyItem.CreatedByName + "" - replyItem.ActionIcon = "🔒︎" - case "unlock": - replyItem.ActionType = "This topic has been reopened by " + replyItem.CreatedByName + "" - replyItem.ActionIcon = "🔓︎" - case "stick": - replyItem.ActionType = "This topic has been pinned by " + replyItem.CreatedByName + "" - replyItem.ActionIcon = "📌︎" - case "unstick": - replyItem.ActionType = "This topic has been unpinned by " + replyItem.CreatedByName + "" - replyItem.ActionIcon = "📌︎" - case "move": - replyItem.ActionType = "This topic has been moved by " + replyItem.CreatedByName + "" - default: - replyItem.ActionType = replyItem.ActionType + " has happened" - replyItem.ActionIcon = "" + replyItem := common.ReplyUser{ClassName: ""} + for rows.Next() { + err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType) + if err != nil { + return common.InternalError(err, w, r) } - } - replyItem.Liked = false - if common.Vhooks["topic_reply_row_assign"] != nil { + replyItem.UserLink = common.BuildProfileURL(common.NameToSlug(replyItem.CreatedByName), replyItem.CreatedBy) + replyItem.ParentID = topic.ID + replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums") + replyItem.ContentLines = strings.Count(replyItem.Content, "\n") + + postGroup, err = common.Groups.Get(replyItem.Group) + if err != nil { + return common.InternalError(err, w, r) + } + + if postGroup.IsMod || postGroup.IsAdmin { + replyItem.ClassName = common.Config.StaffCSS + } else { + replyItem.ClassName = "" + } + + // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this? + replyItem.Avatar = common.BuildAvatar(replyItem.CreatedBy, replyItem.Avatar) + replyItem.Tag = postGroup.Tag + replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt) + + // We really shouldn't have inline HTML, we should do something about this... + if replyItem.ActionType != "" { + switch replyItem.ActionType { + case "lock": + replyItem.ActionType = "This topic has been locked by " + replyItem.CreatedByName + "" + replyItem.ActionIcon = "🔒︎" + case "unlock": + replyItem.ActionType = "This topic has been reopened by " + replyItem.CreatedByName + "" + replyItem.ActionIcon = "🔓︎" + case "stick": + replyItem.ActionType = "This topic has been pinned by " + replyItem.CreatedByName + "" + replyItem.ActionIcon = "📌︎" + case "unstick": + replyItem.ActionType = "This topic has been unpinned by " + replyItem.CreatedByName + "" + replyItem.ActionIcon = "📌︎" + case "move": + replyItem.ActionType = "This topic has been moved by " + replyItem.CreatedByName + "" + default: + replyItem.ActionType = replyItem.ActionType + " has happened" + replyItem.ActionIcon = "" + } + } + replyItem.Liked = false + common.RunVhook("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) + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) } - //replyList = append(replyList, 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) - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) } - //tpage.ItemList = replyList if common.PreRenderHooks["pre_render_view_topic"] != nil { if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) { return nil @@ -195,7 +191,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit if err != nil { return common.InternalError(err, w, r) } - common.TopicViewCounter.Bump(topic.ID) // TODO Move this into the router? + common.TopicViewCounter.Bump(topic.ID) // TODO: Move this into the router? return nil } @@ -228,9 +224,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 - if common.Vhooks["topic_create_pre_loop"] != nil { - common.RunVhook("topic_create_pre_loop", w, r, fid, &headerVars, &user, &strictmode) - } + common.RunVhook("topic_create_pre_loop", w, r, fid, &headerVars, &user, &strictmode) // TODO: Re-add support for plugin_guilds var forumList []common.Forum diff --git a/template_forum.go b/template_forum.go index 95267661..96befd50 100644 --- a/template_forum.go +++ b/template_forum.go @@ -3,9 +3,9 @@ // Code generated by Gosora. More below: /* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */ package main -import "strconv" import "net/http" import "./common" +import "strconv" // nolint func init() { @@ -93,118 +93,142 @@ w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) w.Write(forum_4) w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Page + 1))) w.Write(forum_5) -w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) +} w.Write(forum_6) -w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Page + 1))) +if tmpl_forum_vars.CurrentUser.ID != 0 { w.Write(forum_7) } w.Write(forum_8) -if tmpl_forum_vars.CurrentUser.ID != 0 { -w.Write(forum_9) -} -w.Write(forum_10) w.Write([]byte(tmpl_forum_vars.Title)) +w.Write(forum_9) +if tmpl_forum_vars.CurrentUser.ID != 0 { +if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { +w.Write(forum_10) +w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) w.Write(forum_11) -if tmpl_forum_vars.CurrentUser.ID != 0 { -if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { w.Write(forum_12) -w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) -w.Write(forum_13) -w.Write(forum_14) } else { +w.Write(forum_13) +} +w.Write(forum_14) +} w.Write(forum_15) -} -w.Write(forum_16) -} -w.Write(forum_17) if tmpl_forum_vars.CurrentUser.ID != 0 { -w.Write(forum_18) +w.Write(forum_16) if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { -w.Write(forum_19) +w.Write(forum_17) w.Write([]byte(tmpl_forum_vars.CurrentUser.Avatar)) -w.Write(forum_20) +w.Write(forum_18) w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) -w.Write(forum_21) +w.Write(forum_19) if tmpl_forum_vars.CurrentUser.Perms.UploadFiles { +w.Write(forum_20) +} +w.Write(forum_21) +} +} w.Write(forum_22) -} -w.Write(forum_23) -} -} -w.Write(forum_24) if len(tmpl_forum_vars.ItemList) != 0 { for _, item := range tmpl_forum_vars.ItemList { -w.Write(forum_25) +w.Write(forum_23) w.Write([]byte(strconv.Itoa(item.ID))) +w.Write(forum_24) +if item.Sticky { +w.Write(forum_25) +} else { +if item.IsClosed { w.Write(forum_26) -if item.Sticky { +} +} w.Write(forum_27) -} else { -if item.IsClosed { +w.Write([]byte(item.Creator.Link)) w.Write(forum_28) -} -} -w.Write(forum_29) -w.Write([]byte(item.Creator.Link)) -w.Write(forum_30) w.Write([]byte(item.Creator.Avatar)) +w.Write(forum_29) +w.Write([]byte(item.Creator.Name)) +w.Write(forum_30) +w.Write([]byte(item.Creator.Name)) w.Write(forum_31) -w.Write([]byte(item.Creator.Name)) -w.Write(forum_32) -w.Write([]byte(item.Creator.Name)) -w.Write(forum_33) w.Write([]byte(item.Link)) -w.Write(forum_34) +w.Write(forum_32) w.Write([]byte(item.Title)) -w.Write(forum_35) +w.Write(forum_33) w.Write([]byte(item.Creator.Link)) -w.Write(forum_36) +w.Write(forum_34) w.Write([]byte(item.Creator.Name)) -w.Write(forum_37) +w.Write(forum_35) if item.IsClosed { +w.Write(forum_36) +} +if item.Sticky { +w.Write(forum_37) +} w.Write(forum_38) -} -if item.Sticky { -w.Write(forum_39) -} -w.Write(forum_40) w.Write([]byte(strconv.Itoa(item.PostCount))) -w.Write(forum_41) +w.Write(forum_39) w.Write([]byte(strconv.Itoa(item.LikeCount))) -w.Write(forum_42) +w.Write(forum_40) if item.Sticky { -w.Write(forum_43) +w.Write(forum_41) } else { if item.IsClosed { -w.Write(forum_44) +w.Write(forum_42) } } -w.Write(forum_45) +w.Write(forum_43) w.Write([]byte(item.LastUser.Link)) -w.Write(forum_46) +w.Write(forum_44) w.Write([]byte(item.LastUser.Avatar)) -w.Write(forum_47) +w.Write(forum_45) w.Write([]byte(item.LastUser.Name)) +w.Write(forum_46) +w.Write([]byte(item.LastUser.Name)) +w.Write(forum_47) +w.Write([]byte(item.LastUser.Link)) w.Write(forum_48) w.Write([]byte(item.LastUser.Name)) w.Write(forum_49) -w.Write([]byte(item.LastUser.Link)) -w.Write(forum_50) -w.Write([]byte(item.LastUser.Name)) -w.Write(forum_51) w.Write([]byte(item.RelativeLastReplyAt)) -w.Write(forum_52) +w.Write(forum_50) } } else { -w.Write(forum_53) +w.Write(forum_51) if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { -w.Write(forum_54) +w.Write(forum_52) w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) +w.Write(forum_53) +} +w.Write(forum_54) +} w.Write(forum_55) -} +if tmpl_forum_vars.LastPage > 1 { w.Write(forum_56) -} +if tmpl_forum_vars.Page > 1 { w.Write(forum_57) +w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Page - 1))) +w.Write(forum_58) +w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Page - 1))) +w.Write(forum_59) +} +if len(tmpl_forum_vars.PageList) != 0 { +for _, item := range tmpl_forum_vars.PageList { +w.Write(forum_60) +w.Write([]byte(strconv.Itoa(item))) +w.Write(forum_61) +w.Write([]byte(strconv.Itoa(item))) +w.Write(forum_62) +} +} +if tmpl_forum_vars.LastPage != tmpl_forum_vars.Page { +w.Write(forum_63) +w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Page + 1))) +w.Write(forum_64) +w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Page + 1))) +w.Write(forum_65) +} +w.Write(forum_66) +} +w.Write(forum_67) w.Write(footer_0) w.Write([]byte(common.BuildWidget("footer",tmpl_forum_vars.Header))) w.Write(footer_1) diff --git a/template_list.go b/template_list.go index 679ed331..138a12f4 100644 --- a/template_list.go +++ b/template_list.go @@ -1114,11 +1114,10 @@ var topics_62 = []byte(` var topics_63 = []byte(`
`) -var topics_64 = []byte(` +var topics_64 = []byte(`
-
`) +var topics_66 = []byte(`" />`) var topics_67 = []byte(`
`) @@ -1139,41 +1138,38 @@ var topics_74 = []byte(` var forum_0 = []byte(``) -var forum_3 = []byte(``) -var forum_8 = []byte(` +var forum_5 = []byte(`">>
`) +var forum_6 = []byte(`
+var forum_7 = []byte(` has_opt`) +var forum_8 = []byte(`">

`) -var forum_11 = []byte(`

+var forum_9 = []byte(`
`) -var forum_12 = []byte(` +var forum_10 = []byte(`
+var forum_11 = []byte(`">
`) -var forum_14 = []byte(` +var forum_12 = []byte(`
`) -var forum_15 = []byte(`
`) -var forum_16 = []byte(` +var forum_13 = []byte(`
`) +var forum_14 = []byte(`
`) -var forum_17 = []byte(` +var forum_15 = []byte(`
`) -var forum_18 = []byte(` +var forum_16 = []byte(`
@@ -1190,13 +1186,13 @@ var forum_18 = []byte(`
`) -var forum_19 = []byte(` +var forum_17 = []byte(` `) -var forum_24 = []byte(` +var forum_22 = []byte(`
`) -var forum_25 = []byte(`
+var forum_23 = []byte(`
+var forum_25 = []byte(`topic_sticky`) +var forum_26 = []byte(`topic_closed`) +var forum_27 = []byte(`"> `)
-var forum_32 = []byte(`'s Avatar +var forum_28 = []byte(`">`)
+var forum_30 = []byte(`'s Avatar `) -var forum_35 = []byte(` +var forum_32 = []byte(`" itemprop="itemListElement">`) +var forum_33 = []byte(`
`) -var forum_37 = []byte(` +var forum_34 = []byte(`">`) +var forum_35 = []byte(` `) -var forum_38 = []byte(` | 🔒︎`) -var forum_39 = []byte(` | 📍︎`) -var forum_40 = []byte(` +var forum_36 = []byte(` | 🔒︎`) +var forum_37 = []byte(` | 📍︎`) +var forum_38 = []byte(`
`) -var forum_41 = []byte(`
+var forum_39 = []byte(`

`) -var forum_42 = []byte(` +var forum_40 = []byte(`
+var forum_41 = []byte(`topic_sticky`) +var forum_42 = []byte(`topic_closed`) +var forum_43 = []byte(`"> `)
-var forum_48 = []byte(`'s Avatar +var forum_44 = []byte(`">`)
+var forum_46 = []byte(`'s Avatar `) -var forum_51 = []byte(`
+var forum_48 = []byte(`" class="lastName" style="font-size: 14px;">`) +var forum_49 = []byte(`
`) -var forum_52 = []byte(` +var forum_50 = []byte(`
`) -var forum_53 = []byte(`
There aren't any topics in this forum yet.`) -var forum_54 = []byte(` Start one?`) -var forum_56 = []byte(`
`) -var forum_57 = []byte(` +var forum_51 = []byte(`
There aren't any topics in this forum yet.`) +var forum_52 = []byte(` Start one?`) +var forum_54 = []byte(`
`) +var forum_55 = []byte(`
+ +`) +var forum_56 = []byte(` +
+ `) +var forum_57 = []byte(` + `) +var forum_60 = []byte(` + + `) +var forum_63 = []byte(` + + `) +var forum_66 = []byte(` +
+`) +var forum_67 = []byte(` + `) var guilds_guild_list_0 = []byte(` diff --git a/templates/forum.html b/templates/forum.html index b6779dd2..d506ca9c 100644 --- a/templates/forum.html +++ b/templates/forum.html @@ -1,8 +1,7 @@ {{template "header.html" . }} {{if gt .Page 1}}{{end}} -{{if ne .LastPage .Page}} -{{end}} +{{if ne .LastPage .Page}}{{end}}
@@ -101,5 +100,19 @@
{{else}}
There aren't any topics in this forum yet.{{if .CurrentUser.Perms.CreateTopic}} Start one?{{end}}
{{end}}
+ +{{if gt .LastPage 1}} +
+ {{if gt .Page 1}}
+ {{end}} + {{range .PageList}} +
{{.}}
+ {{end}} + {{if ne .LastPage .Page}} + +
{{end}} +
+{{end}} + {{template "footer.html" . }} diff --git a/templates/topics.html b/templates/topics.html index b4caca93..7ed93f4d 100644 --- a/templates/topics.html +++ b/templates/topics.html @@ -128,9 +128,8 @@ {{if gt .LastPage 1}}
- {{if gt .Page 1}} - -
{{end}} + {{if gt .Page 1}}
+ {{end}} {{range .PageList}}
{{.}}
{{end}} diff --git a/themes/shadow/public/main.css b/themes/shadow/public/main.css index 97c1bdf1..3e2a4b8c 100644 --- a/themes/shadow/public/main.css +++ b/themes/shadow/public/main.css @@ -484,7 +484,7 @@ textarea.large { padding: 5px; width: calc(100% - 16px); } -.formitem select { +select { background-color: var(--input-background-color); border: 1px solid var(--input-border-color); color: var(--input-text-color); @@ -613,6 +613,13 @@ input[type=checkbox]:checked + label.poll_option_label .sel { margin-left: 3px; background: var(--bright-input-border-color); } +.pollinput { + display: flex; + margin-bottom: 8px; +} +.quick_create_form .pollinputlabel { + display: none; +} /*#poll_option_text_0 { color: hsl(359,98%,43%); diff --git a/themes/shadow/public/panel.css b/themes/shadow/public/panel.css index 8514333f..36eec88c 100644 --- a/themes/shadow/public/panel.css +++ b/themes/shadow/public/panel.css @@ -16,6 +16,11 @@ margin-left: 2px; } +.timeRangeSelector { + padding: 2px; + margin-top: -3px; + margin-bottom: -3px; +} .panel_floater { margin-left: auto; } diff --git a/themes/tempra-conflux/public/main.css b/themes/tempra-conflux/public/main.css index 14798d26..270b5542 100644 --- a/themes/tempra-conflux/public/main.css +++ b/themes/tempra-conflux/public/main.css @@ -488,24 +488,24 @@ li a { .little_row_avatar { display: none; } -.topic_create_form .topic_button_row .formitem { +.quick_create_form .quick_button_row .formitem { display: flex; } -.topic_create_form .formbutton:first-child { +.quick_create_form .formbutton:first-child { margin-left: 0px; margin-right: 5px; } -.topic_create_form .formbutton:not(:first-child) { +.quick_create_form .formbutton:not(:first-child) { margin-left: 0px; margin-right: 5px; } -.topic_create_form .formbutton:last-child { +.quick_create_form .formbutton:last-child { margin-left: auto; } -.topic_create_form .upload_file_dock { +.quick_create_form .upload_file_dock { display: flex; } -.topic_create_form .uploadItem { +.quick_create_form .uploadItem { display: inline-block; margin-left: 8px; margin-right: 8px; @@ -765,6 +765,51 @@ button.username { border-bottom: 1.5px inset var(--main-border-color); } +/* TODO: Show the avatar next to the reply form */ +.topic_reply_container .userinfo { + display: none; +} + +input[type=checkbox] { + display: none; +} +input[type=checkbox] + label { + display: inline-block; + width: 12px; + height: 12px; + margin-bottom: -2px; + border: 1px solid hsl(0, 0%, 80%); + background-color: white; +} +input[type=checkbox]:checked + label .sel { + display: inline-block; + width: 5px; + height: 5px; + background-color: white; +} +input[type=checkbox] + label.poll_option_label { + width: 18px; + height: 18px; + margin-right: 2px; + background-color: white; + border: 1px solid hsl(0, 0%, 70%); + color: #505050; +} +input[type=checkbox]:checked + label.poll_option_label .sel { + display: inline-block; + width: 10px; + height: 10px; + margin-left: 3px; + background: hsl(0,0%,70%); +} +.poll_option { + margin-bottom: 1px; +} + +.quick_create_form .pollinputlabel { + display: none; +} + .simple { background-color: white; } @@ -945,7 +990,7 @@ button.username { .pageset { display: flex; margin-bottom: 10px; - margin-top: -5px; + margin-top: 8px; } .pageitem { border: 1px solid var(--main-border-color); @@ -958,6 +1003,9 @@ button.username { color: black; text-decoration: none; } +.colstack_right .pageset { + margin-top: -5px; +} /* Firefox specific CSS */ @supports (-moz-appearance: none) { diff --git a/themes/tempra-simple/public/main.css b/themes/tempra-simple/public/main.css index dab6a2d5..7212cd41 100644 --- a/themes/tempra-simple/public/main.css +++ b/themes/tempra-simple/public/main.css @@ -372,8 +372,18 @@ li a { border: none; } +input, select { + padding: 3px; +} /* Mostly for textareas */ -.formitem:only-child { width: 100%; } +.formitem:only-child { + width: 100%; +} +.formitem:only-child select { + padding: 1px; + margin-top: -1px; + margin-bottom: -1px; +} .formitem textarea { width: 100%; height: 100px; @@ -383,9 +393,6 @@ li a { margin: 0 auto; float: none; } -.formitem:not(:only-child) input, .formitem:not(:only-child) select { - padding: 3px; -} .formitem:not(:only-child).formlabel { padding-top: 15px; padding-bottom: 12px; @@ -417,6 +424,9 @@ li a { /* Topics */ +.topic_list { + border-bottom: none; +} .topic_list .topic_row { display: grid; grid-template-columns: calc(100% - 204px) 204px; @@ -737,6 +747,10 @@ input[type=checkbox]:checked + label.poll_option_label .sel { margin-left: auto; } +.quick_create_form .pollinputlabel { + display: none; +} + .alert { display: block; padding: 5px; @@ -869,7 +883,7 @@ input[type=checkbox]:checked + label.poll_option_label .sel { .pageset { display: flex; margin-bottom: 10px; - margin-top: -5px; + margin-top: 8px; } .pageitem { background-color: white; @@ -882,5 +896,8 @@ input[type=checkbox]:checked + label.poll_option_label .sel { color: black; text-decoration: none; } +.colstack_right .pageset { + margin-top: -5px; +} {{template "media.partial.css" }} diff --git a/themes/tempra-simple/public/panel.css b/themes/tempra-simple/public/panel.css index 21a3845d..dbb91e40 100644 --- a/themes/tempra-simple/public/panel.css +++ b/themes/tempra-simple/public/panel.css @@ -1,4 +1,6 @@ -/* Control Panel */ +.submenu a { + margin-left: 8px; +} .edit_button:before { content: "Edit"; @@ -125,4 +127,14 @@ content: " || "; padding-left: 2px; padding-right: 2px; +} +.ct_chart { + padding-left: 10px; + padding-top: 14px; + padding-bottom: 4px; + padding-right: 10px; + margin-bottom: 12px; + + background-color: white; + border: 1px solid hsl(0,0%,85%); } \ No newline at end of file