|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
|
|
app = window.app || {};
|
|
|
|
|
|
|
|
app.isFocused = true;
|
|
|
|
app.currentRoom = null;
|
|
|
|
app.widgets = {};
|
|
|
|
app.flags = {};
|
|
|
|
app.cacheBuster = null;
|
|
|
|
|
|
|
|
(function () {
|
|
|
|
let appLoaded = false;
|
|
|
|
const isTouchDevice = utils.isTouchDevice();
|
|
|
|
|
|
|
|
app.cacheBuster = config['cache-buster'];
|
|
|
|
|
|
|
|
let hooks;
|
|
|
|
require(['hooks'], function (_hooks) {
|
|
|
|
hooks = _hooks;
|
|
|
|
});
|
|
|
|
|
|
|
|
$(document).ready(function () {
|
|
|
|
ajaxify.parseData();
|
|
|
|
app.load();
|
|
|
|
});
|
|
|
|
|
|
|
|
app.coldLoad = function () {
|
|
|
|
if (appLoaded) {
|
|
|
|
ajaxify.coldLoad();
|
|
|
|
} else {
|
|
|
|
$(window).on('action:app.load', function () {
|
|
|
|
ajaxify.coldLoad();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
app.handleEarlyClicks = function () {
|
|
|
|
/**
|
|
|
|
* Occasionally, a button or anchor (not meant to be ajaxified) is clicked before
|
|
|
|
* ajaxify is ready. Capture that event and re-click it once NodeBB is ready.
|
|
|
|
*
|
|
|
|
* e.g. New Topic/Reply, post tools
|
|
|
|
*/
|
|
|
|
if (document.body) {
|
|
|
|
let earlyQueue = []; // once we can ES6, use Set instead
|
|
|
|
const earlyClick = function (ev) {
|
|
|
|
let btnEl = ev.target.closest('button');
|
|
|
|
const anchorEl = ev.target.closest('a');
|
|
|
|
if (!btnEl && anchorEl && (anchorEl.getAttribute('data-ajaxify') === 'false' || anchorEl.href === '#')) {
|
|
|
|
btnEl = anchorEl;
|
|
|
|
}
|
|
|
|
if (btnEl && !earlyQueue.includes(btnEl)) {
|
|
|
|
earlyQueue.push(btnEl);
|
|
|
|
ev.stopImmediatePropagation();
|
|
|
|
ev.preventDefault();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
document.body.addEventListener('click', earlyClick);
|
|
|
|
require(['hooks'], function (hooks) {
|
|
|
|
hooks.on('action:ajaxify.end', function () {
|
|
|
|
document.body.removeEventListener('click', earlyClick);
|
|
|
|
earlyQueue.forEach(function (el) {
|
|
|
|
el.click();
|
|
|
|
});
|
|
|
|
earlyQueue = [];
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setTimeout(app.handleEarlyClicks, 50);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
app.handleEarlyClicks();
|
|
|
|
|
|
|
|
app.load = function () {
|
|
|
|
handleStatusChange();
|
|
|
|
|
|
|
|
$('body').on('click', '#new_topic', function (e) {
|
|
|
|
e.preventDefault();
|
|
|
|
app.newTopic();
|
|
|
|
});
|
|
|
|
|
|
|
|
$('#header-menu .container').on('click', '[component="user/logout"]', function () {
|
|
|
|
app.logout();
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
Visibility.change(function (event, state) {
|
|
|
|
app.isFocused = state === 'visible';
|
|
|
|
});
|
|
|
|
|
|
|
|
createHeaderTooltips();
|
|
|
|
|
|
|
|
registerServiceWorker();
|
|
|
|
|
|
|
|
require([
|
|
|
|
'taskbar',
|
|
|
|
'helpers',
|
|
|
|
'forum/pagination',
|
|
|
|
'translator',
|
|
|
|
'messages',
|
|
|
|
'search',
|
|
|
|
'forum/unread',
|
|
|
|
'forum/header/notifications',
|
|
|
|
'forum/header/chat',
|
|
|
|
'timeago/jquery.timeago',
|
|
|
|
], function (taskbar, helpers, pagination, translator, messages, search, unread, notifications, chat) {
|
|
|
|
notifications.prepareDOM();
|
|
|
|
chat.prepareDOM();
|
|
|
|
translator.prepareDOM();
|
|
|
|
taskbar.init();
|
|
|
|
helpers.register();
|
|
|
|
pagination.init();
|
|
|
|
search.init();
|
|
|
|
|
|
|
|
if (app.user.uid > 0) {
|
|
|
|
unread.initUnreadTopics();
|
|
|
|
}
|
|
|
|
function finishLoad() {
|
|
|
|
hooks.fire('action:app.load');
|
|
|
|
messages.show();
|
|
|
|
appLoaded = true;
|
|
|
|
}
|
|
|
|
overrides.overrideTimeago();
|
|
|
|
if (app.user.timeagoCode && app.user.timeagoCode !== 'en') {
|
|
|
|
require(['timeago/locales/jquery.timeago.' + app.user.timeagoCode], finishLoad);
|
|
|
|
} else {
|
|
|
|
finishLoad();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.require = async (modules) => { // allows you to await require.js modules
|
|
|
|
let single = false;
|
|
|
|
if (!Array.isArray(modules)) {
|
|
|
|
modules = [modules];
|
|
|
|
single = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
require(modules, (...exports) => {
|
|
|
|
resolve(single ? exports.pop() : exports);
|
|
|
|
}, reject);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.logout = function (redirect) {
|
|
|
|
redirect = redirect === undefined ? true : redirect;
|
|
|
|
hooks.fire('action:app.logout');
|
|
|
|
|
|
|
|
$.ajax(config.relative_path + '/logout', {
|
|
|
|
type: 'POST',
|
|
|
|
headers: {
|
|
|
|
'x-csrf-token': config.csrf_token,
|
|
|
|
},
|
|
|
|
beforeSend: function () {
|
|
|
|
app.flags._logout = true;
|
|
|
|
},
|
|
|
|
success: function (data) {
|
|
|
|
hooks.fire('action:app.loggedOut', data);
|
|
|
|
if (redirect) {
|
|
|
|
if (data.next) {
|
|
|
|
window.location.href = data.next;
|
|
|
|
} else {
|
|
|
|
window.location.reload();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
app.alert = function (params) {
|
|
|
|
require(['alerts'], function (alerts) {
|
|
|
|
alerts.alert(params);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.removeAlert = function (id) {
|
|
|
|
require(['alerts'], function (alerts) {
|
|
|
|
alerts.remove(id);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.alertSuccess = function (message, timeout) {
|
|
|
|
app.alert({
|
|
|
|
alert_id: utils.generateUUID(),
|
|
|
|
title: '[[global:alert.success]]',
|
|
|
|
message: message,
|
|
|
|
type: 'success',
|
|
|
|
timeout: timeout || 5000,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.alertError = function (message, timeout) {
|
|
|
|
message = (message && message.message) || message;
|
|
|
|
|
|
|
|
if (message === '[[error:revalidate-failure]]') {
|
|
|
|
socket.disconnect();
|
|
|
|
app.reconnect();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
app.alert({
|
|
|
|
alert_id: utils.generateUUID(),
|
|
|
|
title: '[[global:alert.error]]',
|
|
|
|
message: message,
|
|
|
|
type: 'danger',
|
|
|
|
timeout: timeout || 10000,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.enterRoom = function (room, callback) {
|
|
|
|
callback = callback || function () { };
|
|
|
|
if (socket && app.user.uid && app.currentRoom !== room) {
|
|
|
|
const previousRoom = app.currentRoom;
|
|
|
|
app.currentRoom = room;
|
|
|
|
socket.emit('meta.rooms.enter', {
|
|
|
|
enter: room,
|
|
|
|
}, function (err) {
|
|
|
|
if (err) {
|
|
|
|
app.currentRoom = previousRoom;
|
|
|
|
return app.alertError(err.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
app.leaveCurrentRoom = function () {
|
|
|
|
if (!socket || config.maintenanceMode) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const previousRoom = app.currentRoom;
|
|
|
|
app.currentRoom = '';
|
|
|
|
socket.emit('meta.rooms.leaveCurrent', function (err) {
|
|
|
|
if (err) {
|
|
|
|
app.currentRoom = previousRoom;
|
|
|
|
return app.alertError(err.message);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
function highlightNavigationLink() {
|
|
|
|
$('#main-nav li')
|
|
|
|
.removeClass('active')
|
|
|
|
.find('a')
|
|
|
|
.filter(function (i, a) {
|
|
|
|
return $(a).attr('href') !== '#' && window.location.hostname === a.hostname &&
|
|
|
|
(
|
|
|
|
window.location.pathname === a.pathname ||
|
|
|
|
window.location.pathname.startsWith(a.pathname + '/')
|
|
|
|
);
|
|
|
|
})
|
|
|
|
.parent()
|
|
|
|
.addClass('active');
|
|
|
|
}
|
|
|
|
|
|
|
|
app.createUserTooltips = function (els, placement) {
|
|
|
|
if (isTouchDevice) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
els = els || $('body');
|
|
|
|
els.find('.avatar,img[title].teaser-pic,img[title].user-img,div.user-icon,span.user-icon').each(function () {
|
|
|
|
$(this).tooltip({
|
|
|
|
placement: placement || $(this).attr('title-placement') || 'top',
|
|
|
|
title: $(this).attr('title'),
|
|
|
|
container: '#content',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.createStatusTooltips = function () {
|
|
|
|
if (!isTouchDevice) {
|
|
|
|
$('body').tooltip({
|
|
|
|
selector: '.fa-circle.status',
|
|
|
|
placement: 'top',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
app.processPage = function () {
|
|
|
|
highlightNavigationLink();
|
|
|
|
|
|
|
|
$('.timeago').timeago();
|
|
|
|
|
|
|
|
utils.makeNumbersHumanReadable($('.human-readable-number'));
|
|
|
|
|
|
|
|
utils.addCommasToNumbers($('.formatted-number'));
|
|
|
|
|
|
|
|
app.createUserTooltips($('#content'));
|
|
|
|
|
|
|
|
app.createStatusTooltips();
|
|
|
|
};
|
|
|
|
|
|
|
|
app.openChat = function (roomId, uid) {
|
|
|
|
if (!app.user.uid) {
|
|
|
|
return app.alertError('[[error:not-logged-in]]');
|
|
|
|
}
|
|
|
|
|
|
|
|
require(['chat'], function (chat) {
|
|
|
|
function loadAndCenter(chatModal) {
|
|
|
|
chat.load(chatModal.attr('data-uuid'));
|
|
|
|
chat.center(chatModal);
|
|
|
|
chat.focusInput(chatModal);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (chat.modalExists(roomId)) {
|
|
|
|
loadAndCenter(chat.getModal(roomId));
|
|
|
|
} else {
|
|
|
|
socket.emit('modules.chats.loadRoom', { roomId: roomId, uid: uid || app.user.uid }, function (err, roomData) {
|
|
|
|
if (err) {
|
|
|
|
return app.alertError(err.message);
|
|
|
|
}
|
|
|
|
roomData.users = roomData.users.filter(function (user) {
|
|
|
|
return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10);
|
|
|
|
});
|
|
|
|
roomData.uid = uid || app.user.uid;
|
|
|
|
roomData.isSelf = true;
|
|
|
|
chat.createModal(roomData, loadAndCenter);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.newChat = function (touid, callback) {
|
|
|
|
function createChat() {
|
|
|
|
socket.emit('modules.chats.newRoom', { touid: touid }, function (err, roomId) {
|
|
|
|
if (err) {
|
|
|
|
return app.alertError(err.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!ajaxify.data.template.chats) {
|
|
|
|
app.openChat(roomId);
|
|
|
|
} else {
|
|
|
|
ajaxify.go('chats/' + roomId);
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(false, roomId);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
callback = callback || function () { };
|
|
|
|
if (!app.user.uid) {
|
|
|
|
return app.alertError('[[error:not-logged-in]]');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parseInt(touid, 10) === parseInt(app.user.uid, 10)) {
|
|
|
|
return app.alertError('[[error:cant-chat-with-yourself]]');
|
|
|
|
}
|
|
|
|
socket.emit('modules.chats.isDnD', touid, function (err, isDnD) {
|
|
|
|
if (err) {
|
|
|
|
return app.alertError(err.message);
|
|
|
|
}
|
|
|
|
if (!isDnD) {
|
|
|
|
return createChat();
|
|
|
|
}
|
|
|
|
require(['bootbox'], function (bootbox) {
|
|
|
|
bootbox.confirm('[[modules:chat.confirm-chat-with-dnd-user]]', function (ok) {
|
|
|
|
if (ok) {
|
|
|
|
createChat();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.toggleNavbar = function (state) {
|
|
|
|
require(['components'], (components) => {
|
|
|
|
const navbarEl = components.get('navbar');
|
|
|
|
navbarEl[state ? 'show' : 'hide']();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
function createHeaderTooltips() {
|
|
|
|
const env = utils.findBootstrapEnvironment();
|
|
|
|
if (env === 'xs' || env === 'sm' || isTouchDevice) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$('#header-menu li a[title]').each(function () {
|
|
|
|
$(this).tooltip({
|
|
|
|
placement: 'bottom',
|
|
|
|
trigger: 'hover',
|
|
|
|
title: $(this).attr('title'),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$('#search-form').tooltip({
|
|
|
|
placement: 'bottom',
|
|
|
|
trigger: 'hover',
|
|
|
|
title: $('#search-button i').attr('title'),
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$('#user_dropdown').tooltip({
|
|
|
|
placement: 'bottom',
|
|
|
|
trigger: 'hover',
|
|
|
|
title: $('#user_dropdown').attr('title'),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
app.enableTopicSearch = function (options) {
|
|
|
|
console.warn('[deprecated] app.enableTopicSearch is deprecated, please use search.enableQuickSearch(options)');
|
|
|
|
require(['search'], function (search) {
|
|
|
|
search.enableQuickSearch(options);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.handleSearch = function (searchOptions) {
|
|
|
|
console.warn('[deprecated] app.handleSearch is deprecated, please use search.init(options)');
|
|
|
|
require(['search'], function (search) {
|
|
|
|
search.init(searchOptions);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.prepareSearch = function () {
|
|
|
|
console.warn('[deprecated] app.prepareSearch is deprecated, please use search.showAndFocusInput()');
|
|
|
|
require(['search'], function (search) {
|
|
|
|
search.showAndFocusInput();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
function handleStatusChange() {
|
|
|
|
$('[component="header/usercontrol"] [data-status]').off('click').on('click', function (e) {
|
|
|
|
const status = $(this).attr('data-status');
|
|
|
|
socket.emit('user.setStatus', status, function (err) {
|
|
|
|
if (err) {
|
|
|
|
return app.alertError(err.message);
|
|
|
|
}
|
|
|
|
$('[data-uid="' + app.user.uid + '"] [component="user/status"], [component="header/profilelink"] [component="user/status"]')
|
|
|
|
.removeClass('away online dnd offline')
|
|
|
|
.addClass(status);
|
|
|
|
$('[component="header/usercontrol"] [data-status]').each(function () {
|
|
|
|
$(this).find('span').toggleClass('bold', $(this).attr('data-status') === status);
|
|
|
|
});
|
|
|
|
app.user.status = status;
|
|
|
|
});
|
|
|
|
e.preventDefault();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
app.updateUserStatus = function (el, status) {
|
|
|
|
if (!el.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
require(['translator'], function (translator) {
|
|
|
|
translator.translate('[[global:' + status + ']]', function (translated) {
|
|
|
|
el.removeClass('online offline dnd away')
|
|
|
|
.addClass(status)
|
|
|
|
.attr('title', translated)
|
|
|
|
.attr('data-original-title', translated);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.newTopic = function (cid, tags) {
|
|
|
|
hooks.fire('action:composer.topic.new', {
|
|
|
|
cid: cid || ajaxify.data.cid || 0,
|
|
|
|
tags: tags || (ajaxify.data.tag ? [ajaxify.data.tag] : []),
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.loadJQueryUI = function (callback) {
|
|
|
|
if (typeof $().autocomplete === 'function') {
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
require([
|
|
|
|
'jquery-ui/widgets/datepicker',
|
|
|
|
'jquery-ui/widgets/autocomplete',
|
|
|
|
'jquery-ui/widgets/sortable',
|
|
|
|
'jquery-ui/widgets/resizable',
|
|
|
|
'jquery-ui/widgets/draggable',
|
|
|
|
], function () {
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
app.parseAndTranslate = function (template, blockName, data, callback) {
|
|
|
|
if (typeof blockName !== 'string') {
|
|
|
|
callback = data;
|
|
|
|
data = blockName;
|
|
|
|
blockName = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
require(['translator', 'benchpress'], function (translator, Benchpress) {
|
|
|
|
Benchpress.render(template, data, blockName)
|
|
|
|
.then(rendered => translator.translate(rendered))
|
|
|
|
.then(translated => translator.unescape(translated))
|
|
|
|
.then(resolve, reject);
|
|
|
|
});
|
|
|
|
}).then((html) => {
|
|
|
|
html = $(html);
|
|
|
|
if (callback && typeof callback === 'function') {
|
|
|
|
setTimeout(callback, 0, html);
|
|
|
|
}
|
|
|
|
|
|
|
|
return html;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
function registerServiceWorker() {
|
|
|
|
// Do not register for Safari browsers
|
|
|
|
if (!ajaxify.data._locals.useragent.isSafari && 'serviceWorker' in navigator) {
|
|
|
|
navigator.serviceWorker.register(config.relative_path + '/service-worker.js', { scope: config.relative_path + '/' })
|
|
|
|
.then(function () {
|
|
|
|
console.info('ServiceWorker registration succeeded.');
|
|
|
|
}).catch(function (err) {
|
|
|
|
console.info('ServiceWorker registration failed: ', err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}());
|