be47066770
Added better pagination to /forum/{id} Fixed the ordering of the elements in the pagesets. Added hooks to the task system. Reduced the amount of boilerplate for RunVhook and RunVhookNoReturn. Added RunVhookNeedHook to allow plugin developers to add custom content types to the report system. Added BulkGetMap to the DefaultPollStore. Renamed the MultiServer config setting to ServerCount and made it an integer. Don't cache topic lists for empty groups. Don't load replies for topics without any. Tempra Simple: Added chart and submenu CSS. Improved the input and select CSS. Fixed the pageset CSS. Fixed the poll input CSS. Tempra Conflux: Fixed the quick reply button CSS. Hid the avatar in the quick reply area. Added the poll CSS. Fixed the pageset CSS. Shadow: Fixed the time range selector CSS. Fixed the poll input CSS.
330 lines
10 KiB
Go
330 lines
10 KiB
Go
package common
|
|
|
|
import (
|
|
"strconv"
|
|
"sync"
|
|
|
|
"../query_gen/lib"
|
|
)
|
|
|
|
var TopicList TopicListInt
|
|
|
|
type TopicListHolder struct {
|
|
List []*TopicsRow
|
|
ForumList []Forum
|
|
PageList []int
|
|
Page int
|
|
LastPage int
|
|
}
|
|
|
|
type TopicListInt interface {
|
|
GetListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, pageList []int, outPage int, lastPage int, err error)
|
|
GetList(page int) (topicList []*TopicsRow, forumList []Forum, pageList []int, outPage int, lastPage int, err error)
|
|
}
|
|
|
|
type DefaultTopicList struct {
|
|
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
|
|
}
|
|
|
|
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()
|
|
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, pageList []int, page int, lastPage int) {
|
|
if gid%2 == 0 {
|
|
evenLists[gid] = &TopicListHolder{topicList, forumList, pageList, page, lastPage}
|
|
} else {
|
|
oddLists[gid] = &TopicListHolder{topicList, forumList, pageList, page, lastPage}
|
|
}
|
|
}
|
|
|
|
guestGroup, err := Groups.Get(GuestUser.Group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
topicList, forumList, pageList, page, lastPage, err := tList.getListByGroup(guestGroup, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addList(guestGroup.ID, topicList, forumList, pageList, page, lastPage)
|
|
|
|
for _, gid := range tList.groupList {
|
|
group, err := Groups.Get(gid) // TODO: Bulk load the groups?
|
|
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
|
|
}
|
|
addList(group.ID, topicList, forumList, pageList, page, lastPage)
|
|
}
|
|
|
|
tList.oddLock.Lock()
|
|
tList.oddGroups = oddLists
|
|
tList.oddLock.Unlock()
|
|
|
|
tList.evenLock.Lock()
|
|
tList.evenGroups = evenLists
|
|
tList.evenLock.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (tList *DefaultTopicList) GetListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, pageList []int, outPage int, lastPage int, err error) {
|
|
// TODO: Cache the first three pages not just the first along with all the topics on this beaten track
|
|
if page == 1 {
|
|
var holder *TopicListHolder
|
|
var ok bool
|
|
if group.ID%2 == 0 {
|
|
tList.evenLock.RLock()
|
|
holder, ok = tList.evenGroups[group.ID]
|
|
tList.evenLock.RUnlock()
|
|
} else {
|
|
tList.oddLock.RLock()
|
|
holder, ok = tList.oddGroups[group.ID]
|
|
tList.oddLock.RUnlock()
|
|
}
|
|
if ok {
|
|
return holder.List, holder.ForumList, holder.PageList, holder.Page, holder.LastPage, nil
|
|
}
|
|
}
|
|
|
|
return tList.getListByGroup(group, page)
|
|
}
|
|
|
|
func (tList *DefaultTopicList) getListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, pageList []int, outPage int, lastPage int, err error) {
|
|
// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
|
|
canSee := group.CanSee
|
|
|
|
// We need a list of the visible forums for Quick Topic
|
|
// ? - Would it be useful, if we could post in social groups from /topics/?
|
|
for _, fid := range canSee {
|
|
forum := Forums.DirtyGet(fid)
|
|
if forum.Name != "" && forum.Active && (forum.ParentType == "" || forum.ParentType == "forum") {
|
|
fcopy := forum.Copy()
|
|
// TODO: Add a hook here for plugin_guilds
|
|
forumList = append(forumList, fcopy)
|
|
}
|
|
}
|
|
|
|
// ? - Should we be showing plugin_guilds posts on /topics/?
|
|
argList, qlist := ForumListToArgQ(forumList)
|
|
if qlist == "" {
|
|
// We don't want to kill the page, so pass an empty slice and nil error
|
|
return topicList, forumList, pageList, 1, 1, nil
|
|
}
|
|
|
|
topicList, pageList, page, lastPage, err = tList.getList(page, argList, qlist)
|
|
return topicList, forumList, pageList, page, lastPage, err
|
|
}
|
|
|
|
// TODO: Reduce the number of returns
|
|
func (tList *DefaultTopicList) GetList(page int) (topicList []*TopicsRow, forumList []Forum, pageList []int, outPage int, lastPage int, 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, err := Forums.GetAllVisibleIDs()
|
|
if err != nil {
|
|
return nil, nil, nil, 1, 1, err
|
|
}
|
|
|
|
// 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 {
|
|
forum := Forums.DirtyGet(fid)
|
|
if forum.Name != "" && forum.Active && (forum.ParentType == "" || forum.ParentType == "forum") {
|
|
fcopy := forum.Copy()
|
|
// TODO: Add a hook here for plugin_guilds
|
|
forumList = append(forumList, fcopy)
|
|
}
|
|
}
|
|
|
|
// ? - Should we be showing plugin_guilds posts on /topics/?
|
|
argList, qlist := ForumListToArgQ(forumList)
|
|
if qlist == "" {
|
|
// If the super admin can't see anything, then things have gone terribly wrong
|
|
return topicList, forumList, pageList, 1, 1, err
|
|
}
|
|
|
|
topicList, pageList, outPage, lastPage, err = tList.getList(page, argList, qlist)
|
|
return topicList, forumList, pageList, outPage, lastPage, err
|
|
}
|
|
|
|
// TODO: Rename this to TopicListStore and pass back a TopicList instance holding the pagination data and topic list rather than passing them back one argument at a time
|
|
func (tList *DefaultTopicList) getList(page int, argList []interface{}, qlist string) (topicList []*TopicsRow, pageList []int, outPage int, lastPage int, err error) {
|
|
topicCount, err := ArgQToTopicCount(argList, qlist)
|
|
if err != nil {
|
|
return nil, nil, 1, 1, err
|
|
}
|
|
offset, page, lastPage := PageOffset(topicCount, page, Config.ItemsPerPage)
|
|
|
|
stmt, err := qgen.Builder.SimpleSelect("topics", "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, postCount, likeCount", "parentID IN("+qlist+")", "sticky DESC, lastReplyAt DESC, createdBy DESC", "?,?")
|
|
if err != nil {
|
|
return nil, nil, 1, 1, err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
argList = append(argList, offset)
|
|
argList = append(argList, Config.ItemsPerPage)
|
|
|
|
rows, err := stmt.Query(argList...)
|
|
if err != nil {
|
|
return nil, nil, 1, 1, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var reqUserList = make(map[int]bool)
|
|
for rows.Next() {
|
|
topicItem := TopicsRow{ID: 0}
|
|
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.PostCount, &topicItem.LikeCount)
|
|
if err != nil {
|
|
return nil, nil, 1, 1, err
|
|
}
|
|
|
|
topicItem.Link = BuildTopicURL(NameToSlug(topicItem.Title), topicItem.ID)
|
|
forum := Forums.DirtyGet(topicItem.ParentID)
|
|
topicItem.ForumName = forum.Name
|
|
topicItem.ForumLink = forum.Link
|
|
|
|
//topicItem.CreatedAt = RelativeTime(topicItem.CreatedAt)
|
|
topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt)
|
|
|
|
// TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/
|
|
RunVhook("topics_topic_row_assign", &topicItem, &forum)
|
|
topicList = append(topicList, &topicItem)
|
|
reqUserList[topicItem.CreatedBy] = true
|
|
reqUserList[topicItem.LastReplyBy] = true
|
|
}
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return nil, nil, 1, 1, err
|
|
}
|
|
|
|
// Convert the user ID map to a slice, then bulk load the users
|
|
var idSlice = make([]int, len(reqUserList))
|
|
var i int
|
|
for userID := range reqUserList {
|
|
idSlice[i] = userID
|
|
i++
|
|
}
|
|
|
|
// TODO: What if a user is deleted via the Control Panel?
|
|
userList, err := Users.BulkGetMap(idSlice)
|
|
if err != nil {
|
|
return nil, nil, 1, 1, err
|
|
}
|
|
|
|
// Second pass to the add the user data
|
|
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
|
|
for _, topicItem := range topicList {
|
|
topicItem.Creator = userList[topicItem.CreatedBy]
|
|
topicItem.LastUser = userList[topicItem.LastReplyBy]
|
|
}
|
|
|
|
pageList = Paginate(topicCount, Config.ItemsPerPage, 5)
|
|
return topicList, pageList, page, lastPage, nil
|
|
}
|
|
|
|
// Internal. Don't rely on it.
|
|
func ForumListToArgQ(forums []Forum) (argList []interface{}, qlist string) {
|
|
for _, forum := range forums {
|
|
argList = append(argList, strconv.Itoa(forum.ID))
|
|
qlist += "?,"
|
|
}
|
|
if qlist != "" {
|
|
qlist = qlist[0 : len(qlist)-1]
|
|
}
|
|
return argList, qlist
|
|
}
|
|
|
|
// Internal. Don't rely on it.
|
|
func ArgQToTopicCount(argList []interface{}, qlist string) (topicCount int, err error) {
|
|
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "parentID IN("+qlist+")", "")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer topicCountStmt.Close()
|
|
|
|
err = topicCountStmt.QueryRow(argList...).Scan(&topicCount)
|
|
if err != nil && err != ErrNoRows {
|
|
return 0, err
|
|
}
|
|
return topicCount, err
|
|
}
|
|
|
|
func TopicCountInForums(forums []Forum) (topicCount int, err error) {
|
|
return ArgQToTopicCount(ForumListToArgQ(forums))
|
|
}
|