Merge branch 'v12.x'

main
落雨楓 2 years ago
commit ba39ceabe0

@ -0,0 +1,3 @@
{
"extends": "nodebb/lib"
}

@ -1,7 +1,9 @@
Persona theme for NodeBB
====================
Persona is the new default theme for NodeBB as of v0.7.1
The Persona theme is the default theme for NodeBB for versions spanning v0.7.1 through to v2.x
For the v3.x release line, Persona will be a supported theme bundled with NodeBB, but will not be active by default.
## Addons

@ -1,4 +1,10 @@
{
"mobile-menu-side": "Switch which side each mobile menu is on",
"post-quick-reply": "Post quick reply"
"settings.title": "Theme settings",
"settings.intro": "You can customise your theme settings here. Settings are stored on a per-device basis, so you are able to have different settings on different devices (phone, tablet, desktop, etc.)",
"settings.mobile-menu-side": "Switch which side each mobile menu is on",
"settings.autoHidingNavbar": "Automatically hide the navbar on scroll",
"settings.autoHidingNavbar-xs": "Very small screens (e.g. phones in portrait mode)",
"settings.autoHidingNavbar-sm": "Smaller screens (e.g. phones, some tablets)",
"settings.autoHidingNavbar-md": "Medium sized screens (e.g. tablets in landscape mode)",
"settings.autoHidingNavbar-lg": "Larger screens (e.g. desktop computers)"
}

