├── .npmignore ├── public ├── templates │ ├── admin │ │ └── partials │ │ │ └── widgets │ │ │ ├── categories.tpl │ │ │ ├── defaultwidget.tpl │ │ │ ├── mygroups.tpl │ │ │ ├── latestusers.tpl │ │ │ ├── onlineusers.tpl │ │ │ ├── topposters.tpl │ │ │ ├── forumstats.tpl │ │ │ ├── chat.tpl │ │ │ ├── moderators.tpl │ │ │ ├── suggestedtopics.tpl │ │ │ ├── populartags.tpl │ │ │ ├── activeusers.tpl │ │ │ ├── groupposts.tpl │ │ │ ├── recentposts.tpl │ │ │ ├── populartopics.tpl │ │ │ ├── toptopics.tpl │ │ │ ├── recenttopics.tpl │ │ │ ├── text.tpl │ │ │ ├── html.tpl │ │ │ ├── userpost.tpl │ │ │ └── search.tpl │ └── widgets │ │ ├── toptopics.tpl │ │ ├── populartopics.tpl │ │ ├── userpost.tpl │ │ ├── groupposts.tpl │ │ ├── suggestedtopics.tpl │ │ ├── moderators.tpl │ │ ├── topposters.tpl │ │ ├── latestusers.tpl │ │ ├── onlineusers.tpl │ │ ├── activeusers.tpl │ │ ├── groups.tpl │ │ ├── partials │ │ ├── posts.tpl │ │ └── topics.tpl │ │ ├── forumstats.tpl │ │ ├── categories.tpl │ │ ├── recenttopics.tpl │ │ ├── recentposts.tpl │ │ ├── search.tpl │ │ ├── populartags.tpl │ │ └── chat.tpl └── css │ └── widget.css ├── eslint.config.mjs ├── README.md ├── .gitattributes ├── package.json ├── LICENSE ├── plugin.json ├── .gitignore └── library.js /.npmignore: -------------------------------------------------------------------------------- 1 | sftp-config.json 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/categories.tpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/defaultwidget.tpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/templates/widgets/toptopics.tpl: -------------------------------------------------------------------------------- 1 |
2 | 5 |
-------------------------------------------------------------------------------- /public/templates/widgets/populartopics.tpl: -------------------------------------------------------------------------------- 1 |
2 | 5 |
-------------------------------------------------------------------------------- /public/templates/widgets/userpost.tpl: -------------------------------------------------------------------------------- 1 |
2 | 5 |
-------------------------------------------------------------------------------- /public/templates/widgets/groupposts.tpl: -------------------------------------------------------------------------------- 1 |
2 | 5 |
-------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/mygroups.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/latestusers.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/onlineusers.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/topposters.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/forumstats.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import serverConfig from 'eslint-config-nodebb'; 4 | import publicConfig from 'eslint-config-nodebb/public'; 5 | 6 | export default [ 7 | ...publicConfig, 8 | ...serverConfig, 9 | ]; 10 | 11 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/chat.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Enter the room id to use for this widget

5 |
-------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/moderators.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Leave blank to dynamically pull from current category

5 |
-------------------------------------------------------------------------------- /public/templates/widgets/suggestedtopics.tpl: -------------------------------------------------------------------------------- 1 |
2 | {{{ if sidebar }}} 3 | 6 | {{{ else }}} 7 | 8 | {{{ end }}} 9 |
10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeBB Essential Widgets 2 | 3 | Several basic widgets bundled together in one package including 'HTML', 'Markdown', 'Active Users', 'Moderators', 'Forum Stats', 'Recent Topics', 'Categories', 'Popular Tags' and 'Recent Posts' widgets. 4 | 5 | ## Installation 6 | 7 | npm install nodebb-widget-essentials 8 | -------------------------------------------------------------------------------- /public/css/widget.css: -------------------------------------------------------------------------------- 1 | 2 | /* below rules hide stats/teaser for suggested topics widget when it is placed in a sidebar */ 3 | [data-widget-area="sidebar"] .category .content { 4 | width: 100%!important; 5 | } 6 | 7 | [data-widget-area="sidebar"] .category .stats { 8 | display: none!important; 9 | } 10 | 11 | [data-widget-area="sidebar"] .category .teaser { 12 | display: none!important; 13 | } -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/suggestedtopics.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |
7 | 8 | 9 |
-------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/populartags.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | 11 |
12 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/activeusers.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | 8 |

Leave blank to to dynamically pull from current category

9 |
10 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/groupposts.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | 12 |
13 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/recentposts.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | 8 |

Leave blank to dynamically pull from current category. If placed on a page other than a category will pull from all recent posts

9 |
-------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/populartopics.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | 13 |
-------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/toptopics.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |
7 | 8 | 14 |
-------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/recenttopics.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |
7 | 8 | 9 |

Leave blank to dynamically pull from current category. If placed on a page other than a category will pull from all recent posts

10 |
11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /public/templates/widgets/moderators.tpl: -------------------------------------------------------------------------------- 1 |
2 | {{{ each moderators }}} 3 | 4 | {buildAvatar(@value, "48px", true, "flex-shrink-0")} 5 |
6 |
{./displayname}
7 |
@{./username}
8 |
9 |
10 | {{{ end }}} 11 |
12 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/text.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 7 |

Set the category ID you want to display this widget on.

8 |
9 | 10 |
11 | 12 | 13 |
-------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/html.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 7 |

Set the category IDs you want to display this widget on (separated by commas)

8 |
9 |
10 | 11 | 12 |

Set the topic IDs you want to display this widget on (separated by commas)

13 |
14 | -------------------------------------------------------------------------------- /public/templates/widgets/topposters.tpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/userpost.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |
7 | 8 | 9 |
10 | 11 |
12 | 13 | 18 |
-------------------------------------------------------------------------------- /public/templates/widgets/latestusers.tpl: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /public/templates/widgets/onlineusers.tpl: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /public/templates/widgets/activeusers.tpl: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /public/templates/widgets/groups.tpl: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{{ each groups }}} 4 |
5 | 6 | 7 |
8 |
9 |
{./displayName}
10 |
{./memberCount}
11 |
12 |
{./description}
13 |
14 |
15 |
16 | {{{ end }}} 17 |
18 | -------------------------------------------------------------------------------- /public/templates/widgets/partials/posts.tpl: -------------------------------------------------------------------------------- 1 | {{{ each posts }}} 2 |
  • 3 |
    4 | 5 | {buildAvatar(./user, "24px", true)} 6 | 7 | 8 | 11 | 12 |
    13 |
    14 | {./content} 15 |
    16 | 17 |
    18 | [[global:read-more]] 19 |
    20 | {{{ if !@last}}} 21 |
    22 | {{{ end }}} 23 |
  • 24 | {{{ end }}} -------------------------------------------------------------------------------- /public/templates/admin/partials/widgets/search.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 | 6 |
    7 | 8 | 9 |
    10 | 11 |
    12 | 13 | 21 |
    22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodebb-widget-essentials", 3 | "version": "7.0.41", 4 | "description": "NodeBB Essential Widgets", 5 | "main": "library.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/psychobunny/nodebb-widget-essentials" 13 | }, 14 | "keywords": [ 15 | "nodebb", 16 | "widget", 17 | "essentials", 18 | "markdown", 19 | "text" 20 | ], 21 | "author": { 22 | "name": "psychobunny", 23 | "email": "psycho.bunny@hotmail.com" 24 | }, 25 | "license": "BSD-2-Clause", 26 | "bugs": { 27 | "url": "https://github.com/psychobunny/nodebb-widget-essentials/issues" 28 | }, 29 | "readme": "", 30 | "readmeFilename": "README.md", 31 | "_from": "nodebb-widget-essentials@~0.0.1", 32 | "nbbpm": { 33 | "compatibility": "^4.0.0" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^9.25.1", 37 | "eslint-config-nodebb": "^1.1.4", 38 | "eslint-plugin-import": "^2.31.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/templates/widgets/forumstats.tpl: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |

    {online}

    6 | [[global:online]] 7 |
    8 |
    9 |
    10 |
    11 |

    {users}

    12 | [[global:users]] 13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |

    {topics}

    20 | [[global:topics]] 21 |
    22 |
    23 |
    24 |
    25 |

    {posts}

    26 | [[global:posts]] 27 |
    28 |
    29 |
    30 |
    31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014, psychobunny 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /public/templates/widgets/categories.tpl: -------------------------------------------------------------------------------- 1 | {{{ each categories}}} 2 | 38 | {{{ end }}} 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/templates/widgets/recenttopics.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 5 |
    6 | 7 | 57 | -------------------------------------------------------------------------------- /public/templates/widgets/recentposts.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 5 |
    6 | 7 | 65 | -------------------------------------------------------------------------------- /public/templates/widgets/partials/topics.tpl: -------------------------------------------------------------------------------- 1 | {{{ each topics }}} 2 |
  • 3 |
    4 | {{{ if ./thumbs.length }}} 5 | 6 | 7 | 8 | {{{ end }}} 9 | 10 |
    11 | {./title} 12 | 13 | 23 | 24 |
    25 | 26 | 27 | 28 | 29 | 30 |
    31 |
    32 |
    33 | {{{ if !@last}}} 34 |
    35 | {{{ end }}} 36 |
  • 37 | {{{ end }}} 38 | -------------------------------------------------------------------------------- /public/templates/widgets/search.tpl: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 | 7 | 8 | {{{ if showInControl }}} 9 | 14 | {{{ end }}} 15 |
    16 |
    17 | 21 |
    22 |
    23 |
    24 |
    25 |
    26 | 58 | -------------------------------------------------------------------------------- /public/templates/widgets/populartags.tpl: -------------------------------------------------------------------------------- 1 | 30 | 50 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "nodebb-widget-essentials", 3 | "name": "NodeBB Essential Widgets", 4 | "description": "Several basic widgets bundled together in one package including 'HTML', 'Markdown'", 5 | "url": "https://github.com/psychobunny/nodebb-widget-essentials", 6 | "library": "./library.js", 7 | "hooks": [ 8 | { "hook": "static:app.load", "method": "init" }, 9 | { "hook": "filter:widgets.getWidgets", "method": "defineWidgets" }, 10 | { "hook": "filter:widget.render:text", "method": "renderTextWidget" }, 11 | { "hook": "filter:widget.render:html", "method": "renderHTMLWidget" }, 12 | { "hook": "filter:widget.render:search", "method": "renderSearchWidget"}, 13 | { "hook": "filter:widget.render:onlineusers", "method": "renderOnlineUsersWidget" }, 14 | { "hook": "filter:widget.render:activeusers", "method": "renderActiveUsersWidget" }, 15 | { "hook": "filter:widget.render:latestusers", "method": "renderLatestUsersWidget" }, 16 | { "hook": "filter:widget.render:topposters", "method": "renderTopPostersWidget" }, 17 | { "hook": "filter:widget.render:moderators", "method": "renderModeratorsWidget" }, 18 | { "hook": "filter:widget.render:forumstats", "method": "renderForumStatsWidget" }, 19 | { "hook": "filter:widget.render:recentposts", "method": "renderRecentPostsWidget" }, 20 | { "hook": "filter:widget.render:recenttopics", "method": "renderRecentTopicsWidget" }, 21 | { "hook": "filter:widget.render:recentview", "method": "renderRecentViewWidget" }, 22 | { "hook": "filter:widget.render:categories", "method": "renderCategories" }, 23 | { "hook": "filter:widget.render:populartags", "method": "renderPopularTags" }, 24 | { "hook": "filter:widget.render:populartopics", "method": "renderPopularTopics" }, 25 | { "hook": "filter:widget.render:toptopics", "method": "renderTopTopics" }, 26 | { "hook": "filter:widget.render:newgroups", "method": "renderNewGroups" }, 27 | { "hook": "filter:widget.render:mygroups", "method": "renderMyGroups" }, 28 | { "hook": "filter:widget.render:groupposts", "method": "renderGroupPosts" }, 29 | { "hook": "filter:widget.render:suggestedtopics", "method": "renderSuggestedTopics" }, 30 | { "hook": "filter:widget.render:userpost", "method": "renderUserPost" }, 31 | { "hook": "filter:widget.render:chat", "method": "renderChatRoom" } 32 | ], 33 | "templates": "./public/templates", 34 | "css": [ 35 | "public/css/widget.css" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | 217 | 218 | sftp-config.json 219 | /node_modules 220 | 221 | 222 | *.sublime-project 223 | *.sublime-workspace -------------------------------------------------------------------------------- /public/templates/widgets/chat.tpl: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 19 | 20 |
    21 | 22 | 23 | 24 |
    25 |
    26 | 27 |
    28 |
    29 |
    30 | 31 |
      32 | 33 |
    34 | 40 | 41 |
    42 |
    43 |
    44 |
    [[topic:composer.drag-and-drop-images]]
    45 |
    46 |
    47 | -------------------------------------------------------------------------------- /library.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nconf = require.main.require('nconf'); 4 | const validator = require.main.require('validator'); 5 | const benchpressjs = require.main.require('benchpressjs'); 6 | const _ = require.main.require('lodash'); 7 | 8 | const db = require.main.require('./src/database'); 9 | const categories = require.main.require('./src/categories'); 10 | const user = require.main.require('./src/user'); 11 | const plugins = require.main.require('./src/plugins'); 12 | const topics = require.main.require('./src/topics'); 13 | const posts = require.main.require('./src/posts'); 14 | const groups = require.main.require('./src/groups'); 15 | const utils = require.main.require('./src/utils'); 16 | const meta = require.main.require('./src/meta'); 17 | const privileges = require.main.require('./src/privileges'); 18 | 19 | let app; 20 | 21 | const Widget = module.exports; 22 | 23 | const sidebarLocations = ['left', 'right', 'sidebar']; 24 | 25 | Widget.init = async function (params) { 26 | app = params.app; 27 | }; 28 | 29 | Widget.renderHTMLWidget = async function (widget) { 30 | if (!isVisibleInCategory(widget) || !isVisibleInTopic(widget)) { 31 | return null; 32 | } 33 | const tpl = widget.data ? widget.data.html : ''; 34 | widget.html = await benchpressjs.compileRender(String(tpl), widget.templateData); 35 | return widget; 36 | }; 37 | 38 | Widget.renderTextWidget = async function (widget) { 39 | if (!isVisibleInCategory(widget)) { 40 | return null; 41 | } 42 | const parseAsPost = !!widget.data.parseAsPost; 43 | const text = String(widget.data.text); 44 | 45 | if (parseAsPost) { 46 | widget.html = await plugins.hooks.fire('filter:parse.raw', text); 47 | } else { 48 | widget.html = text.replace(/\r\n/g, '
    '); 49 | } 50 | return widget; 51 | }; 52 | 53 | Widget.renderSearchWidget = async function (widget) { 54 | if (widget.templateData.template.search && widget.location !== 'brand-header') { 55 | return null; 56 | } 57 | const userPrivileges = await privileges.global.get(widget.uid); 58 | 59 | const inOptions = [ 60 | { value: 'titles', label: '[[search:in-titles]]' }, 61 | { value: 'titlesposts', label: '[[search:in-titles-posts]]' }, 62 | { value: 'posts', label: '[[global:posts]]' }, 63 | { value: 'categories', label: '[[global:header.categories]]' }, 64 | ]; 65 | if (userPrivileges['search:users']) { 66 | inOptions.push({ value: 'users', label: '[[global:users]]' }); 67 | } 68 | if (userPrivileges['search:tags']) { 69 | inOptions.push({ value: 'tags', label: '[[tags:tags]]' }); 70 | } 71 | inOptions.forEach((option) => { 72 | option.selected = option.value === widget.data.defaultIn; 73 | }); 74 | 75 | widget.html = await app.renderAsync('widgets/search', { 76 | inOptions: inOptions, 77 | showInControl: widget.data.showInControl === 'on', 78 | enableQuickSearch: widget.data.enableQuickSearch === 'on', 79 | relative_path: nconf.get('relative_path'), 80 | }); 81 | return widget; 82 | }; 83 | 84 | function getValuesArray(widget, field) { 85 | const values = widget.data[field] || ''; 86 | return values.split(',').map(c => parseInt(c, 10)).filter(Boolean); 87 | } 88 | 89 | function isVisibleInCategory(widget) { 90 | const cids = getValuesArray(widget, 'cid'); 91 | return !( 92 | cids.length && 93 | (widget.templateData.template.category || widget.templateData.template.topic) && 94 | !cids.includes(parseInt(widget.templateData.cid, 10)) 95 | ); 96 | } 97 | 98 | function isVisibleInTopic(widget) { 99 | const tids = getValuesArray(widget, 'tid'); 100 | return !( 101 | tids.length && 102 | widget.templateData.template.topic && 103 | !tids.includes(parseInt(widget.templateData.tid, 10)) 104 | ); 105 | } 106 | 107 | Widget.renderRecentViewWidget = async function (widget) { 108 | const [data, allowedCids] = await Promise.all([ 109 | topics.getLatestTopics({ 110 | uid: 111 | widget.uid, 112 | start: 0, 113 | stop: 19, 114 | term: 'month', 115 | }), 116 | categories.getCidsByPrivilege('categories:cid', widget.uid, 'topics:create'), 117 | ]); 118 | 119 | data.relative_path = nconf.get('relative_path'); 120 | data.loggedIn = !!widget.req.uid; 121 | data.config = data.config || {}; 122 | data.config.relative_path = nconf.get('relative_path'); 123 | data.canPost = allowedCids.length > 0; 124 | widget.html = await app.renderAsync('recent', data); 125 | widget.html = widget.html.replace(//, '').replace('
    ', ''); 126 | return widget; 127 | }; 128 | 129 | Widget.renderOnlineUsersWidget = async function (widget) { 130 | const count = Math.max(1, widget.data.numUsers || 24); 131 | const uids = await user.getUidsFromSet('users:online', 0, count - 1); 132 | let userData = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline']); 133 | userData = userData.filter(user => user.status !== 'offline'); 134 | userData.sort((a, b) => b.lastonline - a.lastonline); 135 | widget.html = await app.renderAsync('widgets/onlineusers', { 136 | online_users: userData, 137 | sidebar: sidebarLocations.includes(widget.location), 138 | relative_path: nconf.get('relative_path'), 139 | }); 140 | return widget; 141 | }; 142 | 143 | Widget.renderActiveUsersWidget = async function (widget) { 144 | const count = Math.max(1, widget.data.numUsers || 24); 145 | const cids = getValuesArray(widget, 'cid'); 146 | let uids; 147 | if (cids.length) { 148 | uids = await categories.getActiveUsers(cids); 149 | } else if (widget.templateData.template.topic) { 150 | uids = await topics.getUids(widget.templateData.tid); 151 | } else { 152 | uids = await posts.getRecentPosterUids(0, count - 1); 153 | } 154 | uids = uids.slice(0, count); 155 | 156 | const userData = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'lastposttime']); 157 | userData.sort((a, b) => b.lastposttime - a.lastposttime); 158 | userData.forEach((u) => { 159 | if (u) { 160 | u.lastposttimeISO = utils.toISOString(u.lastposttime); 161 | } 162 | }); 163 | 164 | widget.html = await app.renderAsync('widgets/activeusers', { 165 | active_users: userData, 166 | sidebar: sidebarLocations.includes(widget.location), 167 | relative_path: nconf.get('relative_path'), 168 | }); 169 | return widget; 170 | }; 171 | 172 | Widget.renderLatestUsersWidget = async function (widget) { 173 | const count = Math.max(1, widget.data.numUsers || 24); 174 | const users = await user.getUsersFromSet('users:joindate', widget.uid, 0, count - 1); 175 | widget.html = await app.renderAsync('widgets/latestusers', { 176 | users: users, 177 | sidebar: sidebarLocations.includes(widget.location), 178 | relative_path: nconf.get('relative_path'), 179 | }); 180 | return widget; 181 | }; 182 | 183 | Widget.renderTopPostersWidget = async function (widget) { 184 | const count = Math.max(1, widget.data.numUsers || 24); 185 | const users = await user.getUsersFromSet('users:postcount', widget.uid, 0, count - 1); 186 | 187 | widget.html = await app.renderAsync('widgets/topposters', { 188 | users: users, 189 | sidebar: sidebarLocations.includes(widget.location), 190 | relative_path: nconf.get('relative_path'), 191 | }); 192 | return widget; 193 | }; 194 | 195 | Widget.renderModeratorsWidget = async function (widget) { 196 | let cid; 197 | 198 | if (widget.data.cid) { 199 | cid = widget.data.cid; 200 | } else if (widget.templateData.template.category) { 201 | cid = widget.templateData.cid; 202 | } else if (widget.templateData.template.topic && widget.templateData.category) { 203 | cid = widget.templateData.category.cid; 204 | } 205 | 206 | const moderators = await categories.getModerators(cid); 207 | if (!moderators.length) { 208 | return null; 209 | } 210 | widget.html = await app.renderAsync('widgets/moderators', { 211 | moderators: moderators, 212 | relative_path: nconf.get('relative_path'), 213 | }); 214 | return widget; 215 | }; 216 | 217 | Widget.renderForumStatsWidget = async function (widget) { 218 | const socketRooms = require.main.require('./src/socket.io/admin/rooms'); 219 | const [global, onlineCount, guestCount] = await Promise.all([ 220 | db.getObjectFields('global', ['topicCount', 'postCount', 'userCount']), 221 | db.sortedSetCount('users:online', Date.now() - (meta.config.onlineCutoff * 60000), '+inf'), 222 | socketRooms.getTotalGuestCount(), 223 | ]); 224 | 225 | const stats = { 226 | topics: utils.makeNumberHumanReadable(global.topicCount ? global.topicCount : 0), 227 | posts: utils.makeNumberHumanReadable(global.postCount ? global.postCount : 0), 228 | users: utils.makeNumberHumanReadable(global.userCount ? global.userCount : 0), 229 | online: utils.makeNumberHumanReadable(onlineCount + guestCount), 230 | statsClass: widget.data.statsClass, 231 | }; 232 | widget.html = await app.renderAsync('widgets/forumstats', stats); 233 | return widget; 234 | }; 235 | 236 | Widget.renderRecentPostsWidget = async function (widget) { 237 | let cid; 238 | 239 | if (widget.data.cid) { 240 | cid = widget.data.cid; 241 | } else if (widget.templateData.template.category) { 242 | cid = widget.templateData.cid; 243 | } else if (widget.templateData.template.topic && widget.templateData.category) { 244 | cid = widget.templateData.category.cid; 245 | } 246 | const numPosts = widget.data.numPosts || 4; 247 | let postsData; 248 | if (cid) { 249 | postsData = await categories.getRecentReplies(cid, widget.uid, 0, Math.max(0, numPosts - 1)); 250 | } else { 251 | let cids = await categories.getCidsByPrivilege('categories:cid', widget.uid, 'topics:read'); 252 | cids = cids.filter(cid => cid !== -1); 253 | const pids = await db.getSortedSetRevRange( 254 | cids.map(cid => `cid:${cid}:pids`), 0, Math.max(0, numPosts - 1), 255 | ); 256 | postsData = await posts.getPostSummaryByPids(pids, widget.uid, { stripTags: true }); 257 | } 258 | const data = { 259 | posts: postsData, 260 | numPosts: numPosts, 261 | cid: cid, 262 | relative_path: nconf.get('relative_path'), 263 | }; 264 | widget.html = await app.renderAsync('widgets/recentposts', data); 265 | return widget; 266 | }; 267 | 268 | Widget.renderRecentTopicsWidget = async function (widget) { 269 | const numTopics = (widget.data.numTopics || 8) - 1; 270 | let cids = getValuesArray(widget, 'cid'); 271 | 272 | let key; 273 | if (!cids.length) { 274 | cids = await categories.getCidsByPrivilege('categories:cid', widget.uid, 'topics:read'); 275 | cids = cids.filter(cid => cid !== -1); 276 | } 277 | if (cids.length) { 278 | if (cids.length === 1) { 279 | key = `cid:${cids[0]}:tids:lastposttime`; 280 | } else { 281 | key = cids.map(cid => `cid:${cid}:tids:lastposttime`); 282 | } 283 | } 284 | const data = await topics.getTopicsFromSet(key, widget.uid, 0, Math.max(0, numTopics)); 285 | data.topics.forEach((topicData) => { 286 | if (topicData && !topicData.teaser) { 287 | topicData.teaser = { 288 | user: topicData.user, 289 | timestampISO: topicData.timestampISO, 290 | }; 291 | } 292 | }); 293 | widget.html = await app.renderAsync('widgets/recenttopics', { 294 | topics: data.topics, 295 | numTopics: numTopics, 296 | relative_path: nconf.get('relative_path'), 297 | sidebar: sidebarLocations.includes(widget.location), 298 | }); 299 | return widget; 300 | }; 301 | 302 | Widget.renderCategories = async function (widget) { 303 | let categoryData = await categories.getCategoriesByPrivilege('categories:cid', widget.uid, 'find'); 304 | categoryData = categoryData.filter(c => c && c.cid !== -1); 305 | const tree = categories.getTree(categoryData, 0); 306 | widget.html = await app.renderAsync('widgets/categories', { 307 | categories: tree, 308 | relative_path: nconf.get('relative_path'), 309 | }); 310 | return widget; 311 | }; 312 | 313 | Widget.renderPopularTags = async function (widget) { 314 | const numTags = widget.data.numTags || 8; 315 | const display = widget.data.display || 'buttons'; 316 | let tags = []; 317 | if (widget.templateData.template.category) { 318 | tags = await topics.getCategoryTagsData(widget.templateData.cid, 0, numTags - 1); 319 | } else { 320 | let cids = await categories.getCidsByPrivilege('categories:cid', widget.uid, 'topics:read'); 321 | cids = cids.filter(cid => cid !== -1); 322 | tags = await topics.getCategoryTagsData(cids, 0, numTags - 1); 323 | } 324 | let maxCount = 1; 325 | tags.forEach((t) => { 326 | if (t.score > maxCount) { 327 | maxCount = t.score; 328 | } 329 | }); 330 | tags.forEach((t) => { 331 | t.widthPercent = ((t.score / maxCount) * 100).toFixed(2); 332 | }); 333 | 334 | widget.html = await app.renderAsync('widgets/populartags', { 335 | tags: tags, 336 | display, 337 | template: widget.templateData.template, 338 | relative_path: nconf.get('relative_path'), 339 | }); 340 | return widget; 341 | }; 342 | 343 | Widget.renderPopularTopics = async function (widget) { 344 | const numTopics = widget.data.numTopics || 8; 345 | const data = await topics.getSortedTopics({ 346 | uid: widget.uid, 347 | start: 0, 348 | stop: numTopics - 1, 349 | term: widget.data.duration || 'alltime', 350 | sort: 'posts', 351 | }); 352 | widget.html = await app.renderAsync('widgets/populartopics', { 353 | topics: data.topics, 354 | numTopics: numTopics, 355 | relative_path: nconf.get('relative_path'), 356 | sidebar: sidebarLocations.includes(widget.location), 357 | }); 358 | return widget; 359 | }; 360 | 361 | Widget.renderTopTopics = async function (widget) { 362 | const numTopics = widget.data.numTopics || 8; 363 | const data = await topics.getSortedTopics({ 364 | uid: widget.uid, 365 | start: 0, 366 | stop: numTopics - 1, 367 | term: widget.data.duration || 'alltime', 368 | sort: 'votes', 369 | }); 370 | widget.html = await app.renderAsync('widgets/toptopics', { 371 | topics: data.topics, 372 | numTopics: numTopics, 373 | relative_path: nconf.get('relative_path'), 374 | sidebar: sidebarLocations.includes(widget.location), 375 | }); 376 | return widget; 377 | }; 378 | 379 | Widget.renderMyGroups = async function (widget) { 380 | const { uid } = widget; 381 | const numGroups = parseInt(widget.data.numGroups, 10) || 9; 382 | const groupsData = await groups.getUserGroups([uid]); 383 | let userGroupData = groupsData.length ? groupsData[0] : []; 384 | userGroupData = userGroupData.slice(0, numGroups); 385 | widget.html = await app.renderAsync('widgets/groups', { 386 | groups: userGroupData, 387 | relative_path: nconf.get('relative_path'), 388 | }); 389 | return widget; 390 | }; 391 | 392 | Widget.renderGroupPosts = async function (widget) { 393 | const numPosts = parseInt(widget.data.numPosts, 10) || 4; 394 | const postsData = await groups.getLatestMemberPosts(widget.data.groupName, numPosts, widget.uid); 395 | widget.html = await app.renderAsync('widgets/groupposts', { posts: postsData }); 396 | return widget; 397 | }; 398 | 399 | Widget.renderNewGroups = async function (widget) { 400 | const numGroups = parseInt(widget.data.numGroups, 10) || 8; 401 | const groupNames = await db.getSortedSetRevRange('groups:visible:createtime', 0, numGroups - 1); 402 | const groupsData = await groups.getGroupsData(groupNames); 403 | widget.html = await app.renderAsync('widgets/groups', { 404 | groups: groupsData.filter(Boolean), 405 | relative_path: nconf.get('relative_path'), 406 | }); 407 | return widget; 408 | }; 409 | 410 | Widget.renderSuggestedTopics = async function (widget) { 411 | const numTopics = Math.max(0, (widget.data.numTopics || 8) - 1); 412 | const cutoff = Math.max(0, parseInt(widget.data.cutoff, 10) || 0); 413 | async function getCategoryTopics(term, sort) { 414 | const data = await topics.getSortedTopics({ 415 | cids: widget.templateData.cid, 416 | uid: widget.uid, 417 | start: 0, 418 | stop: 2 * numTopics, 419 | term: term, 420 | sort: sort, 421 | }); 422 | return data.topics; 423 | } 424 | let topicData; 425 | if (widget.templateData.template.topic) { 426 | topicData = await topics.getSuggestedTopics(widget.templateData.tid, widget.uid, 0, numTopics, cutoff); 427 | } else if (widget.templateData.template.category) { 428 | topicData = await getCategoryTopics('month', 'votes'); 429 | if (!topicData.length) { 430 | topicData = await getCategoryTopics('alltime', 'recent'); 431 | } 432 | topicData = _.shuffle(topicData).slice(0, numTopics + 1); 433 | topicData = topicData.filter(topic => topic && !topic.deleted); 434 | } else { 435 | const data = await topics.getTopicsFromSet('topics:recent', widget.uid, 0, numTopics); 436 | topicData = data ? data.topics : []; 437 | topicData = topicData.filter(topic => topic && !topic.deleted); 438 | } 439 | 440 | 441 | widget.html = await app.renderAsync('widgets/suggestedtopics', { 442 | topics: topicData, 443 | config: widget.templateData.config, 444 | sidebar: sidebarLocations.includes(widget.location), 445 | relative_path: nconf.get('relative_path'), 446 | }); 447 | return widget; 448 | }; 449 | 450 | Widget.renderUserPost = async function (widget) { 451 | const numPosts = Math.max(1, (widget.data.numPosts || 1)); 452 | const type = widget.data.postType || 'last'; 453 | let { uid } = widget; 454 | if (widget.templateData.template['account/profile']) { 455 | uid = widget.templateData.uid; 456 | } else if (widget.data.uid) { 457 | uid = widget.data.uid; 458 | } 459 | let pids = []; 460 | const cids = await categories.getCidsByPrivilege('categories:cid', widget.uid, 'topics:read'); 461 | const sets = cids.map(c => `cid:${c}:uid:${uid}:pids`); 462 | const now = Date.now(); 463 | if (type === 'last') { 464 | pids = await db.getSortedSetRevRangeByScore(sets, 0, numPosts, now, '-inf'); 465 | } else if (type === 'first') { 466 | pids = await db.getSortedSetRange(sets, 0, numPosts, now, '-inf'); 467 | } else if (type === 'best') { 468 | pids = await db.getSortedSetRevRange( 469 | cids.map(c => `cid:${c}:uid:${uid}:pids:votes`), 470 | 0, 471 | numPosts, 472 | now, 473 | '-inf' 474 | ); 475 | } 476 | const postObjs = await posts.getPostSummaryByPids(pids, widget.uid, { stripTags: false }); 477 | if (!postObjs.length) { 478 | return null; 479 | } 480 | widget.html = await app.renderAsync('widgets/userpost', { 481 | posts: postObjs, 482 | config: widget.templateData.config, 483 | relative_path: nconf.get('relative_path'), 484 | }); 485 | return widget; 486 | }; 487 | 488 | Widget.renderChatRoom = async function (widget) { 489 | const roomId = (widget.data.roomId || 0); 490 | if (!roomId) { 491 | return null; 492 | } 493 | 494 | const { uid } = widget; 495 | const chatsAPI = require.main.require('./src/api/chats'); 496 | const messaging = require.main.require('./src/messaging'); 497 | try { 498 | const [roomData, publicRooms] = await Promise.all([ 499 | chatsAPI.get({ uid: uid }, { uid, roomId }), 500 | messaging.getPublicRooms(uid, uid), 501 | ]); 502 | 503 | if (!roomData) { 504 | return null; 505 | } 506 | publicRooms.forEach((room) => { 507 | if (room && parseInt(room.roomId, 10) === parseInt(roomId, 10)) { 508 | room.selected = true; 509 | } 510 | }); 511 | 512 | widget.html = await app.renderAsync('widgets/chat', { 513 | roomId: roomId, 514 | isWidget: true, 515 | ...roomData, 516 | publicRooms, 517 | config: widget.templateData.config, 518 | relative_path: nconf.get('relative_path'), 519 | }); 520 | } catch (err) { 521 | if (err.message === '[[error:no-privileges]]') { 522 | return null; 523 | } 524 | throw err; 525 | } 526 | 527 | return widget; 528 | }; 529 | 530 | Widget.defineWidgets = async function (widgets) { 531 | const widgetData = [ 532 | { 533 | widget: 'html', 534 | name: 'HTML', 535 | description: 'Any text, html, or embedded script.', 536 | content: 'admin/partials/widgets/html', 537 | }, 538 | { 539 | widget: 'text', 540 | name: 'Text', 541 | description: 'Text, optionally parsed as a post.', 542 | content: 'admin/partials/widgets/text', 543 | }, 544 | { 545 | widget: 'search', 546 | name: 'Search', 547 | description: 'A search widget', 548 | content: 'admin/partials/widgets/search', 549 | }, 550 | { 551 | widget: 'onlineusers', 552 | name: 'Online Users', 553 | description: 'List of online users', 554 | content: 'admin/partials/widgets/onlineusers', 555 | }, 556 | { 557 | widget: 'activeusers', 558 | name: 'Active Users', 559 | description: 'List of active users in a category/topic', 560 | content: 'admin/partials/widgets/activeusers', 561 | }, 562 | { 563 | widget: 'latestusers', 564 | name: 'Latest Users', 565 | description: 'List of latest registered users.', 566 | content: 'admin/partials/widgets/latestusers', 567 | }, 568 | { 569 | widget: 'topposters', 570 | name: 'Top Posters', 571 | description: 'List of users with the most posts.', 572 | content: 'admin/partials/widgets/topposters', 573 | }, 574 | { 575 | widget: 'moderators', 576 | name: 'Moderators', 577 | description: 'List of moderators in a category.', 578 | content: 'admin/partials/widgets/moderators', 579 | }, 580 | { 581 | widget: 'forumstats', 582 | name: 'Forum Stats', 583 | description: 'Lists user, topics, and post count.', 584 | content: 'admin/partials/widgets/forumstats', 585 | }, 586 | { 587 | widget: 'recentposts', 588 | name: 'Recent Posts', 589 | description: 'Lists the latest posts on your forum.', 590 | content: 'admin/partials/widgets/recentposts', 591 | }, 592 | { 593 | widget: 'recenttopics', 594 | name: 'Recent Topics', 595 | description: 'Lists the latest topics on your forum.', 596 | content: 'admin/partials/widgets/recenttopics', 597 | }, 598 | { 599 | widget: 'recentview', 600 | name: 'Recent View', 601 | description: 'Renders the /recent page', 602 | content: 'admin/partials/widgets/defaultwidget', 603 | }, 604 | { 605 | widget: 'categories', 606 | name: 'Categories', 607 | description: 'Lists the categories on your forum', 608 | content: 'admin/partials/widgets/categories', 609 | }, 610 | { 611 | widget: 'populartags', 612 | name: 'Popular Tags', 613 | description: 'Lists popular tags on your forum', 614 | content: 'admin/partials/widgets/populartags', 615 | }, 616 | { 617 | widget: 'populartopics', 618 | name: 'Popular Topics', 619 | description: 'Lists popular topics on your forum', 620 | content: 'admin/partials/widgets/populartopics', 621 | }, 622 | { 623 | widget: 'toptopics', 624 | name: 'Top Topics', 625 | description: 'Lists top topics on your forum', 626 | content: 'admin/partials/widgets/toptopics', 627 | }, 628 | { 629 | widget: 'mygroups', 630 | name: 'My Groups', 631 | description: 'List of groups that you are in', 632 | content: 'admin/partials/widgets/mygroups', 633 | }, 634 | { 635 | widget: 'newgroups', 636 | name: 'New Groups', 637 | description: 'List of newest groups', 638 | content: 'admin/partials/widgets/mygroups', 639 | }, 640 | { 641 | widget: 'suggestedtopics', 642 | name: 'Suggested Topics', 643 | description: 'Lists of suggested topics.', 644 | content: 'admin/partials/widgets/suggestedtopics', 645 | }, 646 | { 647 | widget: 'userpost', 648 | name: 'User Post', 649 | description: 'Display a users first/last/best post on their profile or by user id.', 650 | content: 'admin/partials/widgets/userpost', 651 | }, 652 | { 653 | widget: 'chat', 654 | name: 'Chat Room', 655 | description: 'Display a chat room as a widget', 656 | content: 'admin/partials/widgets/chat', 657 | }, 658 | ]; 659 | 660 | await Promise.all(widgetData.map(async (widget) => { 661 | widget.content = await app.renderAsync(widget.content, {}); 662 | })); 663 | 664 | widgets = widgets.concat(widgetData); 665 | const groupNames = await db.getSortedSetRevRange('groups:visible:createtime', 0, -1); 666 | let groupsData = await groups.getGroupsData(groupNames); 667 | groupsData = groupsData.filter(Boolean); 668 | groupsData.forEach((group) => { 669 | group.name = validator.escape(String(group.name)); 670 | }); 671 | 672 | const html = await app.renderAsync('admin/partials/widgets/groupposts', { groups: groupsData }); 673 | widgets.push({ 674 | widget: 'groupposts', 675 | name: 'Group Posts', 676 | description: 'Posts made my members of a group', 677 | content: html, 678 | }); 679 | 680 | return widgets; 681 | }; 682 | --------------------------------------------------------------------------------