@ -32,7 +32,7 @@
margin-bottom: 1em;
background-origin: content-box;
width: 100%;
top: 50px;
top: calc(var(--panel-offset) - 20px);
position: absolute;
left: auto;
right: 0px;
@ -45,7 +45,7 @@
border: 4px solid white;
border-radius: 50%;
.fab.btn-morph {
.persona-fab.btn-morph {
top: 93px;
right: 4px;
position: absolute;
@ -77,7 +77,7 @@
&:hover {
.controls {
.opacity(0.8);
opacity: 0.8;
}
}
@ -85,7 +85,7 @@
text-align: center;
height: 200px;
line-height: 200px;
.opacity(0);
opacity: 0;
.transition(opacity .15s linear);
cursor: pointer;
pointer-events: none;
@ -317,7 +317,7 @@
background-color: lighten(@brand-primary, 10%);
}
.fab {
.persona-fab {
color: white;
font-size: 20px;
}

@ -113,7 +113,7 @@
border: 1px solid @btn-success-border;
color: @btn-success-color;
}
}
}
}
.fa-stack {

@ -1,7 +1,5 @@
// Make chats page edge-to-edge
.page-user-chats {
padding-top: 50px;
#content.container {
width: auto;
padding: 0;
@ -12,7 +10,6 @@
}
#panel {
padding-top: 0px;
padding-bottom: 0px;
}
@ -25,31 +22,21 @@
.chats-full, .chat-modal {
display: flex;
flex-wrap: nowrap;
height: calc(100vh - var(--panel-offset));
[component="chat/nav-wrapper"] {
flex: 1;
flex-direction: column;
box-shadow: 0 3px 9px rgba(0,0,0,.5);
padding: 0px 15px;
.chats-list {
flex: 1;
overflow-y: auto;
margin-bottom: 0;
height: ~"calc(100% - 3em)";
height: ~"calc(100% - 5em)";
}
.chat-search {
background-color: @panel-default-heading-bg;
border-bottom: 1px solid @gray-dark;
input {
background-color: @gray-dark;
color: @gray-lighter;
border-radius: 0;
border: none;
height: ~"calc(3em - 2px)";
}
padding-bottom: 15px;
ul {
width: 100%;
}
@ -59,7 +46,6 @@
padding: 0;
overflow-x: hidden;
overflow-y: auto;
border-top: 1px solid @gray-lighter;
li {
position: relative;
@ -71,8 +57,7 @@
border-left: 1px solid;
border-right: 1px solid;
border-bottom: 1px solid;
border-color: #eee;
background: #fff;
border-color: @gray-lighter;
i {
position: relative;
@ -93,7 +78,7 @@
[component="chat/main-wrapper"] {
flex: 3;
padding-bottom: 15px;
.alert {
margin: 1em;
}
@ -111,9 +96,7 @@
[component="chat/header"] {
padding: @panel-heading-padding;
background-color: @gray-dark;
border-bottom: none;
color: @gray-lighter;
border-bottom: 1px solid @modal-header-border-color;
span {
font-weight: 500;
@ -121,13 +104,11 @@
.close {
margin-left: 0.5em;
color: @gray-lighter;
}
.members {
a {
font-weight: 600;
color: @gray-lighter;
}
}
@ -163,7 +144,7 @@
[component="chat/message/remaining"] {
position: absolute;
right: 5.25em;
right: 10em;
z-index: 2;
bottom: 0;
color: @gray-light;
@ -175,11 +156,6 @@
margin-top: 10px;
}
.chat-list {
margin-top: -6px;
margin-left: -1px;
}
.chats-list {
padding: 0;
overflow-x: hidden;
@ -193,10 +169,10 @@
}
> li {
display: flex;
position: relative;
clear: both;
list-style-type: none;
padding: 0.5em;
height: 80px;
.pointer;
@ -214,36 +190,29 @@
&.unread {
background: lighten(@brand-primary, 35%);
border-bottom: 0;
}
.teaser-content {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
opacity: 0.8;
}
.room-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
[component="chat/title"] {
.notification-chat-content {
.room-name {
white-space: nowrap;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 350px;
display: inline-block;
text-overflow: ellipsis;
display: block;
[component="chat/title"] {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 350px;
display: inline-block;
}
}
}
.teaser-content, .room-name {
padding-left: 80px;
}
&.bg-primary {
background: @brand-primary;
border-bottom: 0;
@ -263,7 +232,8 @@
.teaser-timestamp {
font-size: 10px;
margin-top: 10px;
margin-top: .5rem;
margin-right: .5rem;
}
a {
@ -325,15 +295,12 @@
}
.main-avatar {
position: absolute;
top: 0px;
left: 0px;
.avatar {
height: 8rem;
width: 8rem;
font-size: 4rem;
border-radius: 0;
background: @gray-lighter;
}
}
@ -613,6 +580,13 @@
[component="chat/nav-wrapper"][data-loaded="0"] + [component="chat/main-wrapper"] {
display: none;
}
.chats-full, .chat-modal {
height: calc(100vh - var(--panel-offset));
}
[component="chat/messages"] {
width: calc(100vw);
}
}
[data-action="pop-out"] {

@ -22,14 +22,14 @@
margin-bottom: 1em;
background-origin: content-box;
width: 100%;
top: 50px;
top: calc(var(--panel-offset) - 20px);
position: absolute;
left: auto;
right: 0px;
&:hover {
.controls {
.opacity(0.8);
opacity: 0.8;
}
}
@ -37,7 +37,7 @@
text-align: center;
min-height: 200px;
line-height: 200px;
.opacity(0);
opacity: 0;
.transition(opacity .15s linear);
cursor: pointer;
pointer-events: none;

@ -1,36 +1,61 @@
.header, .slideout-menu {
.notifications.dropdown, .chats.dropdown {
.dropdown-menu {
padding: 0;
}
}
.notification-list {
overflow-x: hidden;
overflow-y: auto;
max-height: 250px;
padding: 0;
color: @gray-dark;
li.no-notifs {
text-align: center;
}
li {
display: flex;
gap: 1rem;
width: 400px;
text-align: left;
list-style-type: none;
padding: 0.5em;
clear: both;
padding: .75em 0.5em;
&.loading-text {
text-align: center;
&:hover {
background-color: @dropdown-link-hover-bg;
}
a {
white-space: normal;
margin: 0;
text-overflow: ellipsis;
&.loading-text, &.no-notifs {
justify-content: center;
.text {
margin-left: 40px;
margin-right: 60px;
display: block;
min-height: 32px;
a {
color: inherit;
}
}
.notification-chat-content {
flex: 1;
a {
white-space: normal;
margin: 0;
text-overflow: ellipsis;
.text {
margin-left: 40px;
margin-right: 60px;
display: block;
min-height: 32px;
}
}
}
.notification-chat-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: .5rem;
.relTime {
font-size: 10px;
}
}
@ -51,9 +76,13 @@
.pointer;
}
}
&.unread {
.bg-variant(@state-warning-bg);
background-color: @state-warning-bg;
&:hover {
background-color: darken(@state-warning-bg, 5%);
}
.mark-read:after {
font-weight: 900;
@ -68,22 +97,24 @@
> li {
.pointer;
width: 500px;
padding-bottom: 1rem;
padding: 0;
margin: 0;
gap: 0;
overflow-y: hidden;
.notification-chat-content {
padding: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
}
.teaser-content {
white-space: nowrap;
max-height: 19px;
padding-left: 90px;
padding-right: 10px;
}
&:hover {
background: @gray-lighter;
}
&:not(:last-child) {
border-bottom: 1px solid @gray-lighter;
border-bottom: 1px solid @dropdown-border;
}
&.no_active a {
@ -173,15 +204,20 @@
}
.notif-dropdown-link {
// margin-top: 1em;
border-top: 1px solid rgba(163, 163, 163, 0.5);
padding: 0 5px 0 5px;
.btn-group-justified {
table-layout: auto;
}
a {
display: block;
text-align: center;
padding: 0.5em 0;
padding: 0.5em 0.5em;
font-weight: 600;
color: @dropdown-link-color;
&:hover {
color: @dropdown-link-hover-color;
background: @dropdown-link-hover-bg;
}
}
}
@ -407,24 +443,3 @@ html[data-dir="rtl"] {
text-align: right;
}
}
#mobile-menu:focus i.fa-bars {
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear;
&::before {
font-family: "FontAwesome";
content: "\f110";
}
}
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}

@ -27,13 +27,6 @@
}
}
.opacity(@opacity: 1) {
-moz-opacity: @opacity;
opacity: @opacity;
-ms-filter: ~`"progid:DXImageTransform.Microsoft.Alpha(opacity=(" + "@{opacity}" * 100 + "))"`;
filter: ~`"alpha(opacity = (" + "@{opacity}" * 100 + "))"`;
}
.border-radius (@radius: 5px) {
-webkit-border-radius: @radius;
-moz-border-radius: @radius;
@ -65,7 +58,7 @@
margin-left: -2.5rem;
}
.avatar, .timeline-badge {
.icon .avatar, .timeline-badge {
// Opaque ring
position: relative;
z-index: 1;
@ -107,6 +100,10 @@
&+.timeline-event:before {
display: none;
}
.timeline-text.timeago {
display: none;
}
}
}
@ -152,6 +149,8 @@
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 32px;
height: 32px;
padding: 0;

@ -34,15 +34,9 @@
}
@media (max-width: @screen-md-max) {
body {
padding-top: 0;
padding-bottom: 0;
}
#panel {
background-color: inherit;
min-height: 100%;
padding-top: 80px;
padding-bottom: 40px;
}
@ -59,20 +53,59 @@
html[data-dir="rtl"] button& {
margin-left: 0;
}
&#mobile-menu {
.unread-count::after {
left: 32px;
}
}
.header & .notification-icon {
left: auto;
right: 7px;
top: 10px;
&[component="notifications/icon"] {
right: 41px;
}
&.unread-count::after {
position: static;
}
}
}
.navbar-header .navbar-search {
input[name="term"] {
width: 150px;
}
#menu {
padding-top: 100px;
padding: 4px 0px 4px 0px;
}
#menu .menu-section {
padding-top: 20px;
}
#chats-menu {
.nav-pills {
[component="user/status"] {
position: absolute;
right: 24px;
}
background-color: #101010;
height: 50px;
li {
margin: 0;
padding: 0;
width: 33%;
text-align: center;
height: 100%;
a {
height: 100%;
padding-top: 15px;
}
&.active {
a {
background-color: #1D1F20;
}
}
}
}
}
.slideout-menu {
@ -141,10 +174,18 @@
white-space: nowrap;
text-overflow: ellipsis;
}
.teaser-timestamp {
font-size: 10px;
margin-right: 0.5rem;
margin-top: 0.5rem;
}
}
}
.menu-section {
.notification-list-mobile li .text {
display: block;
}
.chat-list, .notification-list-mobile {
.user-link {
display: inline;
@ -152,6 +193,15 @@
.unread {
background-color: inherit;
}
.notification-chat-content {
padding-top: 10px;
padding-right: 20px;
}
.notification-chat-controls {
display: none;
}
}
.chat-list .unread .room-name::after,
.notification-list-mobile .unread a::after {
@ -171,7 +221,6 @@
font-style: normal;
&:after {
left: 5px;
top: -1px;
padding: 3px 7px;
background: #333;
@ -190,10 +239,6 @@
margin: 0;
}
.menu-section {
margin: 25px 0;
}
.menu-section-title {
text-transform: uppercase;
color: #85888d;
@ -206,7 +251,7 @@
.menu-section-list {
padding: 0;
margin: 10px 0;
margin: 0;
list-style: none;
a, button {

@ -1,4 +1,4 @@
.fab {
.persona-fab {
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.156863), 0px 2px 10px 0px rgba(0, 0, 0, 0.117647);
background-color: @brand-primary;
@ -11,11 +11,11 @@
width: 55.5px;
}
.btn-group.open .dropdown-toggle.fab {
.btn-group.open .dropdown-toggle.persona-fab {
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.156863), 0px 2px 10px 0px rgba(0, 0, 0, 0.117647);
}
.fab.btn-morph {
.persona-fab.btn-morph {
padding: 0;
&.heart {

@ -6,14 +6,8 @@
.taskbar {
display: none;
z-index: @zindex-popover;
left: auto;
// Bootswatch fix
&.navbar-fixed-bottom {
z-index: @zindex-popover;
left: auto;
}
left: 15px;
right: auto;
margin-top: 0;
.transition(.15s ease-in opacity);
@ -35,11 +29,14 @@
}
.navbar-nav {
float: unset;
display: flex;
flex-direction: column;
padding-right: 15px;
padding-bottom: 15px;
li {
float: left;
margin-top: 1rem;
&.new a {
-webkit-animation-name: bounceIn;
@ -162,7 +159,6 @@
&.taskbar-composer, &.taskbar-chat {
a {
text-align: center;
margin-left: 15px;
i {
font-size: 1.8rem;

@ -57,7 +57,7 @@
}
}
.fab.btn-morph {
.persona-fab.btn-morph {
top: 75px;
right: 15px;
position: absolute;

@ -64,7 +64,6 @@
font-weight: 400;
line-height: 1.42857143;
white-space: nowrap;
color: @gray-dark;
}
}
.quick-search-title {
@ -74,10 +73,18 @@
.snippet {
word-break: break-word;
white-space: normal;
font-size: initial;
}
}
}
@media (max-width: @screen-xs-max) {
.quick-search-container {
left: 0px;
right: 0px;
}
}
.quick-search-results, .search-results {
.post-info {
font-size: 12px;

@ -4,20 +4,13 @@ html {
}
body {
@media (min-width: 979px)
{
padding-top: 70px;
}
@media (max-width: 979px)
{
padding-top: 70px;
padding-bottom: 50px;
}
min-height: 100%;
}
#panel {
padding-top: var(--panel-offset);
}
@media (max-width: @screen-xs-max) {
.slideout-panel {
min-height: 100vh;
@ -273,6 +266,10 @@ a.permalink {
}
}
.deco-none, .deco-none:link, .deco-none:hover {
color: inherit;
text-decoration: inherit;
}
.disabled a {
pointer-events: none;

@ -30,7 +30,8 @@
.tag {
text-transform: uppercase;
font-size: 10px;
background: lighten(@gray-lighter, 2.5%);
background: #e9ecef;
color: #7a8288;
padding: 5px;
white-space: nowrap;
}

@ -41,7 +41,7 @@
}
.topic-header {
position: sticky;
top: @navbar-height;
top: calc(var(--panel-offset) - 20px);
background-color: @body-bg;
z-index: @zindex-navbar;
margin-left: -15px;
@ -163,20 +163,18 @@
display: inline-block;
padding: 1rem;
&:first-child {
padding-right: 0.5rem;
}
&:last-child {
padding-left: 0.5rem;
}
&:focus {
text-decoration: none;
}
}
[component="post/upvote"].upvoted, [component="post/downvote"].downvoted {
[component="post/upvote"].upvoted i::before {
content: @fa-var-chevron-circle-up;
color: @brand-primary;
}
[component="post/downvote"].downvoted i::before {
content: @fa-var-chevron-circle-down;
color: @brand-primary;
}
@ -188,7 +186,6 @@
[component="post/parent"] {
border: 0;
font-size: 10px;
background-color: #f0f0f0;
}
.threaded-replies {
@ -374,6 +371,7 @@
}
.quick-reply {
position: relative;
.icon {
position: relative;
border-radius: 50%;
@ -421,7 +419,7 @@
.topic {
&.deleted {
.opacity(0.3);
opacity: 0.3;
.votes {
display: none;
@ -446,7 +444,7 @@
&.deleted {
> .content {
.opacity(0.3);
opacity: 0.3;
}
.votes {
@ -661,6 +659,16 @@
z-index: 1;
}
.selection-tooltip-container {
position: absolute;
padding: 5px;
border: 1px solid @well-border;
background-color: @well-bg;
border-radius: 3px;
margin: 10px 0px 0px 0px;
z-index: 1;
}
@media screen and (min-width: @screen-sm-min) {
.fork-thread-card {
max-width: 33%;

@ -0,0 +1,22 @@
'use strict';
const accountHelpers = require.main.require('./src/controllers/accounts/helpers');
const helpers = require.main.require('./src/controllers/helpers');
const Controllers = module.exports;
Controllers.renderAdminPage = (req, res) => {
res.render('admin/plugins/persona', {});
};
Controllers.renderThemeSettings = async (req, res, next) => {
const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query);
if (!userData) {
return next();
}
userData.title = '[[persona:settings.title]]';
userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[persona:settings.title]]' }]);
res.render('account/theme', userData);
};

@ -1,43 +1,67 @@
'use strict';
var meta = require.main.require('./src/meta');
var user = require.main.require('./src/user');
const meta = require.main.require('./src/meta');
const user = require.main.require('./src/user');
const translator = require.main.require('./src/translator');
var library = {};
const controllers = require('./lib/controllers');
library.init = function(params, callback) {
var app = params.router;
var middleware = params.middleware;
const library = module.exports;
app.get('/admin/plugins/persona', middleware.admin.buildHeader, renderAdmin);
app.get('/api/admin/plugins/persona', renderAdmin);
library.init = async function (params) {
const { router, middleware } = params;
const routeHelpers = require.main.require('./src/routes/helpers');
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/persona', [], controllers.renderAdminPage);
callback();
routeHelpers.setupPageRoute(router, '/user/:userslug/theme', [
middleware.exposeUid,
middleware.ensureLoggedIn,
middleware.canViewUsers,
middleware.checkAccountPermissions,
], controllers.renderThemeSettings);
};
library.addAdminNavigation = function(header, callback) {
library.addAdminNavigation = async function (header) {
header.plugins.push({
route: '/plugins/persona',
icon: 'fa-paint-brush',
name: 'Persona Theme'
name: 'Persona Theme',
});
return header;
};
callback(null, header);
library.addProfileItem = async (data) => {
data.links.push({
id: 'theme',
route: 'theme',
icon: 'fa-paint-brush',
name: await translator.translate('[[persona:settings.title]]'),
visibility: {
self: true,
other: false,
moderator: false,
globalMod: false,
admin: false,
},
});
return data;
};
library.defineWidgetAreas = function(areas, callback) {
library.defineWidgetAreas = async function (areas) {
const locations = ['header', 'sidebar', 'footer'];
const templates = [
'categories.tpl', 'category.tpl', 'topic.tpl', 'users.tpl',
'unread.tpl', 'recent.tpl', 'popular.tpl', 'top.tpl', 'tags.tpl', 'tag.tpl'
'unread.tpl', 'recent.tpl', 'popular.tpl', 'top.tpl', 'tags.tpl', 'tag.tpl',
'login.tpl', 'register.tpl',
];
function capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
return str.charAt(0).toUpperCase() + str.slice(1);
}
templates.forEach(template => {
locations.forEach(location => {
templates.forEach((template) => {
locations.forEach((location) => {
areas.push({
name: capitalizeFirst(template.split('.')[0]) + ' ' + capitalizeFirst(location),
name: `${capitalizeFirst(template.split('.')[0])} ${capitalizeFirst(location)}`,
template: template,
location: location,
});
@ -46,16 +70,25 @@ library.defineWidgetAreas = function(areas, callback) {
areas = areas.concat([
{
name: "Account Header",
template: "account/profile.tpl",
location: "header"
name: 'Main post header',
template: 'topic.tpl',
location: 'mainpost-header',
},
{
name: 'Main post footer',
template: 'topic.tpl',
location: 'mainpost-footer',
},
{
name: 'Account Header',
template: 'account/profile.tpl',
location: 'header',
},
]);
callback(null, areas);
return areas;
};
library.getThemeConfig = async function(config) {
library.getThemeConfig = async function (config) {
const settings = await meta.settings.get('persona');
config.hideSubCategories = settings.hideSubCategories === 'on';
config.hideCategoryLastPost = settings.hideCategoryLastPost === 'on';
@ -63,23 +96,23 @@ library.getThemeConfig = async function(config) {
return config;
};
function renderAdmin(req, res, next) {
res.render('admin/plugins/persona', {});
}
library.addUserToTopic = async function (hookData) {
if (hookData.req.user) {
const userData = await user.getUserData(hookData.req.user.uid);
hookData.templateData.loggedInUser = userData;
} else {
hookData.templateData.loggedInUser = {
uid: 0,
username: '[[global:guest]]',
picture: user.getDefaultAvatar(),
'icon:text': '?',
'icon:bgColor': '#aaa',
};
const settings = await meta.settings.get('persona');
if (settings.enableQuickReply === 'on') {
if (hookData.req.user) {
const userData = await user.getUserData(hookData.req.user.uid);
hookData.templateData.loggedInUser = userData;
} else {
hookData.templateData.loggedInUser = {
uid: 0,
username: '[[global:guest]]',
picture: user.getDefaultAvatar(),
'icon:text': '?',
'icon:bgColor': '#aaa',
};
}
}
return hookData;
};

@ -1,14 +1,17 @@
{
"name": "nodebb-theme-persona",
"version": "11.2.2",
"version": "12.1.15",
"nbbpm": {
"compatibility": "^1.18.0"
"compatibility": "^2.0.0"
},
"description": "Persona theme for NodeBB",
"main": "library.js",
"repository": {
"type": "git",
"url": "https://github.com/psychobunny/nodebb-theme-persona"
"url": "https://github.com/NodeBB/nodebb-theme-persona"
},
"scripts": {
"lint": "eslint ."
},
"keywords": [
"nodebb",
@ -20,26 +23,31 @@
"contributors": [
{
"name": "Andrew Rodrigues",
"email": "andrew@designcreateplay.com",
"email": "andrew@nodebb.org",
"url": "https://github.com/psychobunny"
},
{
"name": "Julian Lam",
"email": "julian@designcreateplay.com",
"email": "julian@nodebb.org",
"url": "https://github.com/julianlam"
},
{
"name": "Barış Soner Uşaklı",
"email": "baris@designcreateplay.com",
"email": "baris@nodebb.org",
"url": "https://github.com/barisusakli"
}
],
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/psychobunny/nodebb-theme-persona/issues"
"url": "https://github.com/NodeBB/nodebb-theme-persona/issues"
},
"dependencies": {
"pulling": "^2.0.0",
"striptags": "^3.2.0"
},
"devDependencies": {
"eslint": "^7.32.0",
"eslint-config-nodebb": "^0.0.2",
"eslint-plugin-import": "^2.24.2"
}
}

@ -5,18 +5,17 @@
{ "hook": "filter:config.get", "method": "getThemeConfig" },
{ "hook": "static:app.load", "method": "init" },
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
{ "hook": "filter:user.profileMenu", "method": "addProfileItem" },
{ "hook": "filter:topic.build", "method": "addUserToTopic" }
],
"scripts": [
"public/persona.js",
"public/modules/autohidingnavbar.js",
"public/modules/quickreply.js"
"public/persona.js"
],
"modules": {
"pulling.js": "node_modules/pulling/build/pulling-drawer.js"
"../admin/plugins/persona.js": "public/admin.js",
"persona/quickreply.js": "public/modules/quickreply.js",
"../client/account/theme.js": "public/settings.js"
},
"acpScripts": [
"public/admin.js"
],
"languages": "languages"
}

@ -0,0 +1,3 @@
{
"extends": "nodebb/public"
}

@ -1,24 +1,15 @@
'use strict';
/* globals $, app */
define('admin/plugins/persona', ['settings'], function(Settings) {
define('admin/plugins/persona', ['settings'], function (Settings) {
var ACP = {};
ACP.init = function() {
ACP.init = function () {
Settings.load('persona', $('.persona-settings'));
$('#save').on('click', function() {
Settings.save('persona', $('.persona-settings'), function() {
app.alert({
type: 'success',
alert_id: 'persona-saved',
title: 'Settings Saved',
message: 'Persona settings saved'
});
});
$('#save').on('click', function () {
Settings.save('persona', $('.persona-settings'));
});
};
return ACP;
});
});

@ -1,14 +1,15 @@
"use strict";
/*globals $, app, ajaxify, socket*/
'use strict';
define('persona/quickreply', [
'components', 'composer/autocomplete', 'api'
], function(components, autocomplete, api) {
'components', 'composer', 'composer/autocomplete', 'api',
'alerts', 'uploadHelpers', 'mousetrap',
], function (
components, composer, autocomplete, api,
alerts, uploadHelpers, mousetrap
) {
var QuickReply = {};
QuickReply.init = function() {
QuickReply.init = function () {
var element = components.get('topic/quickreply/text');
var data = {
element: element,
@ -16,40 +17,85 @@ define('persona/quickreply', [
options: {
style: {
'z-index': 100,
}
},
// listPosition: function(position) {
// this.$el.css(this._applyPlacement(position));
// this.$el.css('position', 'absolute');
// return this;
// }
}
},
};
$(window).trigger('composer:autocomplete:init', data);
autocomplete._active['persona_qr'] = autocomplete.setup(data);
autocomplete._active.persona_qr = autocomplete.setup(data);
// data.element.textcomplete(data.strategies, data.options);
// $('.textcomplete-wrapper').css('height', '100%').find('textarea').css('height', '100%');
components.get('topic/quickreply/button').on('click', function(e) {
mousetrap.bind('ctrl+return', (e) => {
if (e.target === element.get(0)) {
components.get('topic/quickreply/button').get(0).click();
}
});
uploadHelpers.init({
dragDropAreaEl: $('[component="topic/quickreply/container"] .quickreply-message'),
pasteEl: element,
uploadFormEl: $('[component="topic/quickreply/upload"]'),
inputEl: element,
route: '/api/post/upload',
callback: function (uploads) {
let text = element.val();
uploads.forEach((upload) => {
text = text + (text ? '\n' : '') + (upload.isImage ? '!' : '') + `[${upload.filename}](${upload.url})`;
});
element.val(text);
},
});
var ready = true;
components.get('topic/quickreply/button').on('click', function (e) {
e.preventDefault();
if (!ready) {
return;
}
var replyMsg = components.get('topic/quickreply/text').val();
var replyData = {
tid: ajaxify.data.tid,
handle: undefined,
content: replyMsg
content: replyMsg,
};
ready = false;
api.post(`/topics/${ajaxify.data.tid}`, replyData, function (err, data) {
ready = true;
if (err) {
return app.alertError(err.message);
return alerts.error(err);
}
if (data && data.queued) {
app.alertSuccess(data.message);
alerts.alert({
type: 'success',
title: '[[global:alert.success]]',
message: data.message,
timeout: 10000,
clickfn: function () {
ajaxify.go(`/post-queue/${data.id}`);
},
});
}
components.get('topic/quickreply/text').val('');
autocomplete._active['persona_qr'].hide();
autocomplete._active.persona_qr.hide();
});
});
components.get('topic/quickreply/expand').on('click', (e) => {
e.preventDefault();
const textEl = components.get('topic/quickreply/text');
composer.newReply(ajaxify.data.tid, undefined, ajaxify.data.title, utils.escapeHTML(textEl.val()));
textEl.val('');
});
};
return QuickReply;

@ -1,35 +1,36 @@
"use strict";
/*globals ajaxify, config, utils, app, socket, window, document, $*/
'use strict';
$(document).ready(function () {
setupNProgress();
setupTaskbar();
setupEditedByIcon();
setupMobileMenu();
setupQuickReply();
configureNavbarHiding();
fixHeaderPadding();
updatePanelOffset();
$(window).on('resize', utils.debounce(configureNavbarHiding, 200));
$(window).on('resize', fixHeaderPadding);
$(window).on('action:app.loggedIn', function () {
setupMobileMenu();
});
$(window).on('action:app.load', function () {
setupTaskbar();
setupMobileMenu();
});
function fixHeaderPadding() {
var env = utils.findBootstrapEnvironment();
if(!$('#header-menu').hasClass('hidden')){
if (env === 'sm' || env === 'xs' || env === 'md') {
$('#panel').css('padding-top', $('#header-menu').outerHeight(true));
} else {
$('#panel').css('padding-top', $('#header-menu').outerHeight(true) - 70);
}
$(window).on('resize', updatePanelOffset);
function updatePanelOffset() {
const headerEl = document.getElementById('header-menu');
if (!headerEl) {
console.warn('[persona/updatePanelOffset] Could not find #header-menu, panel offset unchanged.');
return;
}
const headerRect = headerEl.getBoundingClientRect();
const headerStyle = window.getComputedStyle(headerEl);
let offset =
headerRect.y + headerRect.height +
(parseInt(headerStyle.marginTop, 10) || 0) +
(parseInt(headerStyle.marginBottom, 10) || 0);
offset = Math.max(0, offset);
document.documentElement.style.setProperty('--panel-offset', `${offset}px`);
localStorage.setItem('panelOffset', offset);
}
var lastBSEnv = '';
@ -37,42 +38,65 @@ $(document).ready(function () {
if (!$.fn.autoHidingNavbar) {
return;
}
var env = utils.findBootstrapEnvironment();
// if env didn't change don't destroy and recreate
if (env === lastBSEnv) {
return;
}
lastBSEnv = env;
var navbarEl = $(".navbar-fixed-top");
navbarEl.autoHidingNavbar('destroy').removeData('plugin_autoHidingNavbar');
navbarEl.css('top', '');
if (env === 'xs' || env === 'sm') {
navbarEl.autoHidingNavbar({
showOnBottom: false,
});
}
function fixTopCss(topValue) {
if (ajaxify.data.template.topic) {
$('.topic .topic-header').css({top: topValue });
} else {
var topicListHeader = $('.topic-list-header');
if (topicListHeader.length) {
topicListHeader.css({ top: topValue });
}
require(['hooks', 'storage'], (hooks, Storage) => {
let preference = ['xs', 'sm'];
try {
preference = JSON.parse(Storage.getItem('persona:navbar:autohide')) || preference;
} catch (e) {
console.warn('[persona/settings] Unable to parse value for navbar autohiding');
}
}
var env = utils.findBootstrapEnvironment();
// if env didn't change don't destroy and recreate
if (env === lastBSEnv) {
return;
}
lastBSEnv = env;
var navbarEl = $('.navbar-fixed-top');
navbarEl.autoHidingNavbar('destroy').removeData('plugin_autoHidingNavbar');
navbarEl.css('top', '');
hooks
.on('filter:navigator.scroll', (data) => {
navbarEl.autoHidingNavbar('setDisableAutohide', true);
return data;
})
.on('action:navigator.scrolled', () => {
navbarEl.autoHidingNavbar('setDisableAutohide', false);
});
navbarEl.off('show.autoHidingNavbar')
.on('show.autoHidingNavbar', function() {
fixTopCss('');
});
hooks.fire('filter:persona.configureNavbarHiding', {
resizeEnvs: preference,
}).then(({ resizeEnvs }) => {
if (resizeEnvs.includes(env)) {
navbarEl.autoHidingNavbar({
showOnBottom: false,
});
}
function fixTopCss(topValue) {
if (ajaxify.data.template.topic) {
$('.topic .topic-header').css({ top: topValue });
} else {
var topicListHeader = $('.topic-list-header');
if (topicListHeader.length) {
topicListHeader.css({ top: topValue });
}
}
}
navbarEl.off('show.autoHidingNavbar')
.on('show.autoHidingNavbar', function () {
fixTopCss('');
});
navbarEl.off('hide.autoHidingNavbar')
.on('hide.autoHidingNavbar', function() {
fixTopCss('0px');
navbarEl.off('hide.autoHidingNavbar')
.on('hide.autoHidingNavbar', function () {
fixTopCss('0px');
});
});
});
}
function setupNProgress() {
@ -155,7 +179,8 @@ $(document).ready(function () {
function setupEditedByIcon() {
function activateEditedTooltips() {
$('[data-pid] [component="post/editor"]').each(function () {
var el = $(this), icon;
var el = $(this);
var icon;
if (!el.attr('data-editor')) {
return;
@ -184,14 +209,14 @@ $(document).ready(function () {
return;
}
require(['pulling', 'storage'], function (Pulling, Storage) {
require(['pulling/build/pulling-drawer', 'storage', 'alerts', 'search'], function (Pulling, Storage, alerts, search) {
if (!Pulling) {
return;
}
// initialization
var chatMenuVisible = !config.disableChat && app.user && parseInt(app.user.uid, 10);
var chatMenuVisible = app.user && parseInt(app.user.uid, 10);
var swapped = !!Storage.getItem('persona:menus:legacy-layout');
var margin = window.innerWidth;
@ -244,8 +269,9 @@ $(document).ready(function () {
$(window).on('resize action:ajaxify.start', function () {
navSlideout.close();
if (chatsSlideout) { chatsSlideout.close(); }
$('.account .cover').css('top', $('[component="navbar"]').height());
if (chatsSlideout) {
chatsSlideout.close();
}
});
navSlideout
@ -272,14 +298,6 @@ $(document).ready(function () {
navSlideout.enable().toggle();
});
function loadNotifications() {
require(['notifications'], function (notifications) {
notifications.loadNotifications($('#menu [data-section="notifications"] ul'));
});
}
navSlideout.on('opened', loadNotifications);
if (chatMenuVisible) {
navSlideout.on('beforeopen', function () {
chatsSlideout.close();
@ -289,22 +307,29 @@ $(document).ready(function () {
});
}
$('#menu [data-section="navigation"] ul').html($('#main-nav').html() + ($('#search-menu').html() || '') + ($('#logged-out-menu').html() || ''));
$('#menu [data-section="navigation"] ul').html(
$('#main-nav').html() +
($('#logged-out-menu').html() || '')
);
$('#user-control-list').children().clone(true, true).appendTo($('#menu [data-section="profile"] ul'));
$('#user-control-list').children().clone(true, true).appendTo($('#chats-menu [data-section="profile"] ul'));
socket.on('event:user_status_change', function (data) {
if (parseInt(data.uid, 10) === app.user.uid) {
app.updateUserStatus($('#menu [component="user/status"]'), data.status);
app.updateUserStatus($('#chats-menu [component="user/status"]'), data.status);
navSlideout.close();
}
});
// right slideout chats menu
// right slideout notifications & chats menu
function loadChats() {
require(['chat'], function (chat) {
chat.loadChatsDropdown($('#chats-menu .chat-list'));
function loadNotificationsAndChats() {
require(['notifications', 'chat'], function (notifications, chat) {
const notifList = $('#chats-menu [data-section="notifications"] ul');
notifications.loadNotifications(notifList, function () {
notifList.find('.deco-none').removeClass('deco-none');
chat.loadChatsDropdown($('#chats-menu .chat-list'));
});
});
}
@ -318,7 +343,7 @@ $(document).ready(function () {
});
chatsSlideout
.on('opened', loadChats)
.on('opened', loadNotificationsAndChats)
.on('beforeopen', function () {
navSlideout.close().disable();
})
@ -327,31 +352,35 @@ $(document).ready(function () {
});
}
// add a checkbox in the user settings page
// so users can swap the sides the menus appear on
function setupSetting() {
if (ajaxify.data.template['account/settings'] && !document.getElementById('persona:menus:legacy-layout')) {
require(['translator'], function (translator) {
translator.translate('[[persona:mobile-menu-side]]', function (translated) {
$('<div class="well checkbox"><label><input type="checkbox" id="persona:menus:legacy-layout"/><strong>' + translated + '</strong></label></div>')
.appendTo('#content .account > .row > div:first-child')
.find('input')
.prop('checked', Storage.getItem('persona:menus:legacy-layout', 'true'))
.change(function (e) {
if (e.target.checked) {
Storage.setItem('persona:menus:legacy-layout', 'true');
} else {
Storage.removeItem('persona:menus:legacy-layout');
}
});
});
})
const searchInputEl = $('.navbar-header .navbar-search input[name="term"]');
const searchButton = $('.navbar-header .navbar-search button[type="button"]');
searchButton.off('click').on('click', function () {
if (!config.loggedIn && !app.user.privileges['search:content']) {
alerts.alert({
message: '[[error:search-requires-login]]',
timeout: 3000,
});
ajaxify.go('login');
return false;
}
}
$(window).on('action:ajaxify.end', setupSetting);
setupSetting();
searchButton.addClass('hidden');
searchInputEl.removeClass('hidden').focus();
return false;
});
searchInputEl.on('blur', function () {
searchInputEl.addClass('hidden');
searchButton.removeClass('hidden');
});
search.enableQuickSearch({
searchElements: {
inputEl: searchInputEl,
resultEl: $('.navbar-header .navbar-search .quick-search-container'),
},
searchOptions: {
in: config.searchDefaultInQuick,
},
});
});
}
@ -436,13 +465,13 @@ $(document).ready(function () {
}
function setupFavouriteMorph(parent, uid, username) {
require(['api'], function (api) {
require(['api', 'alerts'], function (api, alerts) {
parent.find('.btn-morph').click(function (ev) {
var type = $(this).hasClass('plus') ? 'follow' : 'unfollow';
var method = $(this).hasClass('plus') ? 'put' : 'del';
api[method]('/users/' + uid + '/follow').then(() => {
app.alertSuccess('[[global:alert.' + type + ', ' + username + ']]');
alerts.success('[[global:alert.' + type + ', ' + username + ']]');
});
$(this).toggleClass('plus').toggleClass('heart');
@ -452,9 +481,9 @@ $(document).ready(function () {
$(this).prepend('<b class="drop"></b>');
}
var drop = $(this).find('b.drop').removeClass('animate'),
x = ev.pageX - drop.width() / 2 - $(this).offset().left,
y = ev.pageY - drop.height() / 2 - $(this).offset().top;
var drop = $(this).find('b.drop').removeClass('animate');
var x = ev.pageX - (drop.width() / 2) - $(this).offset().left;
var y = ev.pageY - (drop.height() / 2) - $(this).offset().top;
drop.css({ top: y + 'px', left: x + 'px' }).addClass('animate');
});

@ -0,0 +1,53 @@
'use strict';
define('forum/account/theme', ['forum/account/header', 'storage', 'settings', 'alerts'], function (header, Storage, settings, alerts) {
const Theme = {};
Theme.init = () => {
header.init();
Theme.setupForm();
};
Theme.setupForm = () => {
const saveEl = document.getElementById('save');
const formEl = document.getElementById('theme-settings');
const [sidebarSwapped, autohideNavbarEnvs] = [
!!Storage.getItem('persona:menus:legacy-layout'),
Storage.getItem('persona:navbar:autohide'),
];
document.getElementById('persona:menus:legacy-layout').checked = sidebarSwapped;
try {
const parsed = JSON.parse(autohideNavbarEnvs) || ['xs', 'sm'];
parsed.forEach((env) => {
const optionEl = document.getElementById('persona:navbar:autohide').querySelector(`option[value="${env}"]`);
optionEl.selected = true;
});
} catch (e) {
console.warn(e);
}
if (saveEl) {
saveEl.addEventListener('click', () => {
const themeSettings = settings.helper.serializeForm($(formEl));
Object.keys(themeSettings).forEach((key) => {
if (key === 'persona:menus:legacy-layout') {
if (themeSettings[key] === 'on') {
Storage.setItem('persona:menus:legacy-layout', 'true');
} else {
Storage.removeItem('persona:menus:legacy-layout');
}
return;
}
Storage.setItem(key, themeSettings[key]);
});
alerts.success('[[success:settings-saved]]');
});
}
};
return Theme;
});

@ -0,0 +1 @@
<!-- IMPORT account/posts.tpl -->

@ -1,30 +0,0 @@
<div class="account">
<!-- IMPORT partials/account/header.tpl -->
<form class="form-horizontal edit-form">
<div class="control-group">
<label class="control-label" for="inputNewEmail">[[user:email]]</label>
<div class="controls">
<input class="form-control" type="text" id="inputNewEmail" placeholder="[[user:email]]" value="{email}">
</div>
</div>
<!-- disables autocomplete on FF --><input type="password" style="display:none">
<!-- IF isSelf -->
<div class="control-group">
<label class="control-label" for="inputCurrentPassword">[[user:current_password]]</label>
<div class="controls">
<input autocomplete="off" class="form-control" type="password" id="inputCurrentPassword" placeholder="[[user:current_password]]" value=""<!-- IF !hasPassword --> disabled<!-- ENDIF !hasPassword -->>
</div>
</div>
<!-- ENDIF isSelf -->
<input type="hidden" name="uid" id="inputUID" value="{uid}" />
<br/>
<div class="form-actions">
<button id="submitBtn" class="btn btn-primary btn-block"><i class="hide fa fa-spinner fa-spin"></i> [[user:change_email]]</button>
</div>
</form>
</div>

@ -146,6 +146,49 @@
<!-- ENDIF history.bans.length -->
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
[[user:info.mute-history]]
{{{ if !muted }}}
{{{ if !isSelf }}}
<button class="btn btn-xs pull-right btn-danger" component="account/mute">[[user:mute_account]]</button>
{{{ end }}}
{{{ else }}}
{{{ if !isSelf }}}
<button class="btn btn-xs pull-right btn-success" component="account/unmute">[[user:unmute_account]]</button>
{{{ end }}}
{{{ end }}}
</h3>
</div>
<div class="panel-body">
{{{ if history.mutes.length }}}
<ul class="ban-history">
{{{ each history.mutes }}}
<li>
<p>
<a href="{config.relative_path}/user/{history.mutes.user.userslug}">{buildAvatar(history.mutes.user, "sm", true)}</a>
<strong>
<a href="<!-- IF history.mutes.user.userslug -->{config.relative_path}/user/{history.mutes.user.userslug}<!-- ELSE -->#<!-- ENDIF history.mutes.user.userslug -->" itemprop="author" data-username="{history.mutes.user.username}" data-uid="{history.mutes.user.uid}">{history.mutes.user.username}</a>
</strong>
<span class="timestamp timeago" title="{../timestampISO}"></span> &mdash; {../timestampReadable}<br />
{{{ if ../until }}}
<span class="expiry">[[user:info.muted-until, {../untilReadable}]]</span><br />
{{{ end }}}
<span class="reason"><strong>[[user:info.banned-reason-label]]</strong>: {../reason}</span>
</p>
</li>
{{{end}}}
</ul>
{{{ else }}}
<div class="alert alert-success">[[user:info.no-mute-history]]</div>
{{{ end }}}
</div>
</div>
<!-- IF isAdminOrGlobalModerator -->
<div class="panel panel-default">
<div class="panel-heading">

@ -18,8 +18,8 @@
<h4>[[user:select-homepage]]</h4>
<div class="well">
<div class="form-group">
<label for="dailyDigestFreq">[[user:homepage]]</label>
<select class="form-control" data-property="homePageRoute">
<label for="homePageRoute">[[user:homepage]]</label>
<select class="form-control" id="homePageRoute" data-property="homePageRoute">
<option value="none">None</option>
{{{each homePageRoutes}}}
<option value="{homePageRoutes.route}" <!-- IF homePageRoutes.selected -->selected="1"<!-- ENDIF homePageRoutes.selected -->>{homePageRoutes.name}</option>

@ -0,0 +1,27 @@
<div class="account">
<!-- IMPORT partials/account/header.tpl -->
<p>[[persona:settings.intro]]</p>
<hr />
<form id="theme-settings" role="form">
<div class="checkbox">
<label>
<input type="checkbox" id="persona:menus:legacy-layout" name="persona:menus:legacy-layout"> <strong>[[persona:settings.mobile-menu-side]]</strong>
</label>
</div><br />
<div class="form-group">
<label for="persona:navbar:autohide">[[persona:settings.autoHidingNavbar]]</label>
<select multiple class="form-control" name="persona:navbar:autohide" id="persona:navbar:autohide">
<option value="xs">[[persona:settings.autoHidingNavbar-xs]]</option>
<option value="sm">[[persona:settings.autoHidingNavbar-sm]]</option>
<option value="md">[[persona:settings.autoHidingNavbar-md]]</option>
<option value="lg">[[persona:settings.autoHidingNavbar-lg]]</option>
</select>
</div>
<button id="save" type="button" class="btn btn-primary">[[global:save_changes]]</button>
</form>
</div>

@ -6,30 +6,7 @@
<button id="chat-close-btn" type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<button type="button" class="close hidden-xs hidden-sm" data-action="maximize"><span aria-hidden="true"><i class="fa fa-expand"></i></span><span class="sr-only">[[modules:chat.maximize]]</span></button>
<button type="button" class="close hidden-xs hidden-sm" data-action="minimize"><span aria-hidden="true"><i class="fa fa-minus"></i></span><span class="sr-only">[[modules:chat.minimize]]</span></button>
<div class="dropdown pull-right">
<button class="close" data-toggle="dropdown" component="chat/controlsToggle"><i class="fa fa-gear"></i></button>
<ul class="dropdown-menu dropdown-menu-right pull-right" component="chat/controls">
<li class="dropdown-header">[[modules:chat.options]]</li>
<li>
<a href="#" data-action="members"><i class="fa fa-fw fa-cog"></i> [[modules:chat.manage-room]]</a>
</li>
<li>
<a href="#" data-action="rename"><i class="fa fa-fw fa-edit"></i> [[modules:chat.rename-room]]</a>
</li>
<li>
<a href="#" data-action="leave"><i class="fa fa-fw fa-sign-out"></i> [[modules:chat.leave]]</a>
</li>
<!-- IF users.length -->
<li role="separator" class="divider"></li>
<li class="dropdown-header">[[modules:chat.in-room]]</li>
{{{each users}}}
<li>
<a href="{config.relative_path}/uid/{../uid}">{buildAvatar(users, "sm", true)} {../username}</a>
</li>
{{{end}}}
<!-- END -->
</ul>
</div>
<!-- IMPORT partials/chats/options.tpl -->
<h4 component="chat/room/name"><!-- IF roomName -->{roomName}<!-- ELSE -->{usernames}<!-- ENDIF roomName --></h4>
</div>
@ -42,10 +19,15 @@
<div component="chat/composer">
<textarea component="chat/input" placeholder="[[modules:chat.placeholder]]" class="form-control chat-input mousetrap" rows="1"></textarea>
<button component="chat/upload/button" class="btn btn-light" type="button"><i class="fa fa-fw fa-2x fa-upload"></i></button>
<button class="btn btn-primary" type="button" data-action="send"><i class="fa fa-fw fa-2x fa-paper-plane"></i></button>
<span component="chat/message/remaining">{maximumChatMessageLength}</span>
<form component="chat/upload" method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple class="hidden"/>
</form>
</div>
</div>
<div class="imagedrop"><div>[[topic:composer.drag_and_drop_images]]</div></div>
</div>
</div>
</div>

@ -1,7 +1,7 @@
<div class="chats-full">
<div component="chat/nav-wrapper" data-loaded="<!-- IF roomId -->1<!-- ELSE -->0<!-- END -->">
<div class="chat-search dropdown">
<input class="form-control" type="text" component="chat/search" placeholder="[[users:enter_username]]" data-toggle="dropdown" />
<input class="form-control" type="text" component="chat/search" placeholder="[[users:search-user-for-chat]]" data-toggle="dropdown" />
<ul component="chat/search/list" class="dropdown-menu"></ul>
</div>
<ul component="chat/recent" class="chats-list" data-nextstart="{nextStart}">
@ -13,4 +13,5 @@
<div component="chat/main-wrapper">
<!-- IMPORT partials/chats/message-window.tpl -->
</div>
<div class="imagedrop"><div>[[topic:composer.drag_and_drop_images]]</div></div>
</div>

@ -60,10 +60,9 @@
<div class="form-group">
<h2 class="h4" for="state">[[flags:state]]</h2>
<select class="form-control" id="state" name="state" disabled>
<option value="open">[[flags:state-open]]</option>
<option value="wip">[[flags:state-wip]]</option>
<option value="resolved">[[flags:state-resolved]]</option>
<option value="rejected">[[flags:state-rejected]]</option>
{{{ each states }}}
<option value="{@key}">{./label}</option>
{{{ end }}}
</select>
</div>
<div class="form-group">
@ -147,7 +146,14 @@
<li><a href="#" data-action="chat">[[flags:start-new-chat]]</a></li>
{{{ end }}}
<li role="separator" class="divider"></li>
{{{ if privileges.ban }}}<li><a href="#" data-action="ban">[[user:ban_account]]</a></li>{{{ end }}}
{{{ if privileges.ban }}}
<li class="{{{ if target.user.banned }}}hidden{{{ end }}}"><a href="#" data-action="ban">[[user:ban_account]]</a></li>
<li class="{{{ if !target.user.banned }}}hidden{{{ end }}}"><a href="#" data-action="unban">[[user:unban_account]]</a></li>
{{{ end }}}
{{{ if privileges.mute}}}
<li class="{{{ if target.user.muted }}}hidden{{{ end }}}"><a href="#" data-action="mute">[[user:mute_account]]</a></li>
<li class="{{{ if !target.user.muted }}}hidden{{{ end }}}"><a href="#" data-action="unmute">[[user:unmute_account]]</a></li>
{{{ end }}}
{{{ if privileges.admin:users }}}
<li><a href="#" data-action="delete-account">[[user:delete_account_as_admin]]</a></li>
<li><a href="#" data-action="delete-content">[[user:delete_content]]</a></li>

@ -1,14 +1,6 @@
</div><!-- /.container#content -->
</main>
<!-- IF !isSpider -->
<div class="topic-search hidden">
<div class="btn-group">
<button type="button" class="btn btn-default count"></button>
<button type="button" class="btn btn-default prev"><i class="fa fa-fw fa-angle-up"></i></button>
<button type="button" class="btn btn-default next"><i class="fa fa-fw fa-angle-down"></i></button>
</div>
</div>
<div component="toaster/tray" class="alert-window">
<div id="reconnect-alert" class="alert alert-dismissable alert-warning clearfix hide" component="toaster/toast">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
@ -17,10 +9,6 @@
</div>
<!-- ENDIF !isSpider -->
<div class="hide">
<!-- IMPORT 500-embed.tpl -->
</div>
<!-- IMPORT partials/footer/js.tpl -->
</body>
</html>

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{function.localeToHTML, userLang, defaultLang}" {{{if languageDirection}}}data-dir="{languageDirection}" style="direction: {languageDirection};" {{{end}}} >
<html lang="{function.localeToHTML, userLang, defaultLang}" {{{if languageDirection}}}data-dir="{languageDirection}" style="direction: {languageDirection};"{{{end}}}>
<head>
<title>{browserTitle}</title>
{{{each metaTags}}}{function.buildMetaTag}{{{end}}}
@ -11,6 +11,7 @@
var app = {
user: JSON.parse('{{userJSON}}')
};
document.documentElement.style.setProperty('--panel-offset', `${localStorage.getItem('panelOffset') || 0}px`);
</script>
{{{if useCustomHTML}}}

@ -1,75 +1,93 @@
<!-- IMPORT partials/breadcrumbs.tpl -->
<div class="row">
<!-- IF allowLocalLogin -->
<div class="<!-- IF alternate_logins -->col-md-6<!-- ELSE -->col-md-12<!-- ENDIF alternate_logins -->">
<div class="login-block">
<div class="alert alert-danger" id="login-error-notify" <!-- IF error -->style="display:block"<!-- ELSE -->style="display: none;"<!-- ENDIF error -->>
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>[[login:failed_login_attempt]]</strong>
<p>{error}</p>
</div>
<form class="form-horizontal" role="form" method="post" id="login-form">
<div class="form-group">
<label for="username" class="col-lg-2 control-label">{allowLoginWith}</label>
<div class="col-lg-10">
<input class="form-control" type="text" placeholder="{allowLoginWith}" name="username" id="username" autocorrect="off" autocapitalize="off" value="{username}"/>
</div>
</div>
<div class="form-group">
<label for="password" class="col-lg-2 control-label">[[user:password]]</label>
<div class="col-lg-10">
<input class="form-control" type="password" placeholder="[[user:password]]" name="password" id="password" <!-- IF username -->autocomplete="off"<!-- ENDIF username -->/>
<p id="caps-lock-warning" class="text-danger hidden">
<i class="fa fa-exclamation-triangle"></i> [[login:caps-lock-enabled]]
</p>
<div data-widget-area="header">
{{{each widgets.header}}}
{{widgets.header.html}}
{{{end}}}
</div>
<div class="row login">
<div class="{{{ if widgets.sidebar.length }}}col-lg-9 col-sm-12{{{ else }}}col-lg-12{{{ end }}}">
<div class="row">
{{{ if allowLocalLogin }}}
<div class="{{{ if alternate_logins }}}col-md-6{{{ else }}}col-md-12{{{ end }}}">
<div class="login-block">
<div class="alert alert-danger" id="login-error-notify" <!-- IF error -->style="display:block"<!-- ELSE -->style="display: none;"<!-- ENDIF error -->>
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>[[login:failed_login_attempt]]</strong>
<p>{error}</p>
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<div class="checkbox">
<label>
<input type="checkbox" name="remember" id="remember" checked /> [[login:remember_me]]
</label>
<form class="form-horizontal" role="form" method="post" id="login-form">
<div class="form-group">
<label for="username" class="col-lg-2 control-label">{allowLoginWith}</label>
<div class="col-lg-10">
<input class="form-control" type="text" placeholder="{allowLoginWith}" name="username" id="username" autocorrect="off" autocapitalize="off" value="{username}"/>
</div>
</div>
</div>
</div>
{{{each loginFormEntry}}}
<div class="form-group loginFormEntry">
<label for="login-{loginFormEntry.styleName}" class="col-lg-4 control-label">{loginFormEntry.label}</label>
<div id="login-{loginFormEntry.styleName}" class="col-lg-8">{{loginFormEntry.html}}</div>
</div>
{{{end}}}
<input type="hidden" name="_csrf" value="{config.csrf_token}" />
<input type="hidden" name="noscript" id="noscript" value="true" />
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<button class="btn btn-primary btn-lg btn-block" id="login" type="submit">[[global:login]]</button>
<!-- IF allowRegistration -->
<span>[[login:dont_have_account]] <a href="{config.relative_path}/register">[[register:register]]</a></span>
<!-- ENDIF allowRegistration -->
<!-- IF allowPasswordReset -->
&nbsp; <a id="reset-link" href="{config.relative_path}/reset">[[login:forgot_password]]</a>
<!-- ENDIF allowPasswordReset -->
</div>
<div class="form-group">
<label for="password" class="col-lg-2 control-label">[[user:password]]</label>
<div class="col-lg-10">
<input class="form-control" type="password" placeholder="[[user:password]]" name="password" id="password" <!-- IF username -->autocomplete="off"<!-- ENDIF username -->/>
<p id="caps-lock-warning" class="text-danger hidden">
<i class="fa fa-exclamation-triangle"></i> [[login:caps-lock-enabled]]
</p>
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<div class="checkbox">
<label>
<input type="checkbox" name="remember" id="remember" checked /> [[login:remember_me]]
</label>
</div>
</div>
</div>
{{{each loginFormEntry}}}
<div class="form-group loginFormEntry">
<label for="login-{loginFormEntry.styleName}" class="col-lg-4 control-label">{loginFormEntry.label}</label>
<div id="login-{loginFormEntry.styleName}" class="col-lg-8">{{loginFormEntry.html}}</div>
</div>
{{{end}}}
<input type="hidden" name="_csrf" value="{config.csrf_token}" />
<input type="hidden" name="noscript" id="noscript" value="true" />
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<button class="btn btn-primary btn-lg btn-block" id="login" type="submit">[[global:login]]</button>
<!-- IF allowRegistration -->
<span>[[login:dont_have_account]] <a href="{config.relative_path}/register">[[register:register]]</a></span>
<!-- ENDIF allowRegistration -->
<!-- IF allowPasswordReset -->
&nbsp; <a id="reset-link" href="{config.relative_path}/reset">[[login:forgot_password]]</a>
<!-- ENDIF allowPasswordReset -->
</div>
</div>
</form>
</div>
</form>
</div>
{{{ end }}}
{{{ if alternate_logins }}}
<div class="{{{ if allowLocalLogin }}}col-md-6{{{ else }}}col-md-12{{{ end }}}">
<div class="alt-login-block">
<h4>[[login:alternative_logins]]</h4>
<ul class="alt-logins">
{{{each authentication}}}
<li class="{authentication.name}"><a rel="nofollow noopener noreferrer" target="_top" href="{config.relative_path}{authentication.url}"><i class="fa {authentication.icon} fa-3x"></i></a></li>
{{{end}}}
</ul>
</div>
</div>
{{{ end }}}
</div>
</div>
<!-- ENDIF allowLocalLogin -->
<!-- IF alternate_logins -->
<div class="<!-- IF allowLocalLogin -->col-md-6<!-- ELSE -->col-md-12<!-- ENDIF allowLocalLogin -->">
<div class="alt-login-block">
<h4>[[login:alternative_logins]]</h4>
<ul class="alt-logins">
{{{each authentication}}}
<li class="{authentication.name}"><a rel="nofollow noopener noreferrer" target="_top" href="{config.relative_path}{authentication.url}"><i class="fa {authentication.icon} fa-3x"></i></a></li>
{{{end}}}
</ul>
</div>
<div data-widget-area="sidebar" class="col-lg-3 col-sm-12 {{{ if !widgets.sidebar.length }}}hidden{{{ end }}}">
{{{each widgets.sidebar}}}
{{widgets.sidebar.html}}
{{{end}}}
</div>
<!-- ENDIF alternate_logins -->
</div>
<div data-widget-area="footer">
{{{each widgets.footer}}}
{{widgets.footer.html}}
{{{end}}}
</div>

@ -1,3 +1,3 @@
<div id="taskbar" class="taskbar navbar-fixed-bottom">
<div class="navbar-inner"><ul class="nav navbar-nav pull-right"></ul></div>
<div class="navbar-inner"><ul class="nav navbar-nav"></ul></div>
</div>

@ -24,8 +24,8 @@
<small>[[global:reputation]]</small>
<span class="human-readable-number">{reputation}</span>
</div>
<button class="btn-morph fab <!-- IF banned --> hide<!-- ENDIF banned -->">
<button class="btn-morph persona-fab <!-- IF banned --> hide<!-- ENDIF banned -->">
<span>
<span class="s1"></span>
<span class="s2"></span>

@ -17,7 +17,7 @@
<!-- IF loggedIn -->
<!-- IF !isSelf -->
<button class="btn-morph fab <!-- IF isFollowing -->heart<!-- ELSE -->plus<!-- ENDIF isFollowing -->" title="<!-- IF isFollowing -->[[global:unfollow]]<!-- ELSE -->[[global:follow]]<!-- ENDIF isFollowing -->">
<button class="btn-morph persona-fab <!-- IF isFollowing -->heart<!-- ELSE -->plus<!-- ENDIF isFollowing -->" title="<!-- IF isFollowing -->[[global:unfollow]]<!-- ELSE -->[[global:follow]]<!-- ENDIF isFollowing -->">
<span>
<span class="s1"></span>
<span class="s2"></span>

@ -1,5 +1,5 @@
<div class="btn-group account-fab bottom-sheet">
<button type="button" class="fab dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button type="button" class="persona-fab dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right">
@ -27,22 +27,32 @@
<li>
<a href="{config.relative_path}/user/{userslug}" class="inline-block" id="profile">[[user:profile]]</a>
</li>
<!-- IF showHidden -->
<!-- IF canEdit -->
<li><a href="{config.relative_path}/user/{userslug}/edit">[[user:edit]]</a></li>
<li><a href="{config.relative_path}/user/{userslug}/settings">[[user:settings]]</a></li>
<!-- ENDIF showHidden -->
<!-- ENDIF canEdit -->
<!-- IF !isSelf -->
<!-- IF canBan -->
{{{ if (canBan || canMute) }}}
<li role="separator" class="divider"></li>
<li class="dropdown-header">[[user:admin_actions_label]]</li>
{{{ end }}}
{{{ if canBan }}}
<li class="<!-- IF banned -->hide<!-- ENDIF banned -->">
<a component="account/ban" href="#">[[user:ban_account]]</a>
</li>
<li class="<!-- IF !banned -->hide<!-- ENDIF !banned -->">
<a component="account/unban" href="#">[[user:unban_account]]</a>
</li>
<!-- ENDIF canBan -->
{{{ end }}}
{{{ if canMute }}}
<li class="<!-- IF muted -->hide<!-- ENDIF muted -->">
<a component="account/mute" href="#">[[user:mute_account]]</a>
</li>
<li class="<!-- IF !muted -->hide<!-- ENDIF !muted -->">
<a component="account/unmute" href="#">[[user:unmute_account]]</a>
</li>
{{{ end }}}
<!-- IF isAdmin -->
<li>
<a component="account/delete-account" href="#" class="">[[user:delete_account_as_admin]]</a>
@ -55,18 +65,19 @@
<li role="separator" class="divider"></li>
<li><a href="{config.relative_path}/user/{userslug}/following">[[user:following]] <span class="badge badge-default pull-right formatted-number" title="{counts.following}">{counts.following}</span></a></li>
<li><a href="{config.relative_path}/user/{userslug}/followers">[[user:followers]] <span class="badge badge-default pull-right formatted-number" title="{counts.followers}">{counts.followers}</span></a></li>
<!-- IF showHidden -->
<!-- IF canEdit -->
<li><a href="{config.relative_path}/user/{userslug}/blocks">[[user:blocks]] <span class="badge badge-default pull-right formatted-number" title="{counts.blocks}">{counts.blocks}</span></a></li>
<!-- ENDIF showHidden -->
<!-- ENDIF canEdit -->
<li role="separator" class="divider"></li>
<li><a href="{config.relative_path}/user/{userslug}/topics">[[global:topics]] <span class="badge badge-default pull-right formatted-number" title="{counts.topics}">{counts.topics}</span></a></li>
<li><a href="{config.relative_path}/user/{userslug}/posts">[[global:posts]] <span class="badge badge-default pull-right formatted-number" title="{counts.posts}">{counts.posts}</span></a></li>
<!-- IF !reputation:disabled -->
<li><a href="{config.relative_path}/user/{userslug}/best">[[global:best]] <span class="badge badge-default pull-right formatted-number" title="{counts.best}">{counts.best}</span></a></li>
<li><a href="{config.relative_path}/user/{userslug}/controversial">[[global:controversial]] <span class="badge badge-default pull-right formatted-number" title="{counts.controversial}">{counts.controversial}</span></a></li>
<!-- ENDIF !reputation:disabled -->
<li><a href="{config.relative_path}/user/{userslug}/groups">[[global:header.groups]] <span class="badge badge-default pull-right formatted-number" title="{counts.groups}">{counts.groups}</span></a></li>
<!-- IF showHidden -->
<!-- IF canEdit -->
<li><a href="{config.relative_path}/user/{userslug}/categories">[[user:watched_categories]] <span class="badge badge-default pull-right formatted-number" title="{counts.categoriesWatched}">{counts.categoriesWatched}</span></a></li>
<li><a href="{config.relative_path}/user/{userslug}/bookmarks">[[user:bookmarks]] <span class="badge badge-default pull-right formatted-number" title="{counts.bookmarks}">{counts.bookmarks}</span></a></li>
<li><a href="{config.relative_path}/user/{userslug}/watched">[[user:watched]] <span class="badge badge-default pull-right formatted-number" title="{counts.watched}">{counts.watched}</span></a></li>
@ -78,7 +89,7 @@
<!-- ENDIF !downvote:disabled -->
<!-- ENDIF !reputation:disabled -->
<li><a href="{config.relative_path}/user/{userslug}/uploads">[[global:uploads]] <span class="badge badge-default pull-right formatted-number" title="{counts.uploaded}">{counts.uploaded}</span></a></li>
<!-- ENDIF showHidden -->
<!-- ENDIF canEdit -->
{{{each profile_links}}}
<!-- IF @first -->

@ -3,7 +3,7 @@
{{{each breadcrumbs}}}
<li<!-- IF @last --> component="breadcrumb/current"<!-- ENDIF @last --> itemscope="itemscope" itemprop="itemListElement" itemtype="http://schema.org/ListItem" <!-- IF @last -->class="active"<!-- ENDIF @last -->>
<meta itemprop="position" content="{@index}" />
<!-- IF !@last --><a href="{breadcrumbs.url}" itemprop="item"><!-- ENDIF !@last -->
{{{ if ./url }}}<a href="{breadcrumbs.url}" itemprop="item">{{{ end }}}
<span itemprop="name">
{breadcrumbs.text}
<!-- IF @last -->
@ -11,7 +11,7 @@
<!-- IF rssFeedUrl --><a target="_blank" href="{rssFeedUrl}" itemprop="item"><i class="fa fa-rss-square"></i></a><!-- ENDIF rssFeedUrl --><!-- ENDIF !feeds:disableRSS -->
<!-- ENDIF @last -->
</span>
<!-- IF !@last --></a><!-- ENDIF !@last -->
{{{ if ./url }}}</a>{{{ end }}}
</li>
{{{end}}}
</ol>

@ -6,11 +6,9 @@
<input type="text" class="form-control" autocomplete="off">
</div>
<ul component="category/list" class="dropdown-menu category-dropdown-menu" role="menu">
{{{ if allCategoriesUrl }}}
<li role="presentation" class="category" data-all="all">
<a role="menu-item" href="{config.relative_path}/{allCategoriesUrl}"><i component="category/select/icon" class="fa fa-fw fa-check {{{if selectedCategory}}}invisible{{{end}}}"></i> [[unread:all_categories]]</a>
</li>
{{{ end }}}
{{{each categoryItems}}}
<li role="presentation" class="category {{{ if ../disabledClass }}}disabled{{{ end }}}" data-cid="{../cid}" data-parent-cid="{../parentCid}" data-name="{../name}">
<a role="menu-item" href="#">{../level}<i component="category/select/icon" class="fa fa-fw fa-check {{{ if !../selected }}}invisible{{{ end }}}"></i><span component="category-markup" style="{{{ if ../match }}}font-weight: bold;{{{end}}}">{{{ if ../icon }}}<span class="fa-stack" style="{function.generateCategoryBackground}"><i class="fa fa-fw fa-stack-1x {../icon}" style="color: {../color};"></i></span>{{{ end }}} {../name}</span></a>

@ -1,6 +1,7 @@
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span component="category-selector-selected">{{{ if selectedCategory }}}<span class="fa-stack" style="{function.generateCategoryBackground, selectedCategory}"><i class="fa fa-fw fa-stack-1x {selectedCategory.icon}" style="color: {selectedCategory.color};"></i></span> {selectedCategory.name}{{{ else }}}
{{{ if selectCategoryLabel }}}{selectCategoryLabel}{{{ else }}}[[topic:thread_tools.select_category]]{{{ end }}}{{{ end }}}</span> <span class="caret"></span>
<span component="category-selector-selected">{{{ if (selectedCategory && !showCategorySelectLabel) }}}<span class="fa-stack" style="{function.generateCategoryBackground, selectedCategory}"><i class="fa fa-fw fa-stack-1x {selectedCategory.icon}" style="color: {selectedCategory.color};"></i></span> {selectedCategory.name}{{{ else }}}
<span class="visible-sm-inline visible-md-inline visible-lg-inline">{{{ if selectCategoryLabel }}}{selectCategoryLabel}{{{ else }}}[[topic:thread_tools.select_category]]{{{ end }}}</span><span class="visible-xs-inline"><i class="fa fa-fw {{{ if selectCategoryIcon }}}{selectCategoryIcon}{{{ else }}}fa-list{{{ end }}}"></i></span>
{{{ end }}}</span> <span class="caret"></span>
</button>
<div component="category-selector-search" class="hidden">
<input type="text" class="form-control" autocomplete="off">

@ -10,5 +10,6 @@
<li><a href="#" class="oldest_to_newest" data-sort="oldest_to_newest"><i class="fa fa-fw"></i> [[topic:oldest_to_newest]]</a></li>
<li><a href="#" class="most_posts" data-sort="most_posts"><i class="fa fa-fw"></i> [[topic:most_posts]]</a></li>
<li><a href="#" class="most_votes" data-sort="most_votes"><i class="fa fa-fw"></i> [[topic:most_votes]]</a></li>
<li><a href="#" class="most_views" data-sort="most_views"><i class="fa fa-fw"></i> [[topic:most_views]]</a></li>
</ul>
</div>

@ -1,11 +1,41 @@
<!-- IF config.loggedIn -->
<section class="menu-section" data-section="chats">
<h3 class="menu-section-title">
[[global:header.chats]]
<i class="counter unread-count" component="chat/icon" data-content="{unreadCount.chat}"></i>
</h3>
<ul class="menu-section-list chat-list" component="chat/list">
<a class="navigation-link" href="{relative_path}/user/{user.userslug}/chats">[[modules:chat.see_all]]</a>
</ul>
</section>
<!-- ENDIF config.loggedIn -->
{{{ if config.loggedIn }}}
<ul class="nav nav-pills">
<li>
<a href="#notifications" data-toggle="tab"><span class="counter unread-count" component="notifications/icon" data-content="{unreadCount.notification}"></span> <i class="fa fa-fw fa-bell"></i></a>
</li>
{{{ if !config.disableChat }}}
<li>
<a href="#chats" data-toggle="tab"><i class="counter unread-count" component="chat/icon" data-content="{unreadCount.chat}"></i> <i class="fa fa-fw fa-comment"></i></a>
</li>
{{{ end }}}
<li class="active">
<a href="#profile" data-toggle="tab">
{buildAvatar(user, "sm", true, "user-icon")}
<i component="user/status" class="fa fa-fw fa-circle status {user.status}"></i>
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade active in" id="profile">
<section class="menu-section" data-section="profile">
<ul class="menu-section-list" component="header/usercontrol"></ul>
</section>
</div>
<div class="tab-pane fade" id="notifications">
<section class="menu-section" data-section="notifications">
<ul class="menu-section-list notification-list-mobile" component="notifications/list"></ul>
<p class="menu-section-list"><a href="{relative_path}/notifications">[[notifications:see_all]]</a></p>
</section>
</div>
{{{ if !config.disableChat }}}
<div class="tab-pane fade" id="chats">
<section class="menu-section" data-section="chats">
<ul class="menu-section-list chat-list" component="chat/list">
<a class="navigation-link" href="{relative_path}/user/{user.userslug}/chats">[[modules:chat.see_all]]</a>
</ul>
</section>
</div>
{{{ end }}}
</div>
{{{ end }}}

@ -1,14 +1,6 @@
<!-- IF rooms.length -->
{{{each rooms}}}
<li class="<!-- IF ../unread -->unread<!-- ENDIF ../unread -->" data-roomid="{rooms.roomId}">
<strong class="room-name">
<!-- IF !rooms.lastUser.uid -->
<span>[[modules:chat.no-users-in-room]]</span>
<!-- ELSE -->
<!-- IF rooms.roomName -->{rooms.roomName}<!-- ELSE -->{rooms.usernames}<!-- ENDIF rooms.roomName -->
<!-- ENDIF !rooms.lastUser.uid -->
</strong>
<div class="avatar-placeholder"></div>
{{{each rooms.users}}}
<!-- IF @first -->
<div class="main-avatar">
@ -25,8 +17,20 @@
{{{end}}}
</ul>
<span class="teaser-content">{rooms.teaser.content}</span>
<span class="teaser-timestamp pull-right">{rooms.teaser.timeago}</span>
<div class="notification-chat-content">
<strong class="room-name">
<!-- IF !rooms.lastUser.uid -->
<span>[[modules:chat.no-users-in-room]]</span>
<!-- ELSE -->
<!-- IF rooms.roomName -->{rooms.roomName}<!-- ELSE -->{rooms.usernames}<!-- ENDIF rooms.roomName -->
<!-- ENDIF !rooms.lastUser.uid -->
</strong>
<span class="teaser-content">
<strong class="teaser-username">{rooms.teaser.user.username}:</strong>
{rooms.teaser.content}
</span>
</div>
<div class="teaser-timestamp notification-chat-controls">{rooms.teaser.timeago}</div>
</li>
{{{end}}}
<!-- ELSE -->

@ -1,33 +1,10 @@
<!-- IF roomId -->
<div component="chat/messages" class="expanded-chat" data-roomid="{roomId}">
<div component="chat/header">
<button type="button" class="close" data-action="pop-out"><span aria-hidden="true"><i class="fa fa-compress"></i></span><span class="sr-only">[[modules:chat.pop-out]]</span></button>
<button type="button" class="close" aria-label="Close" data-action="close"><span aria-hidden="true">&times;</span></button>
<button type="button" class="close" data-action="pop-out"><span aria-hidden="true"><i class="fa fa-compress"></i></span><span class="sr-only">[[modules:chat.pop-out]]</span></button>
<div class="dropdown pull-right">
<button class="close" data-toggle="dropdown" component="chat/controlsToggle"><i class="fa fa-gear"></i></button>
<ul class="dropdown-menu dropdown-menu-right pull-right" component="chat/controls">
<li class="dropdown-header">[[modules:chat.options]]</li>
<li>
<a href="#" data-action="members"><i class="fa fa-fw fa-cog"></i> [[modules:chat.manage-room]]</a>
</li>
<li>
<a href="#" data-action="rename"><i class="fa fa-fw fa-edit"></i> [[modules:chat.rename-room]]</a>
</li>
<li>
<a href="#" data-action="leave"><i class="fa fa-fw fa-sign-out"></i> [[modules:chat.leave]]</a>
</li>
<!-- IF users.length -->
<li role="separator" class="divider"></li>
<li class="dropdown-header">[[modules:chat.in-room]]</li>
{{{each users}}}
<li>
<a href="{config.relative_path}/uid/{../uid}">{buildAvatar(users, "sm", true)} {../username}</a>
</li>
{{{end}}}
<!-- END -->
</ul>
</div>
<!-- IMPORT partials/chats/options.tpl -->
<span class="members">
[[modules:chat.chatting_with]]:
{{{each users}}}
@ -41,8 +18,12 @@
</ul>
<div component="chat/composer">
<textarea component="chat/input" placeholder="[[modules:chat.placeholder]]" class="form-control chat-input mousetrap" rows="2"></textarea>
<button component="chat/upload/button" class="btn btn-light" type="button"><i class="fa fa-fw fa-2x fa-upload"></i></button>
<button class="btn btn-primary" type="button" data-action="send"><i class="fa fa-fw fa-2x fa-paper-plane"></i></button>
<span component="chat/message/remaining">{maximumChatMessageLength}</span>
<form component="chat/upload" method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple class="hidden"/>
</form>
</div>
</div>
<!-- ELSE -->

@ -0,0 +1,24 @@
<div class="dropdown pull-right">
<button class="close" data-toggle="dropdown" component="chat/controlsToggle"><i class="fa fa-gear"></i></button>
<ul class="dropdown-menu dropdown-menu-right pull-right" component="chat/controls">
<li class="dropdown-header">[[modules:chat.options]]</li>
<li>
<a href="#" data-action="members"><i class="fa fa-fw fa-cog"></i> [[modules:chat.manage-room]]</a>
</li>
<li>
<a href="#" data-action="rename"><i class="fa fa-fw fa-edit"></i> [[modules:chat.rename-room]]</a>
</li>
<li>
<a href="#" data-action="leave"><i class="fa fa-fw fa-sign-out"></i> [[modules:chat.leave]]</a>
</li>
<!-- IF users.length -->
<li role="separator" class="divider"></li>
<li class="dropdown-header">[[modules:chat.in-room]]</li>
{{{each users}}}
<li>
<a href="{config.relative_path}/uid/{../uid}">{buildAvatar(users, "sm", true)} {../username}</a>
</li>
{{{end}}}
<!-- END -->
</ul>
</div>

@ -1,12 +1,4 @@
<li component="chat/recent/room" data-roomid="{rooms.roomId}" class="<!-- IF rooms.unread -->unread<!-- ENDIF rooms.unread -->">
<strong class="room-name">
<!-- IF !rooms.lastUser.uid -->
<span>[[modules:chat.no-users-in-room]]</span>
<!-- ELSE -->
<span component="chat/title"><!-- IF rooms.roomName -->{rooms.roomName}<!-- ELSE -->{rooms.usernames}<!-- ENDIF rooms.roomName --></span>
<!-- ENDIF !rooms.lastUser.uid -->
</strong>
<div class="avatar-placeholder"></div>
{{{each rooms.users}}}
<!-- IF @first -->
<div class="main-avatar">
@ -22,4 +14,14 @@
</li>
{{{end}}}
</ul>
<div class="notification-chat-content">
<strong class="room-name">
<!-- IF !rooms.lastUser.uid -->
<span>[[modules:chat.no-users-in-room]]</span>
<!-- ELSE -->
<span component="chat/title"><!-- IF rooms.roomName -->{rooms.roomName}<!-- ELSE -->{rooms.usernames}<!-- ENDIF rooms.roomName --></span>
<!-- ENDIF !rooms.lastUser.uid -->
</strong>
</div>
</li>

@ -1,12 +1,28 @@
<div class="navbar-header">
<button type="button" class="navbar-toggle pull-left" id="mobile-menu">
<span component="notifications/icon" class="notification-icon fa fa-fw fa-bell-o unread-count" data-content="{unreadCount.notification}"></span>
<i class="fa fa-lg fa-fw fa-bars"></i>
<i class="fa fa-lg fa-fw fa-bars unread-count" data-content="{unreadCount.mobileUnread}" data-unread-url="{unreadCount.unreadUrl}"></i>
</button>
<button type="button" class="navbar-toggle hidden" id="mobile-chats">
{{{ if config.loggedIn }}}
<button type="button" class="navbar-toggle" id="mobile-chats">
<span component="notifications/icon" class="notification-icon fa fa-fw fa-bell-o unread-count" data-content="{unreadCount.notification}"></span>
<span component="chat/icon" class="notification-icon fa fa-fw fa-comments unread-count" data-content="{unreadCount.chat}"></span>
<i class="fa fa-lg fa-comment-o"></i>
{buildAvatar(user, "md", true)}
</button>
{{{ end }}}
{{{ if config.searchEnabled }}}
<div class="navbar-search visible-xs pull-right">
<form action="{config.relative_path}/search" method="GET">
<button type="button" class="btn btn-link"><i class="fa fa-lg fa-fw fa-search" title="[[global:header.search]]"></i></button>
<input autocomplete="off" type="text" class="form-control hidden" name="term" placeholder="[[global:search]]"/>
<button class="btn btn-primary hidden" type="submit"></button>
<input type="text" class="hidden" name="in" value="{config.searchDefaultInQuick}" />
</form>
<div class="quick-search-container hidden">
<div class="quick-search-results-container"></div>
</div>
</div>
{{{ end }}}
<!-- IF brand:logo -->
<a href="<!-- IF brand:logo:url -->{brand:logo:url}<!-- ELSE -->{relative_path}/<!-- ENDIF brand:logo:url -->">
@ -41,8 +57,12 @@
</li>
</ul>
</li>
<li class="notif-dropdown-link"><a href="#" class="mark-all-read">[[notifications:mark_all_read]]</a></li>
<li class="notif-dropdown-link"><a href="{relative_path}/notifications">[[notifications:see_all]]</a></li>
<li class="notif-dropdown-link">
<div class="btn-group btn-group-justified">
<a role="button" href="#" class="btn btn-secondary mark-all-read"><i class="fa fa-check-double"></i> [[notifications:mark_all_read]]</a>
<a class="btn btn-secondary" href="{relative_path}/notifications"><i class="fa fa-list"></i> [[notifications:see_all]]</a>
</div>
</li>
</ul>
</li>
@ -59,8 +79,12 @@
</li>
</ul>
</li>
<li class="notif-dropdown-link"><a href="#" class="mark-all-read" component="chats/mark-all-read">[[modules:chat.mark_all_read]]</a></li>
<li class="notif-dropdown-link"><a href="{relative_path}/user/{user.userslug}/chats">[[modules:chat.see_all]]</a></li>
<li class="notif-dropdown-link">
<div class="btn-group btn-group-justified">
<a class="btn btn-secondary mark-all-read" href="#" component="chats/mark-all-read"><i class="fa fa-check-double"></i> [[modules:chat.mark_all_read]]</a>
<a class="btn btn-secondary" href="{relative_path}/user/{user.userslug}/chats"><i class="fa fa-comments"></i> [[modules:chat.see_all]]</a>
</div>
</li>
</ul>
</li>
<!-- ENDIF canChat -->
@ -109,7 +133,7 @@
<i class="fa fa-fw fa-gear"></i> <span>[[user:settings]]</span>
</a>
</li>
<!-- IF showModMenu -->
{{{ if showModMenu }}}
<li role="presentation" class="divider"></li>
<li class="dropdown-header">[[pages:moderator-tools]]</li>
<li>
@ -127,7 +151,16 @@
<i class="fa fa-fw fa-ban"></i> <span>[[pages:ip-blacklist]]</span>
</a>
</li>
<!-- ENDIF showModMenu -->
{{{ else }}}
{{{ if postQueueEnabled }}}
<li>
<a href="{relative_path}/post-queue">
<i class="fa fa-fw fa-list-alt"></i> <span>[[pages:post-queue]]</span>
</a>
</li>
{{{ end }}}
{{{ end }}}
<li role="presentation" class="divider"></li>
<li component="user/logout">
<form method="post" action="{relative_path}/logout">
@ -202,16 +235,24 @@
<ul id="main-nav" class="nav navbar-nav">
{{{each navigation}}}
<!-- IF function.displayMenuItem, @index -->
<li class="{navigation.class}">
<a class="navigation-link" href="{navigation.route}" title="{navigation.title}" <!-- IF navigation.id -->id="{navigation.id}"<!-- ENDIF navigation.id --><!-- IF navigation.properties.targetBlank --> target="_blank"<!-- ENDIF navigation.properties.targetBlank -->>
<!-- IF navigation.iconClass -->
<li class="{navigation.class}{{{ if navigation.dropdown }}} dropdown{{{ end }}}">
<a title="{navigation.title}" class="navigation-link {{{ if navigation.dropdown }}}dropdown-toggle{{{ end }}}"
{{{ if navigation.dropdown }}} href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" {{{ else }}} href="{navigation.route}"{{{ end }}} {{{ if navigation.id }}}id="{navigation.id}"{{{ end }}}{{{ if navigation.targetBlank }}} target="_blank"{{{ end }}}>
{{{ if navigation.iconClass }}}
<i class="fa fa-fw {navigation.iconClass}" data-content="{navigation.content}"></i>
<!-- ENDIF navigation.iconClass -->
<!-- IF navigation.text -->
{{{ end }}}
{{{ if navigation.text }}}
<span class="{navigation.textClass}">{navigation.text}</span>
<!-- ENDIF navigation.text -->
{{{ end }}}
{{{ if navigation.dropdown}}}
<i class="fa fa-caret-down"></i>
{{{ end }}}
</a>
{{{ if navigation.dropdown }}}
<ul class="dropdown-menu">
{navigation.dropdownContent}
</ul>
{{{ end }}}
</li>
<!-- ENDIF function.displayMenuItem -->
{{{end}}}

@ -13,11 +13,13 @@
<a href="{config.relative_path}/user/{notifications.user.userslug}"><div class="pull-left avatar avatar-md avatar-rounded" style="background-color: {notifications.user.icon:bgColor};">{notifications.user.icon:text}</div></a>
<!-- ENDIF notifications.image -->
{{{ if ./nid }}}<div class="pull-right mark-read" aria-label="Mark Read"></div>{{{ end }}}
<a href="{notifications.path}">
<span class="pull-right relTime">{notifications.timeago}</span>
<a href="{notifications.path}" class="notification-chat-content deco-none">
<span class="text">{notifications.bodyShort}</span>
</a>
<div class="clear"></div>
<div class="notification-chat-controls">
{{{ if ./nid }}}<div class="mark-read" aria-label="Mark Read"></div>{{{ end }}}
<span class="relTime">{notifications.timeago}</span>
</div>
</li>
{{{end}}}

@ -10,8 +10,18 @@
<small class="topic-category"><a href="{config.relative_path}/category/{../category.slug}">[[global:posted_in, {../category.name}]]</a></small>
{{{ if ../isMainPost }}}
{{{ if ../topic.tags.length }}}
<span class="tag-list">
{{{ each ../topic.tags }}}
<a href="{config.relative_path}/tags/{topic.tags.valueEncoded}"><span class="tag tag-item tag-class-{topic.tags.class}">{topic.tags.valueEscaped}</span></a>
{{{ end }}}
</span>
{{{ end }}}
{{{ end }}}
<div class="post-info">
<a href="{config.relative_path}/user/{../user.userslug}">{buildAvatar(../user, "md", true, "user-img")}</a>
<a href="{config.relative_path}/user/{../user.userslug}">{buildAvatar(../user, "md", true, "user-img not-responsive")}</a>
<div class="post-author">
<a href="{config.relative_path}/user/{../user.userslug}">{../user.displayname}</a><br />

@ -1,7 +1,7 @@
<ul id="quick-search-results" class="quick-search-results">
{{{each posts}}}
<li data-tid="{posts.topic.tid}" data-pid="{posts.pid}">
<a href="{config.relative_path}/post/{posts.pid}">
<a href="{config.relative_path}/post/{posts.pid}" class="deco-none">
{buildAvatar(posts.user, "sm", true)}
<span class="quick-search-title">{posts.topic.title}</span>
<br/>

@ -0,0 +1,50 @@
<div id="results" class="search-results col-md-12" data-search-query="{search_query}">
<!-- IF matchCount -->
<div class="alert alert-info">[[search:results_matching, {matchCount}, {search_query}, {time}]] </div>
<!-- ELSE -->
<!-- IF search_query -->
<div class="alert alert-warning">[[search:no-matches]]</div>
<!-- ENDIF search_query -->
<!-- ENDIF matchCount -->
{{{each posts}}}
<div class="topic-row panel panel-default clearfix">
<div class="panel-body">
<a href="{config.relative_path}/user/{posts.user.userslug}">{buildAvatar(posts.user, "sm", true)}</a>
<span class="search-result-text search-result-title"><a href="{config.relative_path}/post/{posts.pid}">{posts.topic.title}</a></span>
<br/>
<!-- IF showAsPosts -->
<div class="search-result-text">
{posts.content}
<p class="fade-out"></p>
</div>
<!-- ENDIF showAsPosts -->
<small class="post-info pull-right">
<a href="{config.relative_path}/category/{posts.category.slug}"><span class="fa-stack" style="{function.generateCategoryBackground, posts.category}"><i style="color:{posts.category.color};" class="fa {posts.category.icon} fa-stack-1x"></i></span> {posts.category.name}</a> &bull;
<span class="timeago" title="{posts.timestampISO}"></span>
</small>
</div>
</div>
{{{end}}}
<!-- IF users.length -->
<ul id="users-container" class="users-container">
<!-- IMPORT partials/users_list.tpl -->
</ul>
<!-- ENDIF users.length -->
<!-- IF tags.length -->
<!-- IMPORT partials/tags_list.tpl -->
<!-- ENDIF tags.length -->
{{{ if categories.length }}}
<ul class="categories">
{{{each categories}}}
<!-- IMPORT partials/categories/item.tpl -->
{{{end}}}
</ul>
{{{ end }}}
<!-- IMPORT partials/paginator.tpl -->
</div>

@ -1,27 +1,4 @@
<div class="menu-profile">
<!-- IF user.uid -->
{buildAvatar(user, "lg", true, "user-icon")}
<i component="user/status" class="fa fa-fw fa-circle status {user.status}"></i>
<!-- ENDIF user.uid -->
</div>
<section class="menu-section" data-section="navigation">
<h3 class="menu-section-title">[[global:header.navigation]]</h3>
<ul class="menu-section-list"></ul>
</section>
<!-- IF config.loggedIn -->
<section class="menu-section" data-section="profile">
<h3 class="menu-section-title">[[global:header.profile]]</h3>
<ul class="menu-section-list" component="header/usercontrol"></ul>
</section>
<section class="menu-section" data-section="notifications">
<h3 class="menu-section-title">
[[global:header.notifications]]
<span class="counter unread-count" component="notifications/icon" data-content="{unreadCount.notification}"></span>
</h3>
<ul class="menu-section-list notification-list-mobile" component="notifications/list"></ul>
<p class="menu-section-list"><a href="{relative_path}/notifications">[[notifications:see_all]]</a></p>
</section>
<!-- ENDIF config.loggedIn -->

@ -1,5 +1,5 @@
{{{each tags}}}
<h3 class="pull-left tag-container">
<a href="{config.relative_path}/tags/{tags.valueEscaped}" data-value="{tags.valueEscaped}"><span class="tag-item" data-tag="{tags.valueEscaped}" style="<!-- IF tags.color -->color: {tags.color};<!-- ENDIF tags.color --><!-- IF tags.bgColor -->background-color: {tags.bgColor};<!-- ENDIF tags.bgColor -->">{tags.valueEscaped}</span><span class="tag-topic-count human-readable-number" title="{tags.score}">{tags.score}</span></a>
<a href="{config.relative_path}/tags/{tags.valueEncoded}" data-value="{tags.valueEscaped}"><span class="tag-item tag-class-{tags.class}" data-tag="{tags.valueEscaped}">{tags.valueEscaped}</span><span class="tag-topic-count human-readable-number" title="{tags.score}">{tags.score}</span></a>
</h3>
{{{end}}}

@ -1,13 +1 @@
<div component="topic/browsing-users" class="inline-block hidden-xs">
{{{each browsingUsers}}}
<div class="pull-left" data-uid="{browsingUsers.uid}">
<a href="<!-- IF browsingUsers.userslug -->{config.relative_path}/user/{browsingUsers.userslug}<!-- ELSE -->#<!-- ENDIF browsingUsers.userslug -->">
<!-- IF browsingUsers.picture -->
<img class="avatar avatar-sm avatar-rounded" component="user/picture" src="{browsingUsers.picture}" align="left" itemprop="image" title="{browsingUsers.username}"/>
<!-- ELSE -->
<div class="avatar avatar-sm avatar-rounded" component="user/picture" title="{browsingUsers.username}" style="background-color: {browsingUsers.icon:bgColor};">{browsingUsers.icon:text}</div>
<!-- ENDIF browsingUsers.picture -->
</a>
</div>
{{{end}}}
</div>
<!-- This partial intentionally left blank; overwritten by nodebb-plugin-browsing-users -->

@ -1,25 +0,0 @@
<li component="topic/event" class="timeline-event" data-topic-event-id="{id}">
<div class="timeline-badge">
<i class="fa {{{ if icon }}}{icon}{{{ else }}}fa-circle{{{ end }}}"></i>
</div>
<span class="timeline-text">
{{{ if ../href }}}
<a href="{config.relative_path}{../href}">{../text}</a>&nbsp;
{{{ else }}}
{text}&nbsp;
{{{ end }}}
</span>
{{{ if user }}}
{{{ if !./user.system }}}<span><a href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(user, "xs", true)}&nbsp;{./user.username}</a></span>&nbsp;{{{ end }}}
{{{ if ./user.system }}}<span class="timeline-text">[[global:system-user]]</span>&nbsp;{{{ end }}}
{{{ else }}}
<span class="timeline-text">[[global:unknown-user]]</span>&nbsp;
{{{ end }}}
<span class="timeago timeline-text" title="{timestampISO}"></span>
{{{ if isAdminOrMod}}}
&nbsp;<span component="topic/event/delete" data-topic-event-id="{id}" class="timeline-text pointer" title="[[topic:delete-event]]"><i class="fa fa-trash"></i></span>
{{{ end }}}
</li>

@ -25,8 +25,15 @@
</div>
</div>
</div>
<input type="text" class="form-control" id="indexInput" placeholder="[[global:pagination.enter_index]]">
<div class="row">
<div class="col-xs-6">
<button id="myNextPostBtn" class="btn btn-default form-control" disabled>[[topic:go-to-my-next-post]]</button>
</div>
<div class="col-xs-6">
<input type="number" class="form-control" id="indexInput" placeholder="[[global:pagination.enter_index]]">
</div>
</div>
</li>
</ul>
</div>
</div>
</div>

@ -72,7 +72,7 @@
</li>
<!-- END -->
<!-- IF config.loggedIn -->
{{{ if config.loggedIn }}}
<li>
<a component="post/bookmark" role="menuitem" tabindex="-1" href="#" data-bookmarked="{posts.bookmarked}">
<span class="menu-icon">
@ -83,7 +83,13 @@
<span component="post/bookmark-count" class="bookmarkCount badge" data-bookmarks="{posts.bookmarks}">{posts.bookmarks}</span>&nbsp;
</a>
</li>
<!-- ENDIF config.loggedIn -->
{{{ end }}}
<li>
<a role="menuitem" tabindex="-1" href="#" data-clipboard-text="{posts.absolute_url}">
<i class="fa fa-fw fa-link"></i> [[topic:copy-permalink]]
</a>
</li>
<!-- IF postSharing.length -->
<!-- IF config.loggedIn --><li class="divider"></li><!-- ENDIF config.loggedIn -->

@ -1,4 +1,4 @@
<span component="post/tools" class="dropdown moderator-tools bottom-sheet <!-- IF !posts.display_post_menu -->hidden<!-- ENDIF !posts.display_post_menu -->">
<a href="#" data-toggle="dropdown" data-ajaxify="false"><i class="fa fa-fw fa-ellipsis-v"></i></a>
<ul class="dropdown-menu dropdown-menu-right" role="menu"></ul>
<ul class="dropdown-menu dropdown-menu-right hidden" role="menu"></ul>
</span>

@ -1,3 +1,11 @@
{{{ if (!./index && widgets.mainpost-header.length) }}}
<div data-widget-area="mainpost-header">
{{{ each widgets.mainpost-header }}}
{widgets.mainpost-header.html}
{{{ end }}}
</div>
{{{ end }}}
<div class="clearfix post-header">
<div class="icon pull-left">
<a href="<!-- IF posts.user.userslug -->{config.relative_path}/user/{posts.user.userslug}<!-- ELSE -->#<!-- ENDIF posts.user.userslug -->">
@ -76,7 +84,8 @@
</a>
{{{ end }}}
<small class="pull-right">
<small class="pull-right" component="post/actions">
<!-- IMPORT partials/topic/reactions.tpl -->
<span class="post-tools">
<a component="post/reply" href="#" class="no-select <!-- IF !privileges.topics:reply -->hidden<!-- ENDIF !privileges.topics:reply -->">[[topic:reply]]</a>
<a component="post/quote" href="#" class="no-select <!-- IF !privileges.topics:reply -->hidden<!-- ENDIF !privileges.topics:reply -->">[[topic:quote]]</a>
@ -102,4 +111,11 @@
</small>
</div>
<div component="post/replies/container"></div>
</div>
</div>
{{{ if (!./index && widgets.mainpost-footer.length) }}}
<div data-widget-area="mainpost-footer">
{{{ each widgets.mainpost-footer }}}
{widgets.mainpost-footer.html}
{{{ end }}}
</div>
{{{ end }}}

@ -1,5 +1,5 @@
<!-- IF privileges.topics:reply -->
<div class="clearfix quick-reply">
<div component="topic/quickreply/container" class="clearfix quick-reply">
<div class="icon pull-left hidden-xs">
<a href="<!-- IF posts.user.userslug -->{config.relative_path}/user/{posts.user.userslug}<!-- ELSE -->#<!-- ENDIF posts.user.userslug -->">
{buildAvatar(loggedInUser, "46", true, "", "user/picture")}
@ -12,9 +12,17 @@
<input type="hidden" name="tid" value="{tid}" />
<input type="hidden" name="_csrf" value="{config.csrf_token}" />
<div class="quickreply-message">
<textarea name="content" component="topic/quickreply/text" class="form-control" rows="5"></textarea>
<textarea name="content" component="topic/quickreply/text" class="form-control mousetrap" rows="5" placeholder="[[modules:composer.textarea.placeholder]]"></textarea>
<div class="imagedrop"><div>[[topic:composer.drag_and_drop_images]]</div></div>
</div>
<button type="submit" component="topic/quickreply/button" class="btn btn-primary pull-right">[[persona:post-quick-reply]]</button>
<div class="btn-group pull-right">
<button type="submit" component="topic/quickreply/button" class="btn btn-primary">[[topic:post-quick-reply]]</button>
<button type="submit" component="topic/quickreply/expand" class="btn btn-default" formmethod="get"><i class="fa fa-expand"></i></button>
</div>
</form>
<form component="topic/quickreply/upload" method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple class="hidden"/>
</form>
</div>
<!-- ENDIF privileges.topics:reply -->

@ -0,0 +1 @@
<!-- This partial intentionally left blank; overwritten by nodebb-plugin-reactions -->

@ -0,0 +1,3 @@
<div component="selection/tooltip" class="selection-tooltip-container">
<button component="selection/tooltip/quote" class="btn btn-sm btn-primary quote-tooltip-btn">[[topic:quote]]</button>
</div>

@ -1,5 +1,5 @@
{{{each tags}}}
<a href="{config.relative_path}/tags/{tags.value}">
<span class="tag tag-item tag-{tags.valueEscaped}" data-tag="{tags.value}" style="<!-- IF tags.color -->color: {tags.color};<!-- ENDIF tags.color --><!-- IF tags.bgColor -->background-color: {tags.bgColor};<!-- ENDIF tags.bgColor -->">{tags.valueEscaped}</span>
<a href="{config.relative_path}/tags/{tags.valueEncoded}">
<span class="tag tag-item tag-class-{tags.class}" data-tag="{tags.value}">{tags.valueEscaped}</span>
</a>
{{{end}}}

@ -1,9 +1,11 @@
<ul component="category" class="topic-list" itemscope itemtype="http://www.schema.org/ItemList" data-nextstart="{nextStart}" data-set="{set}">
<meta itemprop="itemListOrder" content="descending">
{{{each topics}}}
<li component="category/topic" class="row clearfix category-item {function.generateTopicClass}" <!-- IMPORT partials/data/category.tpl -->>
<link itemprop="url" content="{config.relative_path}/topic/{../slug}" />
<meta itemprop="name" content="{function.stripTags, ../title}" />
<meta itemprop="itemListOrder" content="descending" />
<meta itemprop="position" content="{../index}" />
<a id="{../index}" data-index="{../index}" component="topic/anchor"></a>
<meta itemprop="name" content="{function.stripTags, title}">
<div class="col-md-6 col-sm-9 col-xs-10 content">
<div class="avatar pull-left">
@ -38,7 +40,7 @@
<!-- IF !topics.noAnchor -->
<a href="{config.relative_path}/topic/{topics.slug}<!-- IF topics.bookmark -->/{topics.bookmark}<!-- ENDIF topics.bookmark -->" itemprop="url">{topics.title}</a><br />
<a href="{config.relative_path}/topic/{topics.slug}<!-- IF topics.bookmark -->/{topics.bookmark}<!-- ENDIF topics.bookmark -->">{topics.title}</a><br />
<!-- ELSE -->
<span>{topics.title}</span><br />
<!-- ENDIF !topics.noAnchor -->
@ -49,14 +51,14 @@
</small>
<!-- ENDIF !template.category -->
<!-- IF topics.tags.length -->
{{{ if topics.tags.length }}}
<span class="tag-list hidden-xs">
{{{each topics.tags}}}
<a href="{config.relative_path}/tags/{topics.tags.valueEscaped}"><span class="tag tag-{topics.tags.valueEscaped}" style="<!-- IF topics.tags.color -->color: {topics.tags.color};<!-- ENDIF topics.tags.color --><!-- IF topics.tags.bgColor -->background-color: {topics.tags.bgColor};<!-- ENDIF topics.tags.bgColor -->">{topics.tags.valueEscaped}</span></a>
<a href="{config.relative_path}/tags/{topics.tags.valueEncoded}"><span class="tag tag-item tag-class-{topics.tags.class}">{topics.tags.valueEscaped}</span></a>
{{{end}}}
<small>&bull;</small>
</span>
<!-- ENDIF topics.tags.length -->
{{{ end }}}
<small class="hidden-xs"><span class="timeago" title="{topics.timestampISO}"></span> &bull; <a href="<!-- IF topics.user.userslug -->{config.relative_path}/user/{topics.user.userslug}<!-- ELSE -->#<!-- ENDIF topics.user.userslug -->">{topics.user.displayname}</a></small>
<small class="visible-xs-inline">

@ -9,28 +9,34 @@
</span>
<br/>
<!-- IF section_online -->
<div class="lastonline">
<span class="timeago" title="{users.lastonlineISO}"></span>
</div>
<!-- ENDIF section_online -->
<!-- IF section_joindate -->
<div title="joindate" class="joindate">
<div class="joindate">
<span class="timeago" title="{users.joindateISO}"></span>
</div>
<!-- ENDIF section_joindate -->
<!-- IF section_sort-reputation -->
<div title="reputation" class="reputation">
<div class="reputation">
<i class="fa fa-star"></i>
<span class="formatted-number">{users.reputation}</span>
</div>
<!-- ENDIF section_sort-reputation -->
<!-- IF section_sort-posts -->
<div title="post count" class="post-count">
<div class="post-count">
<i class="fa fa-pencil"></i>
<span class="formatted-number">{users.postcount}</span>
</div>
<!-- ENDIF section_sort-posts -->
<!-- IF section_flagged -->
<div title="flag count" class="flag-count">
<div class="flag-count">
<i class="fa fa-flag"></i>
<span><a class="formatted-number" href="{config.relative_path}/flags?targetUid={users.uid}">{users.flags}</a></span>
</div>

@ -1,20 +1,49 @@
<!-- IMPORT partials/breadcrumbs.tpl -->
{{{ if !singlePost }}}
<div class="btn-toolbar">
<!-- IMPORT partials/category-filter-right.tpl -->
<div class="btn-group pull-right bottom-sheet" component="post-queue/bulk-actions">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" autocomplete="off" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-clone"></i> [[post-queue:bulk-actions]] <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="#" data-action="accept-all">[[post-queue:accept-all]]</a></li>
<li><a href="#" data-action="accept-selected">[[post-queue:accept-selected]]</a></li>
<li class="divider"></li>
<li><a href="#" data-action="reject-all">[[post-queue:reject-all]]</a></li>
<li><a href="#" data-action="reject-selected">[[post-queue:reject-selected]]</a></li>
</ul>
</div>
</div>
<hr/>
{{{ end }}}
<div class="row">
<div class="col-xs-12">
<div class="post-queue preventSlideout posts-list">
{{{ if !posts.length }}}
<p class="panel-body">
[[post-queue:description, {config.relative_path}/admin/settings/post#post-queue]]
</p>
{{{ if (!posts.length && isAdmin) }}}
{{{ if !singlePost }}}
<div class="alert alert-info">
<p>[[post-queue:no-queued-posts]]</p>
{{{ if !enabled }}}<p>[[post-queue:enabling-help, {config.relative_path}/admin/settings/post#post-queue]]</p>{{{ end }}}
</div>
{{{ else }}}
<div class="alert alert-info">
<p>[[post-queue:no-single-post]]</p>
<p><a href=".">[[post-queue:back-to-list]]</a></p>
</div>
{{{ end }}}
{{{ end }}}
{{{ each posts }}}
<div class="panel panel-default" data-id="{posts.id}">
<div class="panel-heading">
{{{ if !singlePost }}}
<input type="checkbox" autocomplete="off" />
{{{ end }}}
<strong>{{{ if posts.data.tid }}}[[post-queue:reply]]{{{ else }}}[[post-queue:topic]]{{{ end }}}</strong>
<span class="timeago pull-right" title={posts.data.timestampISO}></span>
</div>
@ -62,9 +91,14 @@
</div>
</div>
<div class="panel-footer text-right">
<div class="btn-group">
<button class="btn btn-success btn-xs" data-action="accept"><i class="fa fa-check"></i> [[post-queue:accept]] </button>
<button class="btn btn-danger btn-xs" data-action="reject"><i class="fa fa-times"></i> [[post-queue:reject]]</button>
<div>
{{{ if canAccept }}}
<button class="btn btn-danger btn-xs" data-action="reject"><i class="fa fa-fw fa-times"></i> [[post-queue:reject]]</button>
<button class="btn btn-info btn-xs" data-action="notify"><i class="fa fa-fw fa-bell-o"></i> [[post-queue:notify]]</button>
<button class="btn btn-success btn-xs" data-action="accept"><i class="fa fa-fw fa-check"></i> [[post-queue:accept]] </button>
{{{ else }}}
<button class="btn btn-danger btn-xs" data-action="reject"><i class="fa fa-fw fa-times"></i> [[post-queue:remove]]</button>
{{{ end }}}
</div>
</div>
</div>

@ -1,71 +1,87 @@
<!-- IMPORT partials/breadcrumbs.tpl -->
<div data-widget-area="header">
{{{each widgets.header}}}
{{widgets.header.html}}
{{{end}}}
</div>
<div class="row register">
<div class="{register_window:spansize}">
<div class="register-block">
<div class="alert alert-danger<!-- IF !error --> hidden<!-- ENDIF !error -->" id="register-error-notify" >
<strong>[[error:registration-error]]</strong>
<p>{error}</p>
</div>
<form component="register/local" class="form-horizontal" role="form" action="{config.relative_path}/register" method="post">
<div class="form-group">
<label for="username" class="col-lg-4 control-label">[[register:username]]</label>
<div class="col-lg-8">
<input class="form-control" type="text" placeholder="[[register:username_placeholder]]" name="username" id="username" autocorrect="off" autocapitalize="off" autocomplete="off" />
<span class="register-feedback" id="username-notify"></span>
<span class="help-block">[[register:help.username_restrictions, {minimumUsernameLength}, {maximumUsernameLength}]]</span>
</div>
<div class="row {{{ if widgets.sidebar.length }}}col-lg-9 col-sm-12{{{ else }}}col-lg-12{{{ end }}}">
<div class="{register_window:spansize}">
<div class="register-block">
<div class="alert alert-danger<!-- IF !error --> hidden<!-- ENDIF !error -->" id="register-error-notify" >
<strong>[[error:registration-error]]</strong>
<p>{error}</p>
</div>
<div class="form-group">
<label for="password" class="col-lg-4 control-label">[[register:password]]</label>
<div class="col-lg-8">
<input class="form-control" type="password" placeholder="[[register:password_placeholder]]" name="password" id="password" />
<span class="register-feedback" id="password-notify"></span>
<span class="help-block">[[register:help.minimum_password_length, {minimumPasswordLength}]]</span>
<p id="caps-lock-warning" class="text-danger hidden">
<i class="fa fa-exclamation-triangle"></i> [[login:caps-lock-enabled]]
</p>
<form component="register/local" class="form-horizontal" role="form" action="{config.relative_path}/register" method="post">
<div class="form-group">
<label for="username" class="col-lg-4 control-label">[[register:username]]</label>
<div class="col-lg-8">
<input class="form-control" type="text" placeholder="[[register:username_placeholder]]" name="username" id="username" autocorrect="off" autocapitalize="off" autocomplete="off" />
<span class="register-feedback" id="username-notify"></span>
<span class="help-block">[[register:help.username_restrictions, {minimumUsernameLength}, {maximumUsernameLength}]]</span>
</div>
</div>
</div>
<div class="form-group">
<label for="password-confirm" class="col-lg-4 control-label">[[register:confirm_password]]</label>
<div class="col-lg-8">
<input class="form-control" type="password" placeholder="[[register:confirm_password_placeholder]]" name="password-confirm" id="password-confirm" />
<span class="register-feedback" id="password-confirm-notify"></span>
<div class="form-group">
<label for="password" class="col-lg-4 control-label">[[register:password]]</label>
<div class="col-lg-8">
<input class="form-control" type="password" placeholder="[[register:password_placeholder]]" name="password" id="password" />
<span class="register-feedback" id="password-notify"></span>
<span class="help-block">[[register:help.minimum_password_length, {minimumPasswordLength}]]</span>
<p id="caps-lock-warning" class="text-danger hidden">
<i class="fa fa-exclamation-triangle"></i> [[login:caps-lock-enabled]]
</p>
</div>
</div>
<div class="form-group">
<label for="password-confirm" class="col-lg-4 control-label">[[register:confirm_password]]</label>
<div class="col-lg-8">
<input class="form-control" type="password" placeholder="[[register:confirm_password_placeholder]]" name="password-confirm" id="password-confirm" />
<span class="register-feedback" id="password-confirm-notify"></span>
</div>
</div>
</div>
{{{each regFormEntry}}}
<div class="form-group">
<label for="register-{regFormEntry.styleName}" class="col-lg-4 control-label">{regFormEntry.label}</label>
<div id="register-{regFormEntry.styleName}" class="col-lg-8">
{{regFormEntry.html}}
{{{each regFormEntry}}}
<div class="form-group">
<label for="register-{regFormEntry.styleName}" class="col-lg-4 control-label">{regFormEntry.label}</label>
<div id="register-{regFormEntry.styleName}" class="col-lg-8">
{{regFormEntry.html}}
</div>
</div>
</div>
{{{end}}}
{{{end}}}
<div class="form-group">
<div class="col-lg-offset-4 col-lg-8">
<button class="btn btn-primary btn-lg btn-block" id="register" type="submit">[[register:register_now_button]]</button>
<div class="form-group">
<div class="col-lg-offset-4 col-lg-8">
<button class="btn btn-primary btn-lg btn-block" id="register" type="submit">[[register:register_now_button]]</button>
</div>
</div>
</div>
<input id="token" type="hidden" name="token" value="" />
<input id="noscript" type="hidden" name="noscript" value="true" />
<input type="hidden" name="_csrf" value="{config.csrf_token}" />
</form>
<input id="token" type="hidden" name="token" value="" />
<input id="noscript" type="hidden" name="noscript" value="true" />
<input type="hidden" name="_csrf" value="{config.csrf_token}" />
</form>
</div>
</div>
</div>
<!-- IF alternate_logins -->
<div class="col-md-6">
<div class="alt-register-block">
<h4>[[register:alternative_registration]]</h4>
<ul class="alt-logins">
{{{each authentication}}}
<li class="{authentication.name}"><a rel="nofollow noopener noreferrer" target="_top" href="{config.relative_path}{authentication.url}"><i class="fa {authentication.icon} fa-3x"></i></i></a></li>
{{{end}}}
</ul>
{{{ if alternate_logins }}}
<div class="col-md-6">
<div class="alt-register-block">
<h4>[[register:alternative_registration]]</h4>
<ul class="alt-logins">
{{{each authentication}}}
<li class="{authentication.name}"><a rel="nofollow noopener noreferrer" target="_top" href="{config.relative_path}{authentication.url}"><i class="fa {authentication.icon} fa-3x"></i></i></a></li>
{{{end}}}
</ul>
</div>
</div>
{{{ end }}}
</div>
<div data-widget-area="sidebar" class="col-lg-3 col-sm-12 {{{ if !widgets.sidebar.length }}}hidden{{{ end }}}">
{{{each widgets.sidebar}}}
{{widgets.sidebar.html}}
{{{end}}}
</div>
<!-- ENDIF alternate_logins -->
</div>
<div data-widget-area="footer">
{{{each widgets.footer}}}
{{widgets.footer.html}}
{{{end}}}
</div>

@ -36,12 +36,10 @@
<div class="row">
<div class="col-xs-12 col-sm-8 col-sm-offset-2">
<button class="btn btn-primary btn-block">[[topic:composer.submit]]</button>
<div class="btn-group btn-block">
<button class="btn btn-block btn-primary">[[topic:composer.submit]]</button>
<button class="btn btn-block btn-link" formaction="{config.relative_path}/register/abort?_csrf={config.csrf_token}">{{{ if register }}}[[register:cancel_registration]]{{{ else }}}[[modules:bootbox.cancel]]{{{ end }}}</button>
</div>
</div>
</div>
</form>
<form role="form" method="post" action="{config.relative_path}/register/abort">
<p class="text-center">
<button class="btn btn-link">{{{ if register }}}[[register:cancel_registration]]{{{ else }}}[[modules:bootbox.cancel]]{{{ end }}}</button>
</p>
</form>

@ -168,55 +168,6 @@
</div>
<div class="row">
<div id="results" class="search-results col-md-12" data-search-query="{search_query}">
<!-- IF matchCount -->
<div class="alert alert-info">[[search:results_matching, {matchCount}, {search_query}, {time}]] </div>
<!-- ELSE -->
<!-- IF search_query -->
<div class="alert alert-warning">[[search:no-matches]]</div>
<!-- ENDIF search_query -->
<!-- ENDIF matchCount -->
{{{each posts}}}
<div class="topic-row panel panel-default clearfix">
<div class="panel-body">
<a href="{config.relative_path}/user/{posts.user.userslug}">{buildAvatar(posts.user, "sm", true)}</a>
<span class="search-result-text search-result-title"><a href="{config.relative_path}/post/{posts.pid}">{posts.topic.title}</a></span>
<br/>
<!-- IF showAsPosts -->
<div class="search-result-text">
{posts.content}
<p class="fade-out"></p>
</div>
<!-- ENDIF showAsPosts -->
<small class="post-info pull-right">
<a href="{config.relative_path}/category/{posts.category.slug}"><span class="fa-stack" style="{function.generateCategoryBackground, posts.category}"><i style="color:{posts.category.color};" class="fa {posts.category.icon} fa-stack-1x"></i></span> {posts.category.name}</a> &bull;
<span class="timeago" title="{posts.timestampISO}"></span>
</small>
</div>
</div>
{{{end}}}
<!-- IF users.length -->
<ul id="users-container" class="users-container">
<!-- IMPORT partials/users_list.tpl -->
</ul>
<!-- ENDIF users.length -->
<!-- IF tags.length -->
<!-- IMPORT partials/tags_list.tpl -->
<!-- ENDIF tags.length -->
{{{ if categories.length }}}
<ul class="categories">
{{{each categories}}}
<!-- IMPORT partials/categories/item.tpl -->
{{{end}}}
</ul>
{{{ end }}}
<!-- IMPORT partials/paginator.tpl -->
</div>
<!-- IMPORT partials/search-results.tpl -->
</div>
</div>

@ -7,7 +7,7 @@
<div class="topic <!-- IF widgets.sidebar.length -->col-lg-9 col-sm-12<!-- ELSE -->col-lg-12<!-- ENDIF widgets.sidebar.length -->">
<div class="topic-header">
<h1 component="post/header" class="" itemprop="name">
<span class="topic-title" component="topic/title">
<span class="topic-title">
<span component="topic/labels">
<i component="topic/scheduled" class="fa fa-clock-o <!-- IF !scheduled -->hidden<!-- ENDIF !scheduled -->" title="[[topic:scheduled]]"></i>
<i component="topic/pinned" class="fa fa-thumb-tack <!-- IF (scheduled || !pinned) -->hidden<!-- ENDIF (scheduled || !pinned) -->" title="{{{ if !pinExpiry }}}[[topic:pinned]]{{{ else }}}[[topic:pinned-with-expiry, {pinExpiryISO}]]{{{ end }}}"></i>
@ -15,7 +15,7 @@
<i class="fa fa-arrow-circle-right <!-- IF !oldCid -->hidden<!-- ENDIF !oldCid -->" title="{{{ if privileges.isAdminOrMod }}}[[topic:moved-from, {oldCategory.name}]]{{{ else }}}[[topic:moved]]{{{ end }}}"></i>
{{{each icons}}}{@value}{{{end}}}
</span>
{title}
<span component="topic/title">{title}</span>
</span>
</h1>
@ -36,7 +36,11 @@
{{{ if !feeds:disableRSS }}}
{{{ if rssFeedUrl }}}<a class="hidden-xs" target="_blank" href="{rssFeedUrl}"><i class="fa fa-rss-square"></i></a>{{{ end }}}
{{{ end }}}
{{{ if browsingUsers }}}
<div class="inline-block hidden-xs">
<!-- IMPORT partials/topic/browsing-users.tpl -->
</div>
{{{ end }}}
<!-- IMPORT partials/post_bar.tpl -->
</div>
@ -67,10 +71,17 @@
<!-- IMPORT partials/topic/post.tpl -->
</li>
{renderTopicEvents(@index)}
{renderTopicEvents(@index, config.topicPostSort)}
{{{end}}}
</ul>
{{{ if browsingUsers }}}
<div class="visible-xs">
<!-- IMPORT partials/topic/browsing-users.tpl -->
<hr/>
</div>
{{{ end }}}
<!-- IF config.enableQuickReply -->
<!-- IMPORT partials/topic/quickreply.tpl -->
<!-- ENDIF config.enableQuickReply -->

@ -1,6 +1,14 @@
{{{ if !error }}}
<div class="alert alert-success">
<strong>[[global:alert.success]]</strong>
<p>[[email:unsub.success, {payload.template}]]</p>
{{{ else }}}
<div class="alert alert-warning">
<strong>[[email:unsub.failure.title]]</strong>
<p>[[email:unsub.failure.message, {error}, {config.relative_path}/me/settings]]</p>
{{{ end }}}
<hr />
<p>
<a href="{config.relative_path}/">[[notifications:back_to_home, {config.siteTitle}]]</a>
</p>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save