PERF: Migrate header to discourse widgets
This commit is contained in:
parent
d1f61015c0
commit
514c3976f0
|
@ -1,76 +0,0 @@
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
|
||||||
import mobile from 'discourse/lib/mobile';
|
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
|
||||||
classNames: ['hamburger-panel'],
|
|
||||||
|
|
||||||
@computed('currentUser.read_faq')
|
|
||||||
prioritizeFaq(readFaq) {
|
|
||||||
// If it's a custom FAQ never prioritize it
|
|
||||||
return Ember.isEmpty(this.siteSettings.faq_url) && !readFaq;
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
showKeyboardShortcuts() {
|
|
||||||
return !this.site.mobileView && !this.capabilities.touch;
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
showMobileToggle() {
|
|
||||||
return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
mobileViewLinkTextKey() {
|
|
||||||
return this.site.mobileView ? "desktop_view" : "mobile_view";
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
faqUrl() {
|
|
||||||
return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq');
|
|
||||||
},
|
|
||||||
|
|
||||||
_lookupCount(type) {
|
|
||||||
const state = this.get('topicTrackingState');
|
|
||||||
return state ? state.lookupCount(type) : 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed('topicTrackingState.messageCount')
|
|
||||||
newCount() {
|
|
||||||
return this._lookupCount('new');
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed('topicTrackingState.messageCount')
|
|
||||||
unreadCount() {
|
|
||||||
return this._lookupCount('unread');
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
categories() {
|
|
||||||
const hideUncategorized = !this.siteSettings.allow_uncategorized_topics;
|
|
||||||
const showSubcatList = this.siteSettings.show_subcategory_list;
|
|
||||||
const isStaff = Discourse.User.currentProp('staff');
|
|
||||||
|
|
||||||
return Discourse.Category.list().reject((c) => {
|
|
||||||
if (showSubcatList && c.get('parent_category_id')) { return true; }
|
|
||||||
if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; }
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
showUserDirectoryLink() {
|
|
||||||
if (!this.siteSettings.enable_user_directory) return false;
|
|
||||||
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
keyboardShortcuts() {
|
|
||||||
this.sendAction('showKeyboardAction');
|
|
||||||
},
|
|
||||||
toggleMobileView() {
|
|
||||||
mobile.toggleMobileView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,14 +1,7 @@
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Ember.Component.extend({
|
||||||
tagName: 'li',
|
tagName: 'li',
|
||||||
classNameBindings: [':header-dropdown-toggle', 'active'],
|
classNameBindings: [':header-dropdown-toggle', 'active'],
|
||||||
|
|
||||||
@computed('showUser', 'path')
|
|
||||||
href(showUser, path) {
|
|
||||||
return showUser ? this.currentUser.get('path') : Discourse.getURL(path);
|
|
||||||
},
|
|
||||||
|
|
||||||
active: Ember.computed.alias('toggleVisible'),
|
active: Ember.computed.alias('toggleVisible'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -1,54 +1,3 @@
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
export function needsSecondRowIf() {
|
||||||
|
Ember.warn("DEPRECATION: `needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`");
|
||||||
const TopicCategoryComponent = Ember.Component.extend({
|
|
||||||
needsSecondRow: Ember.computed.gt('secondRowItems.length', 0),
|
|
||||||
secondRowItems: function() { return []; }.property(),
|
|
||||||
|
|
||||||
pmPath: function() {
|
|
||||||
var currentUser = this.get('currentUser');
|
|
||||||
return currentUser && currentUser.pmPath(this.get('topic'));
|
|
||||||
}.property('topic'),
|
|
||||||
|
|
||||||
showPrivateMessageGlyph: function() {
|
|
||||||
return !this.get('topic.is_warning') && this.get('topic.isPrivateMessage');
|
|
||||||
}.property('topic.is_warning', 'topic.isPrivateMessage'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
jumpToTopPost() {
|
|
||||||
const topic = this.get('topic');
|
|
||||||
if (topic) {
|
|
||||||
DiscourseURL.routeTo(topic.get('firstPostUrl'));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
let id = 0;
|
|
||||||
|
|
||||||
// Allow us (and plugins) to register themselves as needing a second
|
|
||||||
// row in the header. If there is at least one thing in the second row
|
|
||||||
// the style changes to accomodate it.
|
|
||||||
function needsSecondRowIf(prop, cb) {
|
|
||||||
const rowId = "_second_row_" + (id++),
|
|
||||||
methodHash = {};
|
|
||||||
|
|
||||||
methodHash[id] = function() {
|
|
||||||
const secondRowItems = this.get('secondRowItems'),
|
|
||||||
propVal = this.get(prop);
|
|
||||||
if (cb.call(this, propVal)) {
|
|
||||||
secondRowItems.addObject(rowId);
|
|
||||||
} else {
|
|
||||||
secondRowItems.removeObject(rowId);
|
|
||||||
}
|
|
||||||
}.observes(prop).on('init');
|
|
||||||
|
|
||||||
TopicCategoryComponent.reopen(methodHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
needsSecondRowIf('topic.category', function(cat) {
|
|
||||||
return cat && (!cat.get('isUncategorizedCategory') || !this.siteSettings.suppress_uncategorized_badge);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default TopicCategoryComponent;
|
|
||||||
export { needsSecondRowIf };
|
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
|
||||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
|
||||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
|
||||||
widget: 'home-logo',
|
|
||||||
showMobileLogo: null,
|
|
||||||
linkUrl: null,
|
|
||||||
classNames: ['title'],
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._super();
|
|
||||||
this.showMobileLogo = this.site.mobileView && !Ember.isEmpty(this.siteSettings.mobile_logo_url);
|
|
||||||
this.linkUrl = this.get('targetUrl') || '/';
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes('minimized')
|
|
||||||
_updateLogo() {
|
|
||||||
// On mobile we don't minimize the logo
|
|
||||||
if (!this.site.mobileView) {
|
|
||||||
this.rerender();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
click(e) {
|
|
||||||
// if they want to open in a new tab, let it so
|
|
||||||
if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) { return true; }
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
DiscourseURL.routeTo(this.linkUrl);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
render(buffer) {
|
|
||||||
const { siteSettings } = this;
|
|
||||||
const logoUrl = siteSettings.logo_url || '';
|
|
||||||
const title = siteSettings.title;
|
|
||||||
|
|
||||||
buffer.push(`<a href="${this.linkUrl}" data-auto-route="true">`);
|
|
||||||
if (!this.site.mobileView && this.get('minimized')) {
|
|
||||||
const logoSmallUrl = siteSettings.logo_small_url || '';
|
|
||||||
if (logoSmallUrl.length) {
|
|
||||||
buffer.push(`<img id='site-logo' class="logo-small" src="${logoSmallUrl}" width="33" height="33" alt="${title}">`);
|
|
||||||
} else {
|
|
||||||
buffer.push(iconHTML('home'));
|
|
||||||
}
|
|
||||||
} else if (this.showMobileLogo) {
|
|
||||||
buffer.push(`<img id="site-logo" class="logo-big" src="${siteSettings.mobile_logo_url}" alt="${title}">`);
|
|
||||||
} else if (logoUrl.length) {
|
|
||||||
buffer.push(`<img id="site-logo" class="logo-big" src="${logoUrl}" alt="${title}">`);
|
|
||||||
} else {
|
|
||||||
buffer.push(`<h2 id="site-text-logo" class="text-logo">${title}</h2>`);
|
|
||||||
}
|
|
||||||
buffer.push('</a>');
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,224 +0,0 @@
|
||||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
|
||||||
import { headerHeight } from 'discourse/views/header';
|
|
||||||
|
|
||||||
const PANEL_BODY_MARGIN = 30;
|
|
||||||
const mutationSupport = !Ember.testing && !!window['MutationObserver'];
|
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
|
||||||
classNameBindings: [':menu-panel', 'visible::hidden', 'viewMode'],
|
|
||||||
_lastVisible: false,
|
|
||||||
|
|
||||||
showClose: Ember.computed.equal('viewMode', 'slide-in'),
|
|
||||||
|
|
||||||
_layoutComponent() {
|
|
||||||
if (!this.get('visible')) { return; }
|
|
||||||
|
|
||||||
const $window = $(window);
|
|
||||||
let width = this.get('maxWidth') || 300;
|
|
||||||
const windowWidth = parseInt($window.width());
|
|
||||||
|
|
||||||
if ((windowWidth - width) < 50) {
|
|
||||||
width = windowWidth - 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewMode = this.get('viewMode');
|
|
||||||
const $panelBody = this.$('.panel-body');
|
|
||||||
let contentHeight = parseInt(this.$('.panel-body-contents').height());
|
|
||||||
|
|
||||||
// We use a mutationObserver to check for style changes, so it's important
|
|
||||||
// we don't set it if it doesn't change. Same goes for the $panelBody!
|
|
||||||
const style = this.$().prop('style');
|
|
||||||
|
|
||||||
if (viewMode === 'drop-down') {
|
|
||||||
const $buttonPanel = $('header ul.icons');
|
|
||||||
if ($buttonPanel.length === 0) { return; }
|
|
||||||
|
|
||||||
// These values need to be set here, not in the css file - this is to deal with the
|
|
||||||
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
|
|
||||||
if (style.top !== '100%' || style.height !== 'auto') {
|
|
||||||
this.$().css({ top: '100%', height: 'auto' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// adjust panel height
|
|
||||||
const fullHeight = parseInt($window.height());
|
|
||||||
const offsetTop = this.$().offset().top;
|
|
||||||
const scrollTop = $window.scrollTop();
|
|
||||||
|
|
||||||
if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) {
|
|
||||||
contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
|
|
||||||
}
|
|
||||||
if ($panelBody.height() !== contentHeight) {
|
|
||||||
$panelBody.height(contentHeight);
|
|
||||||
}
|
|
||||||
$('body').addClass('drop-down-visible');
|
|
||||||
} else {
|
|
||||||
const menuTop = headerHeight();
|
|
||||||
|
|
||||||
let height;
|
|
||||||
const winHeight = $(window).height() - 16;
|
|
||||||
if ((menuTop + contentHeight) < winHeight) {
|
|
||||||
height = contentHeight + "px";
|
|
||||||
} else {
|
|
||||||
height = winHeight - menuTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($panelBody.prop('style').height !== '100%') {
|
|
||||||
$panelBody.height('100%');
|
|
||||||
}
|
|
||||||
if (style.top !== menuTop + "px" || style.height !== height) {
|
|
||||||
this.$().css({ top: menuTop + "px", height });
|
|
||||||
}
|
|
||||||
$('body').removeClass('drop-down-visible');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$().width(width);
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed('force')
|
|
||||||
viewMode() {
|
|
||||||
const force = this.get('force');
|
|
||||||
if (force) { return force; }
|
|
||||||
|
|
||||||
const headerWidth = $('#main-outlet .container').width() || 1100;
|
|
||||||
const screenWidth = $(window).width();
|
|
||||||
const remaining = parseInt((screenWidth - headerWidth) / 2);
|
|
||||||
|
|
||||||
return (remaining < 50) ? 'slide-in' : 'drop-down';
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes('viewMode', 'visible')
|
|
||||||
_visibleChanged() {
|
|
||||||
if (this.get('visible')) {
|
|
||||||
// Allow us to hook into things being shown
|
|
||||||
if (!this._lastVisible) {
|
|
||||||
Ember.run.scheduleOnce('afterRender', () => this.sendAction('onVisible'));
|
|
||||||
this._lastVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('html').on('click.close-menu-panel', (e) => {
|
|
||||||
const $target = $(e.target);
|
|
||||||
if ($target.closest('.header-dropdown-toggle').length > 0) { return; }
|
|
||||||
if ($target.closest('.menu-panel').length > 0) { return; }
|
|
||||||
this.hide();
|
|
||||||
});
|
|
||||||
this.performLayout();
|
|
||||||
this._watchSizeChanges();
|
|
||||||
|
|
||||||
// iOS does not handle scroll events well
|
|
||||||
if (!this.capabilities.isIOS) {
|
|
||||||
$(window).on('scroll.discourse-menu-panel', () => this.performLayout());
|
|
||||||
}
|
|
||||||
} else if (this._lastVisible) {
|
|
||||||
this._lastVisible = false;
|
|
||||||
Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden'));
|
|
||||||
$('html').off('click.close-menu-panel');
|
|
||||||
$(window).off('scroll.discourse-menu-panel');
|
|
||||||
this._stopWatchingSize();
|
|
||||||
$('body').removeClass('drop-down-visible');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
showKeyboardShortcuts() {
|
|
||||||
return !this.site.mobileView && !this.capabilities.touch;
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
showMobileToggle() {
|
|
||||||
return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
mobileViewLinkTextKey() {
|
|
||||||
return this.site.mobileView ? "desktop_view" : "mobile_view";
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
faqUrl() {
|
|
||||||
return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq');
|
|
||||||
},
|
|
||||||
|
|
||||||
performLayout() {
|
|
||||||
Ember.run.scheduleOnce('afterRender', this, this._layoutComponent);
|
|
||||||
},
|
|
||||||
|
|
||||||
_watchSizeChanges() {
|
|
||||||
if (mutationSupport) {
|
|
||||||
this._observer.disconnect();
|
|
||||||
this._observer.observe(this.element, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
||||||
} else {
|
|
||||||
clearInterval(this._resizeInterval);
|
|
||||||
this._resizeInterval = setInterval(() => {
|
|
||||||
Ember.run(() => {
|
|
||||||
const $panelBodyContents = this.$('.panel-body-contents');
|
|
||||||
if ($panelBodyContents && $panelBodyContents.length) {
|
|
||||||
const contentHeight = parseInt($panelBodyContents.height());
|
|
||||||
if (contentHeight !== this._lastHeight) { this.performLayout(); }
|
|
||||||
this._lastHeight = contentHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_stopWatchingSize() {
|
|
||||||
if (mutationSupport) {
|
|
||||||
this._observer.disconnect();
|
|
||||||
} else {
|
|
||||||
clearInterval(this._resizeInterval);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@on('didInsertElement')
|
|
||||||
_bindEvents() {
|
|
||||||
this.$().on('click.discourse-menu-panel', 'a', e => {
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey) { return; }
|
|
||||||
const $target = $(e.target);
|
|
||||||
if ($target.data('ember-action') || $target.closest('.search-link').length > 0) { return; }
|
|
||||||
this.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.appEvents.on('dropdowns:closeAll', this, this.hide);
|
|
||||||
this.appEvents.on('dom:clean', this, this.hide);
|
|
||||||
|
|
||||||
$('body').on('keydown.discourse-menu-panel', e => {
|
|
||||||
if (e.which === 27) {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(window).on('resize.discourse-menu-panel', () => {
|
|
||||||
this.propertyDidChange('viewMode');
|
|
||||||
this.performLayout();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (mutationSupport) {
|
|
||||||
this._observer = new MutationObserver(() => {
|
|
||||||
Ember.run.debounce(this, this.performLayout, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.propertyDidChange('viewMode');
|
|
||||||
},
|
|
||||||
|
|
||||||
@on('willDestroyElement')
|
|
||||||
_removeEvents() {
|
|
||||||
this.appEvents.off('dom:clean', this, this.hide);
|
|
||||||
this.appEvents.off('dropdowns:closeAll', this, this.hide);
|
|
||||||
this.$().off('click.discourse-menu-panel');
|
|
||||||
$('body').off('keydown.discourse-menu-panel');
|
|
||||||
$('html').off('click.close-menu-panel');
|
|
||||||
$(window).off('resize.discourse-menu-panel');
|
|
||||||
$(window).off('scroll.discourse-menu-panel');
|
|
||||||
},
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
this.set('visible', false);
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
close() {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { keyDirty } from 'discourse/widgets/widget';
|
||||||
import { diff, patch } from 'virtual-dom';
|
import { diff, patch } from 'virtual-dom';
|
||||||
import { WidgetClickHook } from 'discourse/widgets/click-hook';
|
import { WidgetClickHook } from 'discourse/widgets/hooks';
|
||||||
import { renderedKey, queryRegistry } from 'discourse/widgets/widget';
|
import { renderedKey, queryRegistry } from 'discourse/widgets/widget';
|
||||||
|
|
||||||
const _cleanCallbacks = {};
|
const _cleanCallbacks = {};
|
||||||
|
@ -13,13 +14,20 @@ export default Ember.Component.extend({
|
||||||
_rootNode: null,
|
_rootNode: null,
|
||||||
_timeout: null,
|
_timeout: null,
|
||||||
_widgetClass: null,
|
_widgetClass: null,
|
||||||
_afterRender: null,
|
_renderCallback: null,
|
||||||
|
_childEvents: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super();
|
this._super();
|
||||||
const name = this.get('widget');
|
const name = this.get('widget');
|
||||||
|
|
||||||
this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`);
|
this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`);
|
||||||
|
|
||||||
|
if (!this._widgetClass) {
|
||||||
|
console.error(`Error: Could not find widget: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._childEvents = [];
|
||||||
this._connected = [];
|
this._connected = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -42,50 +50,64 @@ export default Ember.Component.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
|
this._childEvents.forEach(evt => this.appEvents.off(evt));
|
||||||
Ember.run.cancel(this._timeout);
|
Ember.run.cancel(this._timeout);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
afterRender() {
|
||||||
|
},
|
||||||
|
|
||||||
|
beforePatch() {
|
||||||
|
},
|
||||||
|
|
||||||
|
afterPatch() {
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatch(eventName, key) {
|
||||||
|
this._childEvents.push(eventName);
|
||||||
|
this.appEvents.on(eventName, refreshArg => {
|
||||||
|
const onRefresh = Ember.String.camelize(eventName.replace(/:/, '-'));
|
||||||
|
keyDirty(key, { onRefresh, refreshArg });
|
||||||
|
this.queueRerender();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
queueRerender(callback) {
|
queueRerender(callback) {
|
||||||
if (callback && !this._afterRender) {
|
if (callback && !this._renderCallback) {
|
||||||
this._afterRender = callback;
|
this._renderCallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ember.run.scheduleOnce('render', this, this.rerenderWidget);
|
Ember.run.scheduleOnce('render', this, this.rerenderWidget);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
buildArgs() {
|
||||||
|
},
|
||||||
|
|
||||||
rerenderWidget() {
|
rerenderWidget() {
|
||||||
Ember.run.cancel(this._timeout);
|
Ember.run.cancel(this._timeout);
|
||||||
if (this._rootNode) {
|
if (this._rootNode) {
|
||||||
|
if (!this._widgetClass) { return; }
|
||||||
|
|
||||||
const t0 = new Date().getTime();
|
const t0 = new Date().getTime();
|
||||||
|
|
||||||
|
const args = this.get('args') || this.buildArgs();
|
||||||
const opts = { model: this.get('model') };
|
const opts = { model: this.get('model') };
|
||||||
const newTree = new this._widgetClass(this.get('args'), this.container, opts);
|
const newTree = new this._widgetClass(args, this.container, opts);
|
||||||
|
|
||||||
newTree._emberView = this;
|
newTree._emberView = this;
|
||||||
const patches = diff(this._tree || this._rootNode, newTree);
|
const patches = diff(this._tree || this._rootNode, newTree);
|
||||||
|
|
||||||
const $body = $(document);
|
this.beforePatch();
|
||||||
const prevHeight = $body.height();
|
|
||||||
const prevScrollTop = $body.scrollTop();
|
|
||||||
|
|
||||||
this._rootNode = patch(this._rootNode, patches);
|
this._rootNode = patch(this._rootNode, patches);
|
||||||
|
this.afterPatch();
|
||||||
const height = $body.height();
|
|
||||||
const scrollTop = $body.scrollTop();
|
|
||||||
|
|
||||||
// This hack is for when swapping out many cloaked views at once
|
|
||||||
// when using keyboard navigation. It could suddenly move the
|
|
||||||
// scroll
|
|
||||||
if (prevHeight === height && scrollTop !== prevScrollTop) {
|
|
||||||
$body.scrollTop(prevScrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._tree = newTree;
|
this._tree = newTree;
|
||||||
|
|
||||||
if (this._afterRender) {
|
if (this._renderCallback) {
|
||||||
this._afterRender();
|
this._renderCallback();
|
||||||
this._afterRender = null;
|
this._renderCallback = null;
|
||||||
}
|
}
|
||||||
|
this.afterRender();
|
||||||
|
|
||||||
renderedKey('*');
|
renderedKey('*');
|
||||||
if (this.profileWidget) {
|
if (this.profileWidget) {
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
const LIKED_TYPE = 5;
|
|
||||||
const INVITED_TYPE = 8;
|
|
||||||
const GROUP_SUMMARY_TYPE = 16;
|
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
|
||||||
tagName: 'li',
|
|
||||||
classNameBindings: ['notification.read', 'notification.is_warning'],
|
|
||||||
|
|
||||||
name: function() {
|
|
||||||
var notificationType = this.get("notification.notification_type");
|
|
||||||
var lookup = this.site.get("notificationLookup");
|
|
||||||
return lookup[notificationType];
|
|
||||||
}.property("notification.notification_type"),
|
|
||||||
|
|
||||||
scope: function() {
|
|
||||||
if (this.get("name") === "custom") {
|
|
||||||
return this.get("notification.data.message");
|
|
||||||
} else {
|
|
||||||
return "notifications." + this.get("name");
|
|
||||||
}
|
|
||||||
}.property("name"),
|
|
||||||
|
|
||||||
url: function() {
|
|
||||||
const it = this.get('notification');
|
|
||||||
const badgeId = it.get("data.badge_id");
|
|
||||||
if (badgeId) {
|
|
||||||
var badgeSlug = it.get("data.badge_slug");
|
|
||||||
|
|
||||||
if (!badgeSlug) {
|
|
||||||
const badgeName = it.get("data.badge_name");
|
|
||||||
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
var username = it.get('data.username');
|
|
||||||
username = username ? "?username=" + username.toLowerCase() : "";
|
|
||||||
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username);
|
|
||||||
}
|
|
||||||
|
|
||||||
const topicId = it.get('topic_id');
|
|
||||||
if (topicId) {
|
|
||||||
return Discourse.Utilities.postUrl(it.get("slug"), topicId, it.get("post_number"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it.get('notification_type') === INVITED_TYPE) {
|
|
||||||
return Discourse.getURL('/users/' + it.get('data.display_username'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it.get('data.group_id')) {
|
|
||||||
return Discourse.getURL('/users/' + it.get('data.username') + '/messages/group/' + it.get('data.group_name'));
|
|
||||||
}
|
|
||||||
|
|
||||||
}.property("notification.data.{badge_id,badge_name,display_username}", "model.slug", "model.topic_id", "model.post_number"),
|
|
||||||
|
|
||||||
description: function() {
|
|
||||||
const badgeName = this.get("notification.data.badge_name");
|
|
||||||
if (badgeName) { return Discourse.Utilities.escapeExpression(badgeName); }
|
|
||||||
|
|
||||||
const title = this.get('notification.data.topic_title');
|
|
||||||
return Ember.isEmpty(title) ? "" : Discourse.Utilities.escapeExpression(title);
|
|
||||||
}.property("notification.data.{badge_name,topic_title}"),
|
|
||||||
|
|
||||||
_markRead: function(){
|
|
||||||
this.$('a').click(() => {
|
|
||||||
this.set('notification.read', true);
|
|
||||||
Discourse.setTransientHeader("Discourse-Clear-Notifications", this.get('notification.id'));
|
|
||||||
if (document && document.cookie) {
|
|
||||||
document.cookie = `cn=${this.get('notification.id')}; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}.on('didInsertElement'),
|
|
||||||
|
|
||||||
render(buffer) {
|
|
||||||
const notification = this.get('notification');
|
|
||||||
// since we are reusing views now sometimes this can be unset
|
|
||||||
if (!notification) { return; }
|
|
||||||
const description = this.get('description');
|
|
||||||
const username = notification.get('data.display_username');
|
|
||||||
var text;
|
|
||||||
if (notification.get('notification_type') === GROUP_SUMMARY_TYPE) {
|
|
||||||
const count = notification.get('data.inbox_count');
|
|
||||||
const group_name = notification.get('data.group_name');
|
|
||||||
text = I18n.t(this.get('scope'), {count, group_name});
|
|
||||||
} else if (notification.get('notification_type') === LIKED_TYPE && notification.get("data.count") > 1) {
|
|
||||||
const count = notification.get('data.count') - 2;
|
|
||||||
const username2 = notification.get('data.username2');
|
|
||||||
if (count===0) {
|
|
||||||
text = I18n.t('notifications.liked_2', {description, username, username2});
|
|
||||||
} else {
|
|
||||||
text = I18n.t('notifications.liked_many', {description, username, username2, count});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
text = I18n.t(this.get('scope'), {description, username});
|
|
||||||
}
|
|
||||||
text = Discourse.Emoji.unescape(text);
|
|
||||||
|
|
||||||
const url = this.get('url');
|
|
||||||
if (url) {
|
|
||||||
buffer.push('<a href="' + url + '" alt="' + I18n.t('notifications.alt.' + this.get("name")) + '">' + text + '</a>');
|
|
||||||
} else {
|
|
||||||
buffer.push(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -37,6 +37,25 @@ export default MountWidget.extend({
|
||||||
'searchService');
|
'searchService');
|
||||||
}).volatile(),
|
}).volatile(),
|
||||||
|
|
||||||
|
beforePatch() {
|
||||||
|
const $body = $(document);
|
||||||
|
this.prevHeight = $body.height();
|
||||||
|
this.prevScrollTop = $body.scrollTop();
|
||||||
|
},
|
||||||
|
|
||||||
|
afterPatch() {
|
||||||
|
const $body = $(document);
|
||||||
|
const height = $body.height();
|
||||||
|
const scrollTop = $body.scrollTop();
|
||||||
|
|
||||||
|
// This hack is for when swapping out many cloaked views at once
|
||||||
|
// when using keyboard navigation. It could suddenly move the
|
||||||
|
// scroll
|
||||||
|
if (this.prevHeight === height && scrollTop !== this.prevScrollTop) {
|
||||||
|
$body.scrollTop(this.prevScrollTop);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
scrolled() {
|
scrolled() {
|
||||||
if (this.isDestroyed || this.isDestroying) { return; }
|
if (this.isDestroyed || this.isDestroying) { return; }
|
||||||
if (isWorkaroundActive()) { return; }
|
if (isWorkaroundActive()) { return; }
|
||||||
|
|
|
@ -1,162 +0,0 @@
|
||||||
import {searchForTerm, searchContextDescription, isValidSearchTerm } from 'discourse/lib/search';
|
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
|
||||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
|
||||||
import showModal from 'discourse/lib/show-modal';
|
|
||||||
|
|
||||||
let _dontSearch = false;
|
|
||||||
export default Ember.Component.extend({
|
|
||||||
searchService: Ember.inject.service('search'),
|
|
||||||
classNames: ['search-menu'],
|
|
||||||
typeFilter: null,
|
|
||||||
|
|
||||||
@observes('searchService.searchContext')
|
|
||||||
contextChanged: function() {
|
|
||||||
if (this.get('searchService.searchContextEnabled')) {
|
|
||||||
_dontSearch = true;
|
|
||||||
this.set('searchService.searchContextEnabled', false);
|
|
||||||
_dontSearch = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed('searchService.searchContext', 'searchService.term', 'searchService.searchContextEnabled')
|
|
||||||
fullSearchUrlRelative(searchContext, term, searchContextEnabled) {
|
|
||||||
|
|
||||||
if (searchContextEnabled && Ember.get(searchContext, 'type') === 'topic') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = '/search?q=' + encodeURIComponent(this.get('searchService.term'));
|
|
||||||
if (searchContextEnabled) {
|
|
||||||
if (searchContext.id.toString().toLowerCase() === this.get('currentUser.username_lower') &&
|
|
||||||
searchContext.type === "private_messages"
|
|
||||||
) {
|
|
||||||
url += ' in:private';
|
|
||||||
} else {
|
|
||||||
url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed('fullSearchUrlRelative')
|
|
||||||
fullSearchUrl(fullSearchUrlRelative) {
|
|
||||||
if (fullSearchUrlRelative) {
|
|
||||||
return Discourse.getURL(fullSearchUrlRelative);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed('searchService.searchContext')
|
|
||||||
searchContextDescription(ctx) {
|
|
||||||
return searchContextDescription(Em.get(ctx, 'type'), Em.get(ctx, 'user.username') || Em.get(ctx, 'category.name'));
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes('searchService.searchContextEnabled')
|
|
||||||
searchContextEnabledChanged() {
|
|
||||||
if (_dontSearch) { return; }
|
|
||||||
this.newSearchNeeded();
|
|
||||||
},
|
|
||||||
|
|
||||||
// If we need to perform another search
|
|
||||||
@observes('searchService.term', 'typeFilter')
|
|
||||||
newSearchNeeded() {
|
|
||||||
this.set('noResults', false);
|
|
||||||
const term = this.get('searchService.term');
|
|
||||||
if (isValidSearchTerm(term)) {
|
|
||||||
this.set('loading', true);
|
|
||||||
Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400);
|
|
||||||
} else {
|
|
||||||
this.setProperties({ content: null });
|
|
||||||
}
|
|
||||||
this.set('selectedIndex', 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
searchTerm(term, typeFilter) {
|
|
||||||
// for cancelling debounced search
|
|
||||||
if (this._cancelSearch){
|
|
||||||
this._cancelSearch = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._search) {
|
|
||||||
this._search.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchContext = this.get('searchService.searchContextEnabled') ? this.get('searchService.searchContext') : null;
|
|
||||||
this._search = searchForTerm(term, { typeFilter, searchContext, fullSearchUrl: this.get('fullSearchUrl') });
|
|
||||||
|
|
||||||
this._search.then((content) => {
|
|
||||||
this.setProperties({ noResults: !content, content });
|
|
||||||
}).finally(() => {
|
|
||||||
this.set('loading', false);
|
|
||||||
this._search = null;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed('typeFilter', 'loading')
|
|
||||||
showCancelFilter(typeFilter, loading) {
|
|
||||||
if (loading) { return false; }
|
|
||||||
return !Ember.isEmpty(typeFilter);
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes('searchService.term')
|
|
||||||
termChanged() {
|
|
||||||
this.cancelTypeFilter();
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
fullSearch() {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (this._search) {
|
|
||||||
this._search.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybe we are debounced and delayed
|
|
||||||
// stop that as well
|
|
||||||
this._cancelSearch = true;
|
|
||||||
Em.run.later(function() {
|
|
||||||
self._cancelSearch = false;
|
|
||||||
}, 400);
|
|
||||||
|
|
||||||
const url = this.get('fullSearchUrlRelative');
|
|
||||||
if (url) {
|
|
||||||
DiscourseURL.routeTo(url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
moreOfType(type) {
|
|
||||||
this.set('typeFilter', type);
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelType() {
|
|
||||||
this.cancelTypeFilter();
|
|
||||||
},
|
|
||||||
|
|
||||||
showedSearch() {
|
|
||||||
$('#search-term').focus().select();
|
|
||||||
},
|
|
||||||
|
|
||||||
showSearchHelp() {
|
|
||||||
// TODO: @EvitTrout how do we get a loading indicator here?
|
|
||||||
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => {
|
|
||||||
showModal('searchHelp', { model });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelHighlight() {
|
|
||||||
this.set('searchService.highlightTerm', null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelTypeFilter() {
|
|
||||||
this.set('typeFilter', null);
|
|
||||||
},
|
|
||||||
|
|
||||||
keyDown(e) {
|
|
||||||
if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) {
|
|
||||||
this.set('visible', false);
|
|
||||||
this.send('fullSearch');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,2 +0,0 @@
|
||||||
import SearchResult from 'discourse/components/search-result';
|
|
||||||
export default SearchResult.extend();
|
|
|
@ -1,2 +0,0 @@
|
||||||
import SearchResult from 'discourse/components/search-result';
|
|
||||||
export default SearchResult.extend();
|
|
|
@ -1,2 +0,0 @@
|
||||||
import SearchResult from 'discourse/components/search-result';
|
|
||||||
export default SearchResult.extend();
|
|
|
@ -1,2 +0,0 @@
|
||||||
import SearchResult from 'discourse/components/search-result';
|
|
||||||
export default SearchResult.extend();
|
|
|
@ -1,11 +0,0 @@
|
||||||
export default Ember.Component.extend({
|
|
||||||
tagName: 'ul',
|
|
||||||
|
|
||||||
_highlightOnInsert: function() {
|
|
||||||
const term = this.get('controller.term');
|
|
||||||
if(!_.isEmpty(term)) {
|
|
||||||
this.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'});
|
|
||||||
this.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} );
|
|
||||||
}
|
|
||||||
}.on('didInsertElement')
|
|
||||||
});
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
import MountWidget from 'discourse/components/mount-widget';
|
||||||
|
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
const _flagProperties = [];
|
||||||
|
function addFlagProperty(prop) {
|
||||||
|
_flagProperties.pushObject(prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PANEL_BODY_MARGIN = 30;
|
||||||
|
|
||||||
|
const SiteHeaderComponent = MountWidget.extend({
|
||||||
|
widget: 'header',
|
||||||
|
docAt: null,
|
||||||
|
dockedHeader: null,
|
||||||
|
_topic: null,
|
||||||
|
|
||||||
|
// profileWidget: true,
|
||||||
|
// classNameBindings: ['editingTopic'],
|
||||||
|
|
||||||
|
@observes('currentUser.unread_notifications', 'currentUser.unread_private_messages')
|
||||||
|
_notificationsChanged() {
|
||||||
|
this.queueRerender();
|
||||||
|
},
|
||||||
|
|
||||||
|
examineDockHeader() {
|
||||||
|
const $body = $('body');
|
||||||
|
|
||||||
|
// Check the dock after the current run loop. While rendering,
|
||||||
|
// it's much slower to calculate `outlet.offset()`
|
||||||
|
Ember.run.next(() => {
|
||||||
|
if (this.docAt === null) {
|
||||||
|
const outlet = $('#main-outlet');
|
||||||
|
if (!(outlet && outlet.length === 1)) return;
|
||||||
|
this.docAt = outlet.offset().top;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = window.pageYOffset || $('html').scrollTop();
|
||||||
|
if (offset >= this.docAt) {
|
||||||
|
if (!this.dockedHeader) {
|
||||||
|
$body.addClass('docked');
|
||||||
|
this.dockedHeader = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.dockedHeader) {
|
||||||
|
$body.removeClass('docked');
|
||||||
|
this.dockedHeader = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setTopic(topic) {
|
||||||
|
this._topic = topic;
|
||||||
|
this.queueRerender();
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
$(window).bind('scroll.discourse-dock', () => this.examineDockHeader());
|
||||||
|
$(document).bind('touchmove.discourse-dock', () => this.examineDockHeader());
|
||||||
|
$(window).on('resize.discourse-menu-panel', () => this.afterRender());
|
||||||
|
|
||||||
|
this.appEvents.on('header:show-topic', topic => this.setTopic(topic));
|
||||||
|
this.appEvents.on('header:hide-topic', () => this.setTopic(null));
|
||||||
|
|
||||||
|
this.dispatch('notifications:changed', 'user-notifications');
|
||||||
|
this.dispatch('header:keyboard-trigger', 'header');
|
||||||
|
this.examineDockHeader();
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super();
|
||||||
|
$(window).unbind('scroll.discourse-dock');
|
||||||
|
$(document).unbind('touchmove.discourse-dock');
|
||||||
|
$('body').off('keydown.header');
|
||||||
|
this.appEvents.off('notifications:changed');
|
||||||
|
$(window).off('resize.discourse-menu-panel');
|
||||||
|
|
||||||
|
this.appEvents.off('header:show-topic');
|
||||||
|
this.appEvents.off('header:hide-topic');
|
||||||
|
},
|
||||||
|
|
||||||
|
buildArgs() {
|
||||||
|
return {
|
||||||
|
flagCount: _flagProperties.reduce((prev, cur) => prev + this.get(cur), 0),
|
||||||
|
topic: this._topic,
|
||||||
|
canSignUp: this.get('canSignUp')
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
afterRender() {
|
||||||
|
const $menuPanels = $('.menu-panel');
|
||||||
|
if ($menuPanels.length === 0) { return; }
|
||||||
|
|
||||||
|
const $window = $(window);
|
||||||
|
const windowWidth = parseInt($window.width());
|
||||||
|
|
||||||
|
|
||||||
|
const headerWidth = $('#main-outlet .container').width() || 1100;
|
||||||
|
const remaining = parseInt((windowWidth - headerWidth) / 2);
|
||||||
|
const viewMode = (remaining < 50) ? 'slide-in' : 'drop-down';
|
||||||
|
|
||||||
|
$menuPanels.each((idx, panel) => {
|
||||||
|
const $panel = $(panel);
|
||||||
|
let width = parseInt($panel.attr('data-max-width') || 300);
|
||||||
|
if ((windowWidth - width) < 50) {
|
||||||
|
width = windowWidth - 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel.removeClass('drop-down').removeClass('slide-in').addClass(viewMode);
|
||||||
|
|
||||||
|
const $panelBody = $('.panel-body', $panel);
|
||||||
|
let contentHeight = parseInt($('.panel-body-contents', $panel).height());
|
||||||
|
|
||||||
|
// We use a mutationObserver to check for style changes, so it's important
|
||||||
|
// we don't set it if it doesn't change. Same goes for the $panelBody!
|
||||||
|
const style = $panel.prop('style');
|
||||||
|
|
||||||
|
if (viewMode === 'drop-down') {
|
||||||
|
const $buttonPanel = $('header ul.icons');
|
||||||
|
if ($buttonPanel.length === 0) { return; }
|
||||||
|
|
||||||
|
// These values need to be set here, not in the css file - this is to deal with the
|
||||||
|
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
|
||||||
|
if (style.top !== '100%' || style.height !== 'auto') {
|
||||||
|
$panel.css({ top: '100%', height: 'auto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust panel height
|
||||||
|
const fullHeight = parseInt($window.height());
|
||||||
|
const offsetTop = $panel.offset().top;
|
||||||
|
const scrollTop = $window.scrollTop();
|
||||||
|
|
||||||
|
if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) {
|
||||||
|
contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
|
||||||
|
}
|
||||||
|
if ($panelBody.height() !== contentHeight) {
|
||||||
|
$panelBody.height(contentHeight);
|
||||||
|
}
|
||||||
|
$('body').addClass('drop-down-visible');
|
||||||
|
} else {
|
||||||
|
const menuTop = headerHeight();
|
||||||
|
|
||||||
|
let height;
|
||||||
|
const winHeight = $(window).height() - 16;
|
||||||
|
if ((menuTop + contentHeight) < winHeight) {
|
||||||
|
height = contentHeight + "px";
|
||||||
|
} else {
|
||||||
|
height = winHeight - menuTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($panelBody.prop('style').height !== '100%') {
|
||||||
|
$panelBody.height('100%');
|
||||||
|
}
|
||||||
|
if (style.top !== menuTop + "px" || style.height !== height) {
|
||||||
|
$panel.css({ top: menuTop + "px", height });
|
||||||
|
}
|
||||||
|
$('body').removeClass('drop-down-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel.width(width);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SiteHeaderComponent;
|
||||||
|
|
||||||
|
function applyFlaggedProperties() {
|
||||||
|
const args = _flagProperties.slice();
|
||||||
|
args.push(function() {
|
||||||
|
this.queueRerender();
|
||||||
|
}.on('init'));
|
||||||
|
|
||||||
|
SiteHeaderComponent.reopen({ _flagsChanged: Ember.observer.apply(this, args) });
|
||||||
|
}
|
||||||
|
|
||||||
|
addFlagProperty('currentUser.site_flagged_posts_count');
|
||||||
|
addFlagProperty('currentUser.post_queue_new_count');
|
||||||
|
|
||||||
|
export { addFlagProperty, applyFlaggedProperties };
|
||||||
|
|
||||||
|
export function headerHeight() {
|
||||||
|
const $header = $('header.d-header');
|
||||||
|
const headerOffset = $header.offset();
|
||||||
|
const headerOffsetTop = (headerOffset) ? headerOffset.top : 0;
|
||||||
|
return parseInt($header.outerHeight() + headerOffsetTop - $(window).scrollTop());
|
||||||
|
}
|
|
@ -1,104 +0,0 @@
|
||||||
import { url } from 'discourse/lib/computed';
|
|
||||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
|
||||||
import { headerHeight } from 'discourse/views/header';
|
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
|
||||||
classNames: ['user-menu'],
|
|
||||||
notifications: null,
|
|
||||||
loadingNotifications: false,
|
|
||||||
notificationsPath: url('currentUser.path', '%@/notifications'),
|
|
||||||
bookmarksPath: url('currentUser.path', '%@/activity/bookmarks'),
|
|
||||||
messagesPath: url('currentUser.path', '%@/messages'),
|
|
||||||
preferencesPath: url('currentUser.path', '%@/preferences'),
|
|
||||||
|
|
||||||
@computed('allowAnon', 'isAnon')
|
|
||||||
showEnableAnon(allowAnon, isAnon) { return allowAnon && !isAnon; },
|
|
||||||
|
|
||||||
@computed('allowAnon', 'isAnon')
|
|
||||||
showDisableAnon(allowAnon, isAnon) { return allowAnon && isAnon; },
|
|
||||||
|
|
||||||
@observes('visible')
|
|
||||||
_loadNotifications() {
|
|
||||||
if (this.get("visible")) {
|
|
||||||
this.refreshNotifications();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes('currentUser.lastNotificationChange')
|
|
||||||
_resetCachedNotifications() {
|
|
||||||
const visible = this.get('visible');
|
|
||||||
|
|
||||||
if (!Discourse.get("hasFocus")) {
|
|
||||||
this.set('visible', false);
|
|
||||||
this.set('notifications', null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visible) {
|
|
||||||
this.refreshNotifications();
|
|
||||||
} else {
|
|
||||||
this.set('notifications', null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshNotifications() {
|
|
||||||
if (this.get('loadingNotifications')) { return; }
|
|
||||||
|
|
||||||
// estimate (poorly) the amount of notifications to return
|
|
||||||
var limit = Math.round(($(window).height() - headerHeight()) / 55);
|
|
||||||
// we REALLY don't want to be asking for negative counts of notifications
|
|
||||||
// less than 5 is also not that useful
|
|
||||||
if (limit < 5) { limit = 5; }
|
|
||||||
if (limit > 40) { limit = 40; }
|
|
||||||
|
|
||||||
// TODO: It's a bit odd to use the store in a component, but this one really
|
|
||||||
// wants to reach out and grab notifications
|
|
||||||
const store = this.container.lookup('store:main');
|
|
||||||
const stale = store.findStale('notification', {recent: true, limit }, {cacheKey: 'recent-notifications'});
|
|
||||||
|
|
||||||
if (stale.hasResults) {
|
|
||||||
const results = stale.results;
|
|
||||||
var content = results.get('content');
|
|
||||||
|
|
||||||
// we have to truncate to limit, otherwise we will render too much
|
|
||||||
if (content && (content.length > limit)) {
|
|
||||||
content = content.splice(0, limit);
|
|
||||||
results.set('content', content);
|
|
||||||
results.set('totalRows', limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set('notifications', results);
|
|
||||||
} else {
|
|
||||||
this.set('loadingNotifications', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
stale.refresh().then((notifications) => {
|
|
||||||
this.set('currentUser.unread_notifications', 0);
|
|
||||||
this.set('notifications', notifications);
|
|
||||||
}).catch(() => {
|
|
||||||
this.set('notifications', null);
|
|
||||||
}).finally(() => {
|
|
||||||
this.set('loadingNotifications', false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
allowAnon() {
|
|
||||||
return this.siteSettings.allow_anonymous_posting &&
|
|
||||||
(this.get("currentUser.trust_level") >= this.siteSettings.anonymous_posting_min_trust_level ||
|
|
||||||
this.get("isAnon"));
|
|
||||||
},
|
|
||||||
|
|
||||||
isAnon: Ember.computed.alias('currentUser.is_anonymous'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
toggleAnon() {
|
|
||||||
Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(function(){
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
logout() {
|
|
||||||
this.sendAction('logoutAction');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import MountWidget from 'discourse/components/mount-widget';
|
||||||
|
import { observes } from "ember-addons/ember-computed-decorators";
|
||||||
|
|
||||||
|
export default MountWidget.extend({
|
||||||
|
widget: 'user-notifications-large',
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super();
|
||||||
|
this.args = { notifications: this.get('notifications') };
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('notifications.length')
|
||||||
|
_triggerRefresh() {
|
||||||
|
this.queueRerender();
|
||||||
|
}
|
||||||
|
});
|
|
@ -386,7 +386,7 @@ export default Ember.Controller.extend({
|
||||||
let message = this.get('similarTopicsMessage');
|
let message = this.get('similarTopicsMessage');
|
||||||
if (!message) {
|
if (!message) {
|
||||||
message = Discourse.ComposerMessage.create({
|
message = Discourse.ComposerMessage.create({
|
||||||
templateName: 'composer/similar_topics',
|
templateName: 'composer/similar-topics',
|
||||||
extraClass: 'similar-topics'
|
extraClass: 'similar-topics'
|
||||||
});
|
});
|
||||||
this.set('similarTopicsMessage', message);
|
this.set('similarTopicsMessage', message);
|
||||||
|
|
|
@ -1,77 +1,6 @@
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
import { addFlagProperty as realAddFlagProperty } from 'discourse/components/site-header';
|
||||||
|
|
||||||
const HeaderController = Ember.Controller.extend({
|
export function addFlagProperty(prop) {
|
||||||
topic: null,
|
Ember.warn("importing `addFlagProperty` is deprecated. Use the PluginAPI instead");
|
||||||
showExtraInfo: null,
|
realAddFlagProperty(prop);
|
||||||
hamburgerVisible: false,
|
|
||||||
searchVisible: false,
|
|
||||||
userMenuVisible: false,
|
|
||||||
needs: ['application'],
|
|
||||||
|
|
||||||
canSignUp: Em.computed.alias('controllers.application.canSignUp'),
|
|
||||||
|
|
||||||
showSignUpButton: function() {
|
|
||||||
return this.get('canSignUp') && !this.get('showExtraInfo');
|
|
||||||
}.property('canSignUp', 'showExtraInfo'),
|
|
||||||
|
|
||||||
showStarButton: function() {
|
|
||||||
return Discourse.User.current() && !this.get('topic.isPrivateMessage');
|
|
||||||
}.property('topic.isPrivateMessage'),
|
|
||||||
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
toggleSearch() {
|
|
||||||
this.toggleProperty('searchVisible');
|
|
||||||
},
|
|
||||||
showUserMenu() {
|
|
||||||
if (!this.get('userMenuVisible')) {
|
|
||||||
this.appEvents.trigger('dropdowns:closeAll');
|
|
||||||
this.set('userMenuVisible', true);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
fullPageSearch() {
|
|
||||||
const searchService = this.container.lookup('search-service:main');
|
|
||||||
const context = searchService.get('searchContext');
|
|
||||||
var params = "";
|
|
||||||
|
|
||||||
if (context) {
|
|
||||||
params = `?context=${context.type}&context_id=${context.id}&skip_context=true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
DiscourseURL.routeTo('/search' + params);
|
|
||||||
},
|
|
||||||
toggleMenuPanel(visibleProp) {
|
|
||||||
this.toggleProperty(visibleProp);
|
|
||||||
this.appEvents.trigger('dropdowns:closeAll');
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleStar() {
|
|
||||||
const topic = this.get('topic');
|
|
||||||
if (topic) topic.toggleStar();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Allow plugins to add to the sum of "flags" above the site map
|
|
||||||
const _flagProperties = [];
|
|
||||||
function addFlagProperty(prop) {
|
|
||||||
_flagProperties.pushObject(prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFlaggedProperties() {
|
|
||||||
const args = _flagProperties.slice();
|
|
||||||
args.push(function() {
|
|
||||||
let sum = 0;
|
|
||||||
_flagProperties.forEach((fp) => sum += (this.get(fp) || 0));
|
|
||||||
return sum;
|
|
||||||
});
|
|
||||||
HeaderController.reopen({ flaggedPostsCount: Ember.computed.apply(this, args) });
|
|
||||||
}
|
|
||||||
|
|
||||||
addFlagProperty('currentUser.site_flagged_posts_count');
|
|
||||||
addFlagProperty('currentUser.post_queue_new_count');
|
|
||||||
|
|
||||||
export { addFlagProperty, applyFlaggedProperties };
|
|
||||||
export default HeaderController;
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Composer from 'discourse/models/composer';
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
|
||||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'],
|
needs: ['modal', 'composer', 'quote-button', 'topic-progress', 'application'],
|
||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
allPostsSelected: false,
|
allPostsSelected: false,
|
||||||
editingTopic: false,
|
editingTopic: false,
|
||||||
|
@ -472,11 +472,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
this.get('content').toggleStatus('archived');
|
this.get('content').toggleStatus('archived');
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggle the star on the topic
|
|
||||||
toggleStar() {
|
|
||||||
this.get('content').toggleStar();
|
|
||||||
},
|
|
||||||
|
|
||||||
clearPin() {
|
clearPin() {
|
||||||
this.get('content').clearPin();
|
this.get('content').clearPin();
|
||||||
},
|
},
|
||||||
|
@ -625,10 +620,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
showStarButton: function() {
|
|
||||||
return Discourse.User.current() && !this.get('model.isPrivateMessage');
|
|
||||||
}.property('model.isPrivateMessage'),
|
|
||||||
|
|
||||||
loadingHTML: function() {
|
loadingHTML: function() {
|
||||||
return spinnerHTML;
|
return spinnerHTML;
|
||||||
}.property(),
|
}.property(),
|
||||||
|
|
|
@ -10,13 +10,13 @@ export default Ember.ArrayController.extend({
|
||||||
currentPath: Em.computed.alias('controllers.application.currentPath'),
|
currentPath: Em.computed.alias('controllers.application.currentPath'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
resetNew: function() {
|
resetNew() {
|
||||||
Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => {
|
Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => {
|
||||||
this.setEach('read', true);
|
this.setEach('read', true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
loadMore: function() {
|
loadMore() {
|
||||||
this.get('model').loadMore();
|
this.get('model').loadMore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { applyFlaggedProperties } from 'discourse/controllers/header';
|
import { applyFlaggedProperties } from 'discourse/components/site-header';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'apply-flagged-properties',
|
name: 'apply-flagged-properties',
|
||||||
|
|
|
@ -10,7 +10,8 @@ export default {
|
||||||
siteSettings = container.lookup('site-settings:main'),
|
siteSettings = container.lookup('site-settings:main'),
|
||||||
bus = container.lookup('message-bus:main'),
|
bus = container.lookup('message-bus:main'),
|
||||||
keyValueStore = container.lookup('key-value-store:main'),
|
keyValueStore = container.lookup('key-value-store:main'),
|
||||||
store = container.lookup('store:main');
|
store = container.lookup('store:main'),
|
||||||
|
appEvents = container.lookup('app-events:main');
|
||||||
|
|
||||||
// clear old cached notifications, we used to store in local storage
|
// clear old cached notifications, we used to store in local storage
|
||||||
// TODO 2017 delete this line
|
// TODO 2017 delete this line
|
||||||
|
@ -30,7 +31,7 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bus.subscribe("/notification/" + user.get('id'), function(data) {
|
bus.subscribe(`/notification/${user.get('id')}`, function(data) {
|
||||||
const oldUnread = user.get('unread_notifications');
|
const oldUnread = user.get('unread_notifications');
|
||||||
const oldPM = user.get('unread_private_messages');
|
const oldPM = user.get('unread_private_messages');
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ export default {
|
||||||
user.set('unread_private_messages', data.unread_private_messages);
|
user.set('unread_private_messages', data.unread_private_messages);
|
||||||
|
|
||||||
if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) {
|
if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) {
|
||||||
user.set('lastNotificationChange', new Date());
|
appEvents.trigger('notifications:changed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stale = store.findStale('notification', {}, {cacheKey: 'recent-notifications'});
|
const stale = store.findStale('notification', {}, {cacheKey: 'recent-notifications'});
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default function interceptClick(e) {
|
||||||
$currentTarget.data('auto-route') ||
|
$currentTarget.data('auto-route') ||
|
||||||
$currentTarget.data('share-url') ||
|
$currentTarget.data('share-url') ||
|
||||||
$currentTarget.data('user-card') ||
|
$currentTarget.data('user-card') ||
|
||||||
|
$currentTarget.hasClass('widget-link') ||
|
||||||
$currentTarget.hasClass('mention') ||
|
$currentTarget.hasClass('mention') ||
|
||||||
(!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) ||
|
(!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) ||
|
||||||
$currentTarget.hasClass('lightbox') ||
|
$currentTarget.hasClass('lightbox') ||
|
||||||
|
|
|
@ -10,8 +10,8 @@ const bindings = {
|
||||||
'.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
|
'.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
|
||||||
'b': {handler: 'toggleBookmark'},
|
'b': {handler: 'toggleBookmark'},
|
||||||
'c': {handler: 'createTopic'},
|
'c': {handler: 'createTopic'},
|
||||||
'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true},
|
'ctrl+f': {handler: 'showPageSearch', anonymous: true},
|
||||||
'command+f': {handler: 'showBuiltinSearch', anonymous: true},
|
'command+f': {handler: 'showPageSearch', anonymous: true},
|
||||||
'd': {postAction: 'deletePost'},
|
'd': {postAction: 'deletePost'},
|
||||||
'e': {postAction: 'editPost'},
|
'e': {postAction: 'editPost'},
|
||||||
'end': {handler: 'goToLastPost', anonymous: true},
|
'end': {handler: 'goToLastPost', anonymous: true},
|
||||||
|
@ -142,32 +142,10 @@ export default {
|
||||||
this._changeSection(-1);
|
this._changeSection(-1);
|
||||||
},
|
},
|
||||||
|
|
||||||
showBuiltinSearch() {
|
showPageSearch(event) {
|
||||||
if (this.container.lookup('controller:header').get('searchVisible')) {
|
Ember.run(() => {
|
||||||
this.toggleSearch();
|
this.appEvents.trigger('header:keyboard-trigger', {type: 'page-search', event});
|
||||||
return true;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.searchService.set('searchContextEnabled', false);
|
|
||||||
|
|
||||||
const currentPath = this.container.lookup('controller:application').get('currentPath'),
|
|
||||||
blacklist = [ /^discovery\.categories/ ],
|
|
||||||
whitelist = [ /^topic\./ ],
|
|
||||||
check = function(regex) { return !!currentPath.match(regex); };
|
|
||||||
let showSearch = whitelist.any(check) && !blacklist.any(check);
|
|
||||||
|
|
||||||
// If we're viewing a topic, only intercept search if there are cloaked posts
|
|
||||||
if (showSearch && currentPath.match(/^topic\./)) {
|
|
||||||
showSearch = $('.topic-post .cooked, .small-action:not(.time-gap)').length < this.container.lookup('controller:topic').get('model.postStream.stream.length');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showSearch) {
|
|
||||||
this.searchService.set('searchContextEnabled', true);
|
|
||||||
this.toggleSearch();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createTopic() {
|
createTopic() {
|
||||||
|
@ -182,17 +160,16 @@ export default {
|
||||||
this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true});
|
this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true});
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleSearch() {
|
toggleSearch(event) {
|
||||||
this.container.lookup('controller:header').send('toggleSearch');
|
this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event});
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleHamburgerMenu() {
|
toggleHamburgerMenu(event) {
|
||||||
this.container.lookup('controller:header').send('toggleMenuPanel', 'hamburgerVisible');
|
this.appEvents.trigger('header:keyboard-trigger', {type: 'hamburger', event});
|
||||||
},
|
},
|
||||||
|
|
||||||
showCurrentUser() {
|
showCurrentUser(event) {
|
||||||
this.container.lookup('controller:header').send('toggleMenuPanel', 'userMenuVisible');
|
this.appEvents.trigger('header:keyboard-trigger', {type: 'user', event});
|
||||||
},
|
},
|
||||||
|
|
||||||
showHelpModal() {
|
showHelpModal() {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { createWidget, decorateWidget, changeSetting } from 'discourse/widgets/w
|
||||||
import { onPageChange } from 'discourse/lib/page-tracker';
|
import { onPageChange } from 'discourse/lib/page-tracker';
|
||||||
import { preventCloak } from 'discourse/widgets/post-stream';
|
import { preventCloak } from 'discourse/widgets/post-stream';
|
||||||
import { h } from 'virtual-dom';
|
import { h } from 'virtual-dom';
|
||||||
|
import { addFlagProperty } from 'discourse/components/site-header';
|
||||||
|
|
||||||
class PluginApi {
|
class PluginApi {
|
||||||
constructor(version, container) {
|
constructor(version, container) {
|
||||||
|
@ -284,11 +285,20 @@ class PluginApi {
|
||||||
createWidget(name, args) {
|
createWidget(name, args) {
|
||||||
return createWidget(name, args);
|
return createWidget(name, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a property that can be summed for calculating the flag counter
|
||||||
|
**/
|
||||||
|
addFlagProperty(property) {
|
||||||
|
return addFlagProperty(property);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _pluginv01;
|
let _pluginv01;
|
||||||
function getPluginApi(version) {
|
function getPluginApi(version) {
|
||||||
if (version === "0.1" || version === "0.2" || version === "0.3") {
|
version = parseFloat(version);
|
||||||
|
if (version <= 0.4) {
|
||||||
if (!_pluginv01) {
|
if (!_pluginv01) {
|
||||||
_pluginv01 = new PluginApi(version, Discourse.__container__);
|
_pluginv01 = new PluginApi(version, Discourse.__container__);
|
||||||
}
|
}
|
||||||
|
@ -299,7 +309,7 @@ function getPluginApi(version) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* withPluginApi(version, apiCode, noApi)
|
* withPluginApi(version, apiCodeCallback, opts)
|
||||||
*
|
*
|
||||||
* Helper to version our client side plugin API. Pass the version of the API that your
|
* Helper to version our client side plugin API. Pass the version of the API that your
|
||||||
* plugin is coded against. If that API is available, the `apiCodeCallback` function will
|
* plugin is coded against. If that API is available, the `apiCodeCallback` function will
|
||||||
|
|
|
@ -3,6 +3,7 @@ import logout from 'discourse/lib/logout';
|
||||||
import showModal from 'discourse/lib/show-modal';
|
import showModal from 'discourse/lib/show-modal';
|
||||||
import OpenComposer from "discourse/mixins/open-composer";
|
import OpenComposer from "discourse/mixins/open-composer";
|
||||||
import Category from 'discourse/models/category';
|
import Category from 'discourse/models/category';
|
||||||
|
import mobile from 'discourse/lib/mobile';
|
||||||
|
|
||||||
function unlessReadOnly(method, message) {
|
function unlessReadOnly(method, message) {
|
||||||
return function() {
|
return function() {
|
||||||
|
@ -25,6 +26,22 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
|
showSearchHelp() {
|
||||||
|
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(model => {
|
||||||
|
showModal('searchHelp', { model });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleAnonymous() {
|
||||||
|
Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMobileView() {
|
||||||
|
mobile.toggleMobileView();
|
||||||
|
},
|
||||||
|
|
||||||
logout: unlessReadOnly('_handleLogout', I18n.t("read_only_mode.logout_disabled")),
|
logout: unlessReadOnly('_handleLogout', I18n.t("read_only_mode.logout_disabled")),
|
||||||
|
|
||||||
_collectTitleTokens(tokens) {
|
_collectTitleTokens(tokens) {
|
||||||
|
|
|
@ -75,7 +75,6 @@ const DiscourseRoute = Ember.Route.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
export function cleanDOM() {
|
export function cleanDOM() {
|
||||||
|
|
||||||
if (window.MiniProfiler) {
|
if (window.MiniProfiler) {
|
||||||
window.MiniProfiler.pageTransition();
|
window.MiniProfiler.pageTransition();
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,8 +179,9 @@ const TopicRoute = Discourse.Route.extend({
|
||||||
this.searchService.set('searchContext', null);
|
this.searchService.set('searchContext', null);
|
||||||
this.controllerFor('user-card').set('visible', false);
|
this.controllerFor('user-card').set('visible', false);
|
||||||
|
|
||||||
const topicController = this.controllerFor('topic'),
|
const topicController = this.controllerFor('topic');
|
||||||
postStream = topicController.get('model.postStream');
|
const postStream = topicController.get('model.postStream');
|
||||||
|
|
||||||
postStream.cancelFilter();
|
postStream.cancelFilter();
|
||||||
|
|
||||||
topicController.set('multiSelect', false);
|
topicController.set('multiSelect', false);
|
||||||
|
@ -188,11 +189,7 @@ const TopicRoute = Discourse.Route.extend({
|
||||||
this.controllerFor('composer').set('topic', null);
|
this.controllerFor('composer').set('topic', null);
|
||||||
this.screenTrack.stop();
|
this.screenTrack.stop();
|
||||||
|
|
||||||
const headerController = this.controllerFor('header');
|
this.appEvents.trigger('header:hide-topic');
|
||||||
if (headerController) {
|
|
||||||
headerController.set('topic', null);
|
|
||||||
headerController.set('showExtraInfo', false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
|
@ -207,7 +204,6 @@ const TopicRoute = Discourse.Route.extend({
|
||||||
|
|
||||||
TopicRoute.trigger('setupTopicController', this);
|
TopicRoute.trigger('setupTopicController', this);
|
||||||
|
|
||||||
this.controllerFor('header').setProperties({ topic: model, showExtraInfo: false });
|
|
||||||
this.searchService.set('searchContext', model.get('searchContext'));
|
this.searchService.set('searchContext', model.get('searchContext'));
|
||||||
|
|
||||||
this.controllerFor('composer').set('topic', model);
|
this.controllerFor('composer').set('topic', model);
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
{{render "header"}}
|
{{site-header canSignUp=canSignUp
|
||||||
|
showCreateAccount="showCreateAccount"
|
||||||
|
showLogin="showLogin"
|
||||||
|
showKeyboard="showKeyboardShortcutsHelp"
|
||||||
|
toggleMobileView="toggleMobileView"
|
||||||
|
toggleAnonymous="toggleAnonymous"
|
||||||
|
logout="logout"
|
||||||
|
showSearchHelp="showSearchHelp"}}
|
||||||
|
|
||||||
<div id="main-outlet" class="wrap">
|
<div id="main-outlet" class="wrap">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
{{#menu-panel visible=visible}}
|
|
||||||
{{#if prioritizeFaq}}
|
|
||||||
{{#menu-links}}
|
|
||||||
<li class='heading'>
|
|
||||||
{{#d-link path=faqUrl class="faq-link"}}
|
|
||||||
{{i18n "faq"}}
|
|
||||||
<span class='badge badge-notification'>{{i18n "new_item"}}</span>
|
|
||||||
{{/d-link}}
|
|
||||||
</li>
|
|
||||||
{{/menu-links}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if currentUser.staff}}
|
|
||||||
{{#menu-links}}
|
|
||||||
<li>{{d-link route="admin" class="admin-link" icon="wrench" label="admin_title"}}</li>
|
|
||||||
<li>
|
|
||||||
{{#d-link route="adminFlags" class="flagged-posts-link"}}
|
|
||||||
{{fa-icon "flag"}} {{i18n 'flags_title'}}
|
|
||||||
{{#if currentUser.site_flagged_posts_count}}
|
|
||||||
<span title={{i18n 'notifications.total_flagged'}} class='badge-notification flagged-posts'>{{currentUser.site_flagged_posts_count}}</span>
|
|
||||||
{{/if}}
|
|
||||||
{{/d-link}}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{{#if currentUser.show_queued_posts}}
|
|
||||||
<li>
|
|
||||||
{{#d-link route='queued-posts'}}
|
|
||||||
{{i18n "queue.title"}}
|
|
||||||
{{#if currentUser.post_queue_new_count}}
|
|
||||||
<span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span>
|
|
||||||
{{/if}}
|
|
||||||
{{/d-link}}
|
|
||||||
</li>
|
|
||||||
{{/if}}
|
|
||||||
{{#if currentUser.admin}}
|
|
||||||
<li>{{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}</li>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{plugin-outlet "hamburger-admin"}}
|
|
||||||
{{/menu-links}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#menu-links}}
|
|
||||||
<li>{{d-link route="discovery.latest" class="latest-topics-link" label="filters.latest.title"}}</li>
|
|
||||||
|
|
||||||
{{#if currentUser}}
|
|
||||||
<li>
|
|
||||||
{{#if newCount}}
|
|
||||||
{{d-link route="discovery.new" class="new-topics-link" label="filters.new.title_with_count" count=newCount}}
|
|
||||||
{{else}}
|
|
||||||
{{d-link route="discovery.new" class="new-topics-link" label="filters.new.title"}}
|
|
||||||
{{/if}}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{{#if unreadCount}}
|
|
||||||
{{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title_with_count" count=unreadCount}}
|
|
||||||
{{else}}
|
|
||||||
{{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title"}}
|
|
||||||
{{/if}}
|
|
||||||
</li>
|
|
||||||
{{/if}}
|
|
||||||
<li>{{d-link route="discovery.top" class="top-topics-link" label="filters.top.title"}}</li>
|
|
||||||
|
|
||||||
{{#if siteSettings.enable_badges}}
|
|
||||||
<li>{{d-link route="badges" class="badge-link" label="badges.title"}}</li>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if showUserDirectoryLink}}
|
|
||||||
<li>{{d-link route="users" class="user-directory-link" label="directory.title"}}</li>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{plugin-outlet "site-map-links"}}
|
|
||||||
|
|
||||||
{{plugin-outlet "site-map-links-last"}}
|
|
||||||
{{/menu-links}}
|
|
||||||
|
|
||||||
{{mount-widget widget='hamburger-categories' args=(as-hash categories=categories)}}
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
{{#menu-links omitRule="true"}}
|
|
||||||
<li>{{d-link route="about" class="about-link" label="about.simple_title"}}</li>
|
|
||||||
{{#unless prioritizeFaq}}
|
|
||||||
<li>{{d-link path=faqUrl class="faq-link" label="faq"}}</li>
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
{{#if showKeyboardShortcuts}}
|
|
||||||
<li>{{d-link action="keyboardShortcuts" class="keyboard-shortcuts-link" label="keyboard_shortcuts_help.title"}}</li>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if showMobileToggle}}
|
|
||||||
<li>{{d-link action="toggleMobileView" class="mobile-toggle-link" label=mobileViewLinkTextKey}}</li>
|
|
||||||
{{/if}}
|
|
||||||
{{/menu-links}}
|
|
||||||
{{/menu-panel}}
|
|
|
@ -1,9 +0,0 @@
|
||||||
<a {{action "toggle"}} class='icon' href={{href}} title={{i18n title}} aria-label={{i18n title}} id={{iconId}}>
|
|
||||||
{{#if showUser}}
|
|
||||||
{{bound-avatar currentUser "medium"}}
|
|
||||||
{{else}}
|
|
||||||
{{fa-icon icon}}
|
|
||||||
{{/if}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{yield}}
|
|
|
@ -1,21 +0,0 @@
|
||||||
<div class="extra-info-wrapper">
|
|
||||||
<div {{bind-attr class=":extra-info needsSecondRow:two-rows"}}>
|
|
||||||
<div class="title-wrapper">
|
|
||||||
<h1>
|
|
||||||
{{#if showPrivateMessageGlyph}}
|
|
||||||
<a href='{{pmPath}}'>
|
|
||||||
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
|
||||||
</a>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if topic.details.loaded}}
|
|
||||||
{{topic-status topic=topic}}
|
|
||||||
<a class='topic-link' href='{{unbound topic.url}}' {{action "jumpToTopPost"}}>{{{topic.fancyTitle}}}</a>
|
|
||||||
{{/if}}
|
|
||||||
</h1>
|
|
||||||
{{#if topic.details.loaded}}
|
|
||||||
{{topic-category topic=topic}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<ul class="menu-links columned">
|
|
||||||
{{yield}}
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</ul>
|
|
||||||
{{#unless omitRule}}
|
|
||||||
<hr>
|
|
||||||
{{/unless}}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{{#if visible}}
|
|
||||||
<div class='panel-body'>
|
|
||||||
<div class='panel-body-contents'>
|
|
||||||
{{yield}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
|
@ -1,40 +0,0 @@
|
||||||
{{#menu-panel visible=visible onVisible="showedSearch" onHidden="cancelHighlight" maxWidth="500"}}
|
|
||||||
{{plugin-outlet "above-search"}}
|
|
||||||
{{search-text-field value=searchService.term id="search-term"}}
|
|
||||||
|
|
||||||
<div class="search-context">
|
|
||||||
{{#if searchService.searchContext}}
|
|
||||||
<label>
|
|
||||||
{{input type="checkbox" name="searchContext" checked=searchService.searchContextEnabled}} {{searchContextDescription}}
|
|
||||||
</label>
|
|
||||||
{{/if}}
|
|
||||||
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "show_help"}}</a>
|
|
||||||
<div class='clearfix'></div>
|
|
||||||
</div>
|
|
||||||
{{#if loading}}
|
|
||||||
<div class="searching">{{loading-spinner}}</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="results">
|
|
||||||
{{#if noResults}}
|
|
||||||
<div class="no-results">
|
|
||||||
{{i18n "search.no_results"}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{#each content.resultTypes as |resultType|}}
|
|
||||||
<ul>
|
|
||||||
<li class="heading row">{{resultType.name}}</li>
|
|
||||||
{{component resultType.componentName results=resultType.results term=searchService.term}}
|
|
||||||
</ul>
|
|
||||||
<div class="no-results">
|
|
||||||
{{#if resultType.moreUrl}}
|
|
||||||
<a href={{resultType.moreUrl}} class="filter">{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
|
|
||||||
{{/if}}
|
|
||||||
{{#if resultType.more}}
|
|
||||||
<a href class="filter filter-type" {{action "moreOfType" resultType.type bubbles=false}}>{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/menu-panel}}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{{#each results as |result|}}
|
|
||||||
<li>
|
|
||||||
<a href='{{unbound result.url}}'>
|
|
||||||
{{category-badge result}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{{#each results as |result|}}
|
|
||||||
<li>
|
|
||||||
<a class='search-link' href='{{unbound result.urlWithNumber}}'>
|
|
||||||
<span class='topic'>
|
|
||||||
{{i18n 'search.post_format' post_number=result.post_number username=result.username}}
|
|
||||||
</span>
|
|
||||||
{{#unless site.mobileView}}
|
|
||||||
<span class='blurb'>
|
|
||||||
{{{unbound result.blurb}}}
|
|
||||||
</span>
|
|
||||||
{{/unless}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{{#each results as |result|}}
|
|
||||||
<li>
|
|
||||||
<a class='search-link' href='{{unbound result.url}}'>
|
|
||||||
<span class='topic'>
|
|
||||||
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{{unbound result.topic.fancyTitle}}}</span>{{category-badge result.topic.category}}{{plugin-outlet "search-category"}}
|
|
||||||
</span>
|
|
||||||
{{#unless site.mobileView}}
|
|
||||||
<span class='blurb'>
|
|
||||||
{{format-age result.created_at}} - {{{unbound result.blurb}}}
|
|
||||||
</span>
|
|
||||||
{{/unless}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{{#each results as |result|}}
|
|
||||||
<li>
|
|
||||||
<a href='{{unbound result.path}}'>
|
|
||||||
{{avatar result imageSize="small"}}
|
|
||||||
{{unbound result.username}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
|
@ -1,48 +0,0 @@
|
||||||
{{#menu-panel visible=visible}}
|
|
||||||
<div class='menu-links-header'>
|
|
||||||
<ul class='menu-links-row'>
|
|
||||||
|
|
||||||
{{#if showDisableAnon}}
|
|
||||||
<li>{{d-link route='user' model=currentUser class="user-activity-link" icon="user" label="user.profile"}}</li>
|
|
||||||
<li>{{d-link action="toggleAnon" label="switch_from_anon"}}</li>
|
|
||||||
{{else}}
|
|
||||||
<li>{{d-link route='user' model=currentUser class="user-activity-link" icon="user" translateLabel="false" label=currentUser.username}}</li>
|
|
||||||
{{/if}}
|
|
||||||
<li class='glyphs'>
|
|
||||||
{{d-link path=bookmarksPath title="user.bookmarks" icon="bookmark"}}
|
|
||||||
{{#if siteSettings.enable_private_messages}}
|
|
||||||
{{d-link path=messagesPath title="user.private_messages" icon="envelope"}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if showEnableAnon}}
|
|
||||||
{{d-link action="toggleAnon" title="switch_to_anon" icon="user-secret"}}
|
|
||||||
{{/if}}
|
|
||||||
{{d-link path=preferencesPath title="user.preferences" icon="gear"}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='notifications'>
|
|
||||||
{{#conditional-loading-spinner condition=loadingNotifications containerClass="spinner-container"}}
|
|
||||||
{{#if notifications}}
|
|
||||||
<hr>
|
|
||||||
<ul>
|
|
||||||
{{#each notifications as |n|}}
|
|
||||||
{{notification-item notification=n}}
|
|
||||||
{{/each}}
|
|
||||||
<li class="read last heading">
|
|
||||||
{{#d-link path=notificationsPath}}
|
|
||||||
{{i18n 'notifications.more'}}…
|
|
||||||
{{/d-link}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{{/if}}
|
|
||||||
{{/conditional-loading-spinner}}
|
|
||||||
</div>
|
|
||||||
{{plugin-outlet "user-menu-bottom"}}
|
|
||||||
<div class='logout-link'>
|
|
||||||
<hr>
|
|
||||||
<ul class='menu-links'>
|
|
||||||
<li>{{d-link action="logout" class="logout" icon="sign-out" label="user.log_out"}}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{{/menu-panel}}
|
|
|
@ -2,5 +2,5 @@
|
||||||
<h3>{{i18n 'composer.similar_topics'}}</h3>
|
<h3>{{i18n 'composer.similar_topics'}}</h3>
|
||||||
|
|
||||||
<ul class='topics'>
|
<ul class='topics'>
|
||||||
{{search-result-topic results=similarTopics}}
|
{{mount-widget widget="search-result-topic" args=(as-hash results=similarTopics)}}
|
||||||
</ul>
|
</ul>
|
|
@ -1,66 +0,0 @@
|
||||||
<div class='wrap'>
|
|
||||||
<div class='contents clearfix'>
|
|
||||||
{{home-logo minimized=showExtraInfo}}
|
|
||||||
{{plugin-outlet "header-after-home-logo"}}
|
|
||||||
|
|
||||||
<div class='panel clearfix'>
|
|
||||||
{{#unless currentUser}}
|
|
||||||
{{#if showSignUpButton}}
|
|
||||||
{{d-button action="showCreateAccount" class="btn-primary btn-small sign-up-button" label="sign_up"}}
|
|
||||||
{{/if}}
|
|
||||||
{{d-button action="showLogin" class="btn-primary btn-small login-button" icon="user" label="log_in"}}
|
|
||||||
{{/unless}}
|
|
||||||
<ul class='icons clearfix' role='navigation'>
|
|
||||||
{{#if currentUser}}
|
|
||||||
{{plugin-outlet "header-before-notifications"}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#header-dropdown iconId="search-button"
|
|
||||||
icon="search"
|
|
||||||
action="toggleSearch"
|
|
||||||
toggleVisible=searchVisible
|
|
||||||
mobileAction="fullPageSearch"
|
|
||||||
loginAction="showLogin"
|
|
||||||
title="search.title"
|
|
||||||
path="/search"}}
|
|
||||||
{{/header-dropdown}}
|
|
||||||
|
|
||||||
{{#header-dropdown iconId="toggle-hamburger-menu"
|
|
||||||
icon="bars"
|
|
||||||
toggleVisible=hamburgerVisible
|
|
||||||
loginAction="showLogin"
|
|
||||||
title="hamburger_menu"}}
|
|
||||||
{{#if flaggedPostsCount}}
|
|
||||||
<a href='/admin/flags/active' title='{{i18n 'notifications.total_flagged'}}' class='badge-notification flagged-posts'>{{flaggedPostsCount}}</a>
|
|
||||||
{{/if}}
|
|
||||||
{{/header-dropdown}}
|
|
||||||
|
|
||||||
{{#if currentUser}}
|
|
||||||
{{#header-dropdown iconId="current-user"
|
|
||||||
class="current-user"
|
|
||||||
showUser="true"
|
|
||||||
toggleVisible=userMenuVisible
|
|
||||||
loginAction="showLogin"
|
|
||||||
title="user.avatar.header_title"}}
|
|
||||||
{{#if currentUser.unread_notifications}}
|
|
||||||
<a href {{action "showUserMenu"}} class='badge-notification unread-notifications'>{{currentUser.unread_notifications}}</a>
|
|
||||||
{{/if}}
|
|
||||||
{{#if currentUser.unread_private_messages}}
|
|
||||||
<a href {{action "showUserMenu"}} class='badge-notification unread-private-messages'>{{currentUser.unread_private_messages}}</a>
|
|
||||||
{{/if}}
|
|
||||||
{{plugin-outlet "header-notifications"}}
|
|
||||||
{{/header-dropdown}}
|
|
||||||
{{/if}}
|
|
||||||
</ul>
|
|
||||||
{{plugin-outlet "header-before-dropdowns"}}
|
|
||||||
{{user-menu visible=userMenuVisible logoutAction="logout"}}
|
|
||||||
{{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}}
|
|
||||||
{{search-menu visible=searchVisible}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if showExtraInfo}}
|
|
||||||
{{header-extra-info topic=topic}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{plugin-outlet "header-under-content"}}
|
|
|
@ -14,14 +14,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#each n in model}}
|
{{user-notifications-large notifications=model}}
|
||||||
<div {{bind-attr class=":item :notification n.read::unread"}}>
|
|
||||||
{{notification-item notification=n}}
|
|
||||||
<span class="time">
|
|
||||||
{{format-date n.created_at leaveAgo="true"}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
{{#conditional-loading-spinner condition=loading}}
|
{{#conditional-loading-spinner condition=loading}}
|
||||||
{{#unless model.canLoadMore}}
|
{{#unless model.canLoadMore}}
|
||||||
|
|
|
@ -23,5 +23,7 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class='user-right'>
|
<section class='user-right'>
|
||||||
|
{{#load-more class="notification-history user-stream" selector=".user-stream .notification" action="loadMore"}}
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
|
{{/load-more}}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import afterTransition from 'discourse/lib/after-transition';
|
import afterTransition from 'discourse/lib/after-transition';
|
||||||
import positioningWorkaround from 'discourse/lib/safari-hacks';
|
import positioningWorkaround from 'discourse/lib/safari-hacks';
|
||||||
import { headerHeight } from 'discourse/views/header';
|
import { headerHeight } from 'discourse/components/site-header';
|
||||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
import Composer from 'discourse/models/composer';
|
import Composer from 'discourse/models/composer';
|
||||||
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { on } from 'ember-addons/ember-computed-decorators';
|
|
||||||
|
|
||||||
export default Ember.View.extend({
|
|
||||||
tagName: 'header',
|
|
||||||
classNames: ['d-header', 'clearfix'],
|
|
||||||
classNameBindings: ['editingTopic'],
|
|
||||||
templateName: 'header',
|
|
||||||
|
|
||||||
examineDockHeader() {
|
|
||||||
// Check the dock after the current run loop. While rendering,
|
|
||||||
// it's much slower to calculate `outlet.offset()`
|
|
||||||
Ember.run.next(() => {
|
|
||||||
if (this.docAt === undefined) {
|
|
||||||
const outlet = $('#main-outlet');
|
|
||||||
if (!(outlet && outlet.length === 1)) return;
|
|
||||||
this.docAt = outlet.offset().top;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = window.pageYOffset || $('html').scrollTop();
|
|
||||||
if (offset >= this.docAt) {
|
|
||||||
if (!this.dockedHeader) {
|
|
||||||
$('body').addClass('docked');
|
|
||||||
this.dockedHeader = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.dockedHeader) {
|
|
||||||
$('body').removeClass('docked');
|
|
||||||
this.dockedHeader = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@on('willDestroyElement')
|
|
||||||
_tearDown() {
|
|
||||||
$(window).unbind('scroll.discourse-dock');
|
|
||||||
$(document).unbind('touchmove.discourse-dock');
|
|
||||||
this.$('a.unread-private-messages, a.unread-notifications, a[data-notifications]').off('click.notifications');
|
|
||||||
$('body').off('keydown.header');
|
|
||||||
},
|
|
||||||
|
|
||||||
@on('didInsertElement')
|
|
||||||
_setup() {
|
|
||||||
$(window).bind('scroll.discourse-dock', () => this.examineDockHeader());
|
|
||||||
$(document).bind('touchmove.discourse-dock', () => this.examineDockHeader());
|
|
||||||
this.examineDockHeader();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export function headerHeight() {
|
|
||||||
const $header = $('header.d-header');
|
|
||||||
const headerOffset = $header.offset();
|
|
||||||
const headerOffsetTop = (headerOffset) ? headerOffset.top : 0;
|
|
||||||
return parseInt($header.outerHeight() + headerOffsetTop - $(window).scrollTop());
|
|
||||||
}
|
|
|
@ -23,6 +23,8 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
|
||||||
postStream: Em.computed.alias('topic.postStream'),
|
postStream: Em.computed.alias('topic.postStream'),
|
||||||
archetype: Em.computed.alias('topic.archetype'),
|
archetype: Em.computed.alias('topic.archetype'),
|
||||||
|
|
||||||
|
_lastShowTopic: null,
|
||||||
|
|
||||||
_composeChanged: function() {
|
_composeChanged: function() {
|
||||||
const composerController = Discourse.get('router.composerController');
|
const composerController = Discourse.get('router.composerController');
|
||||||
composerController.clearState();
|
composerController.clearState();
|
||||||
|
@ -73,7 +75,7 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
|
||||||
this.resetExamineDockCache();
|
this.resetExamineDockCache();
|
||||||
|
|
||||||
// this happens after route exit, stuff could have trickled in
|
// this happens after route exit, stuff could have trickled in
|
||||||
this.set('controller.controllers.header.showExtraInfo', false);
|
this.appEvents.trigger('header:hide-topic');
|
||||||
|
|
||||||
}.on('willDestroyElement'),
|
}.on('willDestroyElement'),
|
||||||
|
|
||||||
|
@ -90,6 +92,14 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
|
||||||
offset: 0,
|
offset: 0,
|
||||||
hasScrolled: Em.computed.gt("offset", 0),
|
hasScrolled: Em.computed.gt("offset", 0),
|
||||||
|
|
||||||
|
showTopicInHeader(topic, offset) {
|
||||||
|
if (this.get('docAt')) {
|
||||||
|
return offset >= this.get('docAt') || topic.get('postStream.firstPostNotLoaded');
|
||||||
|
} else {
|
||||||
|
return topic.get('postStream.firstPostNotLoaded');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// The user has scrolled the window, or it is finished rendering and ready for processing.
|
// The user has scrolled the window, or it is finished rendering and ready for processing.
|
||||||
scrolled() {
|
scrolled() {
|
||||||
if (this.isDestroyed || this.isDestroying || this._state !== 'inDOM') {
|
if (this.isDestroyed || this.isDestroying || this._state !== 'inDOM') {
|
||||||
|
@ -106,12 +116,16 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
|
||||||
|
|
||||||
this.set("offset", offset);
|
this.set("offset", offset);
|
||||||
|
|
||||||
const headerController = this.get('controller.controllers.header'),
|
const topic = this.get('controller.model');
|
||||||
topic = this.get('controller.model');
|
const showTopic = this.showTopicInHeader(topic, offset);
|
||||||
if (this.get('docAt')) {
|
if (showTopic !== this._lastShowTopic) {
|
||||||
headerController.set('showExtraInfo', offset >= this.get('docAt') || topic.get('postStream.firstPostNotLoaded'));
|
this._lastShowTopic = showTopic;
|
||||||
|
|
||||||
|
if (showTopic) {
|
||||||
|
this.appEvents.trigger('header:show-topic', topic);
|
||||||
} else {
|
} else {
|
||||||
headerController.set('showExtraInfo', topic.get('postStream.firstPostNotLoaded'));
|
this.appEvents.trigger('header:hide-topic');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger a scrolled event
|
// Trigger a scrolled event
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import LoadMore from "discourse/mixins/load-more";
|
|
||||||
|
|
||||||
export default Ember.View.extend(LoadMore, {
|
|
||||||
eyelineSelector: '.user-stream .notification',
|
|
||||||
classNames: ['user-stream', 'notification-history']
|
|
||||||
});
|
|
|
@ -61,6 +61,7 @@ createWidget('action-link', {
|
||||||
|
|
||||||
createWidget('actions-summary-item', {
|
createWidget('actions-summary-item', {
|
||||||
tagName: 'div.post-action',
|
tagName: 'div.post-action',
|
||||||
|
buildKey: (attrs) => `actions-summary-item-${attrs.id}`,
|
||||||
|
|
||||||
defaultState() {
|
defaultState() {
|
||||||
return { users: [] };
|
return { users: [] };
|
||||||
|
|
|
@ -14,6 +14,8 @@ export default createWidget('button', {
|
||||||
let title;
|
let title;
|
||||||
if (attrs.title) {
|
if (attrs.title) {
|
||||||
title = I18n.t(attrs.title, attrs.titleOptions);
|
title = I18n.t(attrs.title, attrs.titleOptions);
|
||||||
|
} else if (attrs.label) {
|
||||||
|
title = I18n.t(attrs.label, attrs.labelOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributes = { "aria-label": title, title };
|
const attributes = { "aria-label": title, title };
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
/*eslint no-loop-func:0*/
|
|
||||||
|
|
||||||
const CLICK_ATTRIBUTE_NAME = '_discourse_click_widget';
|
|
||||||
const CLICK_OUTSIDE_ATTRIBUTE_NAME = '_discourse_click_outside_widget';
|
|
||||||
|
|
||||||
export class WidgetClickHook {
|
|
||||||
constructor(widget) {
|
|
||||||
this.widget = widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
hook(node) {
|
|
||||||
node[CLICK_ATTRIBUTE_NAME] = this.widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
unhook(node) {
|
|
||||||
node[CLICK_ATTRIBUTE_NAME] = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class WidgetClickOutsideHook {
|
|
||||||
constructor(widget) {
|
|
||||||
this.widget = widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
hook(node) {
|
|
||||||
node.setAttribute('data-click-outside', true);
|
|
||||||
node[CLICK_OUTSIDE_ATTRIBUTE_NAME] = this.widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
unhook(node) {
|
|
||||||
node.removeAttribute('data-click-outside');
|
|
||||||
node[CLICK_OUTSIDE_ATTRIBUTE_NAME] = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let _watchingDocument = false;
|
|
||||||
WidgetClickHook.setupDocumentCallback = function() {
|
|
||||||
if (_watchingDocument) { return; }
|
|
||||||
|
|
||||||
$(document).on('click.discourse-widget', e => {
|
|
||||||
let node = e.target;
|
|
||||||
while (node) {
|
|
||||||
const widget = node[CLICK_ATTRIBUTE_NAME];
|
|
||||||
if (widget) {
|
|
||||||
widget.rerenderResult(() => widget.click(e));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
node = node.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
node = e.target;
|
|
||||||
const $outside = $('[data-click-outside]');
|
|
||||||
$outside.each((i, outNode) => {
|
|
||||||
if (outNode.contains(node)) { return; }
|
|
||||||
const widget = outNode[CLICK_OUTSIDE_ATTRIBUTE_NAME];
|
|
||||||
if (widget) {
|
|
||||||
widget.clickOutside(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
_watchingDocument = true;
|
|
||||||
};
|
|
|
@ -5,7 +5,7 @@ createWidget('hamburger-category', {
|
||||||
tagName: 'li.category-link',
|
tagName: 'li.category-link',
|
||||||
|
|
||||||
html(c) {
|
html(c) {
|
||||||
const results = [ this.attach('category_link', { category: c, allowUncategorized: true }) ];
|
const results = [ this.attach('category-link', { category: c, allowUncategorized: true }) ];
|
||||||
|
|
||||||
const unreadTotal = parseInt(c.get('unreadTopics'), 10) + parseInt(c.get('newTopics'), 10);
|
const unreadTotal = parseInt(c.get('unreadTopics'), 10) + parseInt(c.get('newTopics'), 10);
|
||||||
if (unreadTotal) {
|
if (unreadTotal) {
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
|
||||||
|
export default createWidget('hamburger-menu', {
|
||||||
|
tagName: 'div.hamburger-panel',
|
||||||
|
|
||||||
|
faqLink(href) {
|
||||||
|
return h('a.faq-priority', { attributes: { href } }, [
|
||||||
|
I18n.t('faq'),
|
||||||
|
' ',
|
||||||
|
h('span.badge.badge-notification', I18n.t('new_item'))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
adminLinks() {
|
||||||
|
const { currentUser } = this;
|
||||||
|
|
||||||
|
const links = [{ route: 'admin', className: 'admin-link', icon: 'wrench', label: 'admin_title' },
|
||||||
|
{ route: 'adminFlags',
|
||||||
|
className: 'flagged-posts-link',
|
||||||
|
icon: 'flag',
|
||||||
|
label: 'flags_title',
|
||||||
|
badgeClass: 'flagged-posts',
|
||||||
|
badgeTitle: 'notifications.total_flagged',
|
||||||
|
badgeCount: 'site_flagged_posts_count' }];
|
||||||
|
|
||||||
|
if (currentUser.show_queued_posts) {
|
||||||
|
links.push({ route: 'queued-posts',
|
||||||
|
className: 'queued-posts-link',
|
||||||
|
label: 'queue.title',
|
||||||
|
badgeCount: 'post_queue_new_count',
|
||||||
|
badgeClass: 'queued-posts' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser.admin) {
|
||||||
|
links.push({ route: 'adminSiteSettings',
|
||||||
|
icon: 'gear',
|
||||||
|
label: 'admin.site_settings.title',
|
||||||
|
className: 'settings-link' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.map(l => this.attach('link', l));
|
||||||
|
},
|
||||||
|
|
||||||
|
lookupCount(type) {
|
||||||
|
const tts = this.container.lookup('topic-tracking-state:main');
|
||||||
|
return tts ? tts.lookupCount(type) : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
showUserDirectory() {
|
||||||
|
if (!this.siteSettings.enable_user_directory) return false;
|
||||||
|
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
generalLinks() {
|
||||||
|
const { siteSettings } = this;
|
||||||
|
const links = [];
|
||||||
|
|
||||||
|
links.push({ route: 'discovery.latest', className: 'latest-topics-link', label: 'filters.latest.title' });
|
||||||
|
|
||||||
|
if (this.currentUser) {
|
||||||
|
links.push({ route: 'discovery.new',
|
||||||
|
className: 'new-topics-link',
|
||||||
|
labelCount: 'filters.new.title_with_count',
|
||||||
|
label: 'filters.new.title',
|
||||||
|
count: this.lookupCount('new') });
|
||||||
|
|
||||||
|
links.push({ route: 'discovery.unread',
|
||||||
|
className: 'unread-topics-link',
|
||||||
|
labelCount: 'filters.unread.title_with_count',
|
||||||
|
label: 'filters.unread.title',
|
||||||
|
count: this.lookupCount('unread') });
|
||||||
|
}
|
||||||
|
|
||||||
|
links.push({ route: 'discovery.top', className: 'top-topics-link', label: 'filters.top.title' });
|
||||||
|
|
||||||
|
if (siteSettings.enable_badges) {
|
||||||
|
links.push({ route: 'badges', className: 'badge-link', label: 'badges.title' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showUserDirectory()) {
|
||||||
|
links.push({ route: 'users', className: 'user-directory-link', label: 'directory.title' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.map(l => this.attach('link', l));
|
||||||
|
},
|
||||||
|
|
||||||
|
listCategories() {
|
||||||
|
const hideUncategorized = !this.siteSettings.allow_uncategorized_topics;
|
||||||
|
const showSubcatList = this.siteSettings.show_subcategory_list;
|
||||||
|
const isStaff = Discourse.User.currentProp('staff');
|
||||||
|
|
||||||
|
const categories = Discourse.Category.list().reject((c) => {
|
||||||
|
if (showSubcatList && c.get('parent_category_id')) { return true; }
|
||||||
|
if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; }
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.attach('hamburger-categories', { categories });
|
||||||
|
},
|
||||||
|
|
||||||
|
footerLinks(prioritizeFaq, faqUrl) {
|
||||||
|
const links = [];
|
||||||
|
links.push({ route: 'about', className: 'about-link', label: 'about.simple_title' });
|
||||||
|
|
||||||
|
if (!prioritizeFaq) {
|
||||||
|
links.push({ href: faqUrl, className: 'faq-link', label: 'faq' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { site } = this;
|
||||||
|
if (!site.mobileView && !this.capabilities.touch) {
|
||||||
|
links.push({ action: 'showKeyboard', className: 'keyboard-shortcuts-link', label: 'keyboard_shortcuts_help.title' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch)) {
|
||||||
|
links.push({ action: 'toggleMobileView',
|
||||||
|
className: 'mobile-toggle-link',
|
||||||
|
label: this.site.mobileView ? "desktop_view" : "mobile_view" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.map(l => this.attach('link', l));
|
||||||
|
},
|
||||||
|
|
||||||
|
panelContents() {
|
||||||
|
const { currentUser } = this;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
let faqUrl = this.siteSettings.faq_url;
|
||||||
|
if (!faqUrl || faqUrl.length === 0) {
|
||||||
|
faqUrl = Discourse.getURL('/faq');
|
||||||
|
}
|
||||||
|
|
||||||
|
const prioritizeFaq = this.currentUser && !this.currentUser.read_faq;
|
||||||
|
if (prioritizeFaq) {
|
||||||
|
results.push(this.attach('menu-links', { heading: true, contents: () => this.faqLink(faqUrl) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser && currentUser.staff) {
|
||||||
|
results.push(this.attach('menu-links', { contents: () => this.adminLinks() }));
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(this.attach('menu-links', { contents: () => this.generalLinks() }));
|
||||||
|
results.push(this.listCategories());
|
||||||
|
results.push(h('hr'));
|
||||||
|
results.push(this.attach('menu-links', { omitRule: true, contents: () => this.footerLinks(prioritizeFaq, faqUrl) }));
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
html() {
|
||||||
|
return this.attach('menu-panel', { contents: () => this.panelContents() });
|
||||||
|
},
|
||||||
|
|
||||||
|
clickOutside() {
|
||||||
|
this.sendWidgetAction('toggleHamburger');
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
|
||||||
|
export default createWidget('header-topic-info', {
|
||||||
|
tagName: 'div.extra-info-wrapper',
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
const topic = attrs.topic;
|
||||||
|
|
||||||
|
const heading = [];
|
||||||
|
|
||||||
|
const showPM = !topic.get('is_warning') && topic.get('isPrivateMessage');
|
||||||
|
if (showPM) {
|
||||||
|
const href = this.currentUser && this.currentUser.pmPath(topic);
|
||||||
|
if (href) {
|
||||||
|
heading.push(h('a', { attributes: { href } },
|
||||||
|
h('span.private-message-glyph', iconNode('envelope'))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loaded = topic.get('details.loaded');
|
||||||
|
|
||||||
|
if (loaded) {
|
||||||
|
heading.push(this.attach('topic-status', attrs));
|
||||||
|
heading.push(this.attach('link', { className: 'topic-link',
|
||||||
|
action: 'jumpToTopPost',
|
||||||
|
href: topic.get('url'),
|
||||||
|
contents: () => topic.get('fancyTitle') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = [h('h1', heading)];
|
||||||
|
if (loaded) {
|
||||||
|
const category = topic.get('category');
|
||||||
|
if (category && (!category.get('isUncategorizedCategory') || !this.siteSettings.suppress_uncategorized_badge)) {
|
||||||
|
const parentCategory = category.get('parentCategory');
|
||||||
|
if (parentCategory) {
|
||||||
|
title.push(this.attach('category-link', { category: parentCategory }));
|
||||||
|
}
|
||||||
|
title.push(this.attach('category-link', { category }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contents = h('div.title-wrapper', title);
|
||||||
|
return h('div.extra-info', { className: title.length > 1 ? 'two-rows' : '' }, contents);
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpToTopPost() {
|
||||||
|
const topic = this.attrs.topic;
|
||||||
|
if (topic) {
|
||||||
|
DiscourseURL.routeTo(topic.get('firstPostUrl'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,278 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||||
|
import { avatarImg } from 'discourse/widgets/post';
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
|
||||||
|
const dropdown = {
|
||||||
|
buildClasses(attrs) {
|
||||||
|
if (attrs.active) { return "active"; }
|
||||||
|
},
|
||||||
|
|
||||||
|
click(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.attrs.active) {
|
||||||
|
this.sendWidgetAction(this.attrs.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createWidget('header-notifications', {
|
||||||
|
html(attrs) {
|
||||||
|
const { currentUser } = this;
|
||||||
|
|
||||||
|
const contents = [ avatarImg('medium', { template: currentUser.get('avatar_template'),
|
||||||
|
username: currentUser.get('username') }) ];
|
||||||
|
|
||||||
|
const unreadNotifications = currentUser.get('unread_notifications');
|
||||||
|
if (!!unreadNotifications) {
|
||||||
|
contents.push(this.attach('link', { action: attrs.action,
|
||||||
|
className: 'badge-notification unread-notifications',
|
||||||
|
rawLabel: unreadNotifications }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreadPMs = currentUser.get('unread_private_messages');
|
||||||
|
if (!!unreadPMs) {
|
||||||
|
contents.push(this.attach('link', { action: attrs.action,
|
||||||
|
className: 'badge-notification unread-private-messages',
|
||||||
|
rawLabel: unreadPMs }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createWidget('user-dropdown', jQuery.extend(dropdown, {
|
||||||
|
tagName: 'li.header-dropdown-toggle.current-user',
|
||||||
|
|
||||||
|
buildId() {
|
||||||
|
return 'current-user';
|
||||||
|
},
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
const { currentUser } = this;
|
||||||
|
|
||||||
|
return h('a.icon', { attributes: { href: currentUser.get('path'), 'data-auto-route': true } },
|
||||||
|
this.attach('header-notifications', attrs));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
createWidget('header-dropdown', jQuery.extend(dropdown, {
|
||||||
|
tagName: 'li.header-dropdown-toggle',
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
const title = I18n.t(attrs.title);
|
||||||
|
|
||||||
|
const body = [iconNode(attrs.icon)];
|
||||||
|
if (attrs.contents) {
|
||||||
|
body.push(attrs.contents.call(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('a.icon', { attributes: { href: '',
|
||||||
|
'data-auto-route': true,
|
||||||
|
title,
|
||||||
|
'aria-label': title,
|
||||||
|
id: attrs.iconId } }, body);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
createWidget('header-icons', {
|
||||||
|
tagName: 'ul.icons.clearfix',
|
||||||
|
|
||||||
|
buildAttributes() {
|
||||||
|
return { role: 'navigation' };
|
||||||
|
},
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
const hamburger = this.attach('header-dropdown', {
|
||||||
|
title: 'hamburger_menu',
|
||||||
|
icon: 'bars',
|
||||||
|
iconId: 'toggle-hamburger-menu',
|
||||||
|
active: attrs.hamburgerVisible,
|
||||||
|
action: 'toggleHamburger',
|
||||||
|
contents() {
|
||||||
|
if (!attrs.flagCount) { return; }
|
||||||
|
return this.attach('link', {
|
||||||
|
href: '/admin/flags/active',
|
||||||
|
title: 'notifications.total_flagged',
|
||||||
|
rawLabel: attrs.flagCount,
|
||||||
|
className: 'badge-notification flagged-posts'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = this.attach('header-dropdown', {
|
||||||
|
title: 'search.title',
|
||||||
|
icon: 'search',
|
||||||
|
iconId: 'search-button',
|
||||||
|
action: 'toggleSearchMenu',
|
||||||
|
active: attrs.searchVisible,
|
||||||
|
href: '/search'
|
||||||
|
});
|
||||||
|
|
||||||
|
const icons = [search, hamburger];
|
||||||
|
if (this.currentUser) {
|
||||||
|
icons.push(this.attach('user-dropdown', { active: attrs.userVisible,
|
||||||
|
action: 'toggleUserMenu' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
createWidget('header-buttons', {
|
||||||
|
tagName: 'span',
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
if (this.currentUser) { return; }
|
||||||
|
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
if (attrs.canSignUp && !attrs.topic) {
|
||||||
|
buttons.push(this.attach('button', { label: "sign_up",
|
||||||
|
className: 'btn-primary btn-small sign-up-button',
|
||||||
|
action: "showCreateAccount" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
buttons.push(this.attach('button', { label: 'log_in',
|
||||||
|
className: 'btn-primary btn-small login-button',
|
||||||
|
action: 'showLogin',
|
||||||
|
icon: 'user' }));
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createWidget('header', {
|
||||||
|
tagName: 'header.d-header.clearfix',
|
||||||
|
buildKey: () => `header`,
|
||||||
|
|
||||||
|
defaultState() {
|
||||||
|
return { searchVisible: false,
|
||||||
|
hamburgerVisible: false,
|
||||||
|
userVisible: false,
|
||||||
|
contextEnabled: false };
|
||||||
|
},
|
||||||
|
|
||||||
|
html(attrs, state) {
|
||||||
|
const panels = [this.attach('header-buttons', attrs),
|
||||||
|
this.attach('header-icons', { hamburgerVisible: state.hamburgerVisible,
|
||||||
|
userVisible: state.userVisible,
|
||||||
|
searchVisible: state.searchVisible,
|
||||||
|
flagCount: attrs.flagCount })];
|
||||||
|
|
||||||
|
if (state.searchVisible) {
|
||||||
|
panels.push(this.attach('search-menu', { contextEnabled: state.contextEnabled }));
|
||||||
|
} else if (state.hamburgerVisible) {
|
||||||
|
panels.push(this.attach('hamburger-menu'));
|
||||||
|
} else if (state.userVisible) {
|
||||||
|
panels.push(this.attach('user-menu'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contents = [ this.attach('home-logo', { minimized: !!attrs.topic }),
|
||||||
|
h('div.panel.clearfix', panels) ];
|
||||||
|
|
||||||
|
if (attrs.topic) {
|
||||||
|
contents.push(this.attach('header-topic-info', attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div.wrap', h('div.contents.clearfix', contents));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHighlight() {
|
||||||
|
if (!this.state.searchVisible) {
|
||||||
|
const service = this.container.lookup('search-service:main');
|
||||||
|
service.set('highlightTerm', '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
linkClickedEvent() {
|
||||||
|
this.state.userVisible = false;
|
||||||
|
this.state.hamburgerVisible = false;
|
||||||
|
this.state.searchVisible = false;
|
||||||
|
this.updateHighlight();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSearchMenu() {
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
const searchService = this.container.lookup('search-service:main');
|
||||||
|
const context = searchService.get('searchContext');
|
||||||
|
var params = "";
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
params = `?context=${context.type}&context_id=${context.id}&skip_context=true`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiscourseURL.routeTo('/search' + params);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.searchVisible = !this.state.searchVisible;
|
||||||
|
this.updateHighlight();
|
||||||
|
Ember.run.next(() => $('#search-term').focus());
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleUserMenu() {
|
||||||
|
this.state.userVisible = !this.state.userVisible;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleHamburger() {
|
||||||
|
this.state.hamburgerVisible = !this.state.hamburgerVisible;
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePageSearch() {
|
||||||
|
const { state } = this;
|
||||||
|
|
||||||
|
if (state.searchVisible) {
|
||||||
|
this.toggleSearchMenu();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.contextEnabled = false;
|
||||||
|
|
||||||
|
const currentPath = this.container.lookup('controller:application').get('currentPath');
|
||||||
|
const blacklist = [ /^discovery\.categories/ ];
|
||||||
|
const whitelist = [ /^topic\./ ];
|
||||||
|
const check = function(regex) { return !!currentPath.match(regex); };
|
||||||
|
let showSearch = whitelist.any(check) && !blacklist.any(check);
|
||||||
|
|
||||||
|
// If we're viewing a topic, only intercept search if there are cloaked posts
|
||||||
|
if (showSearch && currentPath.match(/^topic\./)) {
|
||||||
|
showSearch = ($('.topic-post .cooked, .small-action:not(.time-gap)').length <
|
||||||
|
this.container.lookup('controller:topic').get('model.postStream.stream.length'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSearch) {
|
||||||
|
state.contextEnabled = true;
|
||||||
|
this.toggleSearchMenu();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
searchMenuContextChanged(value) {
|
||||||
|
this.state.contextEnabled = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
headerKeyboardTrigger(msg) {
|
||||||
|
switch(msg.type) {
|
||||||
|
case 'search':
|
||||||
|
this.toggleSearchMenu();
|
||||||
|
break;
|
||||||
|
case 'user':
|
||||||
|
this.toggleUserMenu();
|
||||||
|
break;
|
||||||
|
case 'hamburger':
|
||||||
|
this.toggleHamburger();
|
||||||
|
break;
|
||||||
|
case 'page-search':
|
||||||
|
if (!this.togglePageSearch()) {
|
||||||
|
msg.event.preventDefault();
|
||||||
|
msg.event.stopPropagation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||||
|
|
||||||
|
export default createWidget('home-logo', {
|
||||||
|
tagName: 'div.title',
|
||||||
|
|
||||||
|
logo() {
|
||||||
|
const { siteSettings } = this;
|
||||||
|
const mobileView = this.site.mobileView;
|
||||||
|
|
||||||
|
const mobileLogoUrl = siteSettings.mobile_logo_url || "";
|
||||||
|
const showMobileLogo = mobileView && (mobileLogoUrl.length > 0);
|
||||||
|
|
||||||
|
const logoUrl = siteSettings.logo_url || '';
|
||||||
|
const title = siteSettings.title;
|
||||||
|
|
||||||
|
if (!mobileView && this.attrs.minimized) {
|
||||||
|
const logoSmallUrl = siteSettings.logo_small_url || '';
|
||||||
|
if (logoSmallUrl.length) {
|
||||||
|
return h('img#site-logo.logo-small', { attributes: { src: logoSmallUrl, width: 33, height: 33, alt: title } });
|
||||||
|
} else {
|
||||||
|
return iconNode('home');
|
||||||
|
}
|
||||||
|
} else if (showMobileLogo) {
|
||||||
|
return h('img#site-logo.logo-big', { attributes: { src: mobileLogoUrl, alt: title } });
|
||||||
|
} else if (logoUrl.length) {
|
||||||
|
return h('img#site-logo.logo-big', { attributes: { src: logoUrl, alt: title } });
|
||||||
|
} else {
|
||||||
|
return h('h2#site-text-logo.text-logo', title);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
html() {
|
||||||
|
return h('a', { attributes: { href: "/", 'data-auto-route': true } }, this.logo());
|
||||||
|
},
|
||||||
|
|
||||||
|
click(e) {
|
||||||
|
// if they want to open in a new tab, let it so
|
||||||
|
if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) { return true; }
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
DiscourseURL.routeTo("/");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*eslint no-loop-func:0*/
|
||||||
|
|
||||||
|
const CLICK_ATTRIBUTE_NAME = '_discourse_click_widget';
|
||||||
|
const CLICK_OUTSIDE_ATTRIBUTE_NAME = '_discourse_click_outside_widget';
|
||||||
|
const KEY_UP_ATTRIBUTE_NAME = '_discourse_key_up_widget';
|
||||||
|
|
||||||
|
function buildHook(attributeName, setAttr) {
|
||||||
|
return class {
|
||||||
|
constructor(widget) {
|
||||||
|
this.widget = widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook(node) {
|
||||||
|
if (setAttr) {
|
||||||
|
node.setAttribute(setAttr, true);
|
||||||
|
}
|
||||||
|
node[attributeName] = this.widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
unhook(node) {
|
||||||
|
if (setAttr) {
|
||||||
|
node.removeAttribute(setAttr, true);
|
||||||
|
}
|
||||||
|
node[attributeName] = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WidgetClickHook = buildHook(CLICK_ATTRIBUTE_NAME);
|
||||||
|
export const WidgetClickOutsideHook = buildHook(CLICK_OUTSIDE_ATTRIBUTE_NAME, 'data-click-outside');
|
||||||
|
export const WidgetKeyUpHook = buildHook(KEY_UP_ATTRIBUTE_NAME);
|
||||||
|
|
||||||
|
function findNode(node, attrName, cb) {
|
||||||
|
while (node) {
|
||||||
|
const widget = node[attrName];
|
||||||
|
if (widget) {
|
||||||
|
widget.rerenderResult(() => cb(widget));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
node = node.parentNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _watchingDocument = false;
|
||||||
|
WidgetClickHook.setupDocumentCallback = function() {
|
||||||
|
if (_watchingDocument) { return; }
|
||||||
|
|
||||||
|
$(document).on('click.discourse-widget', e => {
|
||||||
|
findNode(e.target, CLICK_ATTRIBUTE_NAME, w => w.click(e));
|
||||||
|
|
||||||
|
let node = e.target;
|
||||||
|
const $outside = $('[data-click-outside]');
|
||||||
|
$outside.each((i, outNode) => {
|
||||||
|
if (outNode.contains(node)) { return; }
|
||||||
|
const widget = outNode[CLICK_OUTSIDE_ATTRIBUTE_NAME];
|
||||||
|
if (widget) {
|
||||||
|
widget.clickOutside(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('keyup.discourse-widget', e => {
|
||||||
|
findNode(e.target, KEY_UP_ATTRIBUTE_NAME, w => w.keyUp(e));
|
||||||
|
});
|
||||||
|
|
||||||
|
_watchingDocument = true;
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
|
||||||
|
export default createWidget('link', {
|
||||||
|
tagName: 'a',
|
||||||
|
|
||||||
|
href(attrs) {
|
||||||
|
const route = attrs.route;
|
||||||
|
if (route) {
|
||||||
|
const router = this.container.lookup('router:main');
|
||||||
|
if (router && router.router) {
|
||||||
|
const params = [route];
|
||||||
|
if (attrs.model) {
|
||||||
|
params.push(attrs.model);
|
||||||
|
}
|
||||||
|
return Discourse.getURL(router.router.generate.apply(router.router, params));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return attrs.href;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buildClasses(attrs) {
|
||||||
|
const result = [];
|
||||||
|
result.push('widget-link');
|
||||||
|
if (attrs.className) { result.push(attrs.className); };
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
buildAttributes(attrs) {
|
||||||
|
return { href: this.href(attrs), title: this.label(attrs) };
|
||||||
|
},
|
||||||
|
|
||||||
|
label(attrs) {
|
||||||
|
if (attrs.labelCount && attrs.count) {
|
||||||
|
return I18n.t(attrs.labelCount, { count: attrs.count });
|
||||||
|
}
|
||||||
|
return attrs.rawLabel || (attrs.label ? I18n.t(attrs.label) : '');
|
||||||
|
},
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
if (attrs.contents) {
|
||||||
|
return attrs.contents();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
if (attrs.icon) {
|
||||||
|
result.push(iconNode(attrs.icon));
|
||||||
|
result.push(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attrs.hideLabel) {
|
||||||
|
result.push(this.label(attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUser = this.currentUser;
|
||||||
|
if (currentUser && attrs.badgeCount) {
|
||||||
|
const val = parseInt(currentUser.get(attrs.badgeCount));
|
||||||
|
if (val > 0) {
|
||||||
|
const title = attrs.badgeTitle ? I18n.t(attrs.badgeTitle) : '';
|
||||||
|
result.push(' ');
|
||||||
|
result.push(h('span.badge-notification', { className: attrs.badgeClass,
|
||||||
|
attributes: { title } }, val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
click(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.attrs.action) {
|
||||||
|
e.preventDefault();
|
||||||
|
return this.sendWidgetAction(this.attrs.action, this.attrs.actionParam);
|
||||||
|
} else {
|
||||||
|
this.sendWidgetEvent('linkClicked');
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiscourseURL.routeTo(this.href(this.attrs));
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
|
||||||
|
createWidget('menu-links', {
|
||||||
|
html(attrs) {
|
||||||
|
const links = [].concat(attrs.contents());
|
||||||
|
const liOpts = { className: attrs.heading ? 'heading' : '' };
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
result.push(h('ul.menu-links.columned', links.map(l => h('li', liOpts, l))));
|
||||||
|
|
||||||
|
result.push(h('div.clearfix'));
|
||||||
|
if (!attrs.omitRule) {
|
||||||
|
result.push(h('hr'));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createWidget('menu-panel', {
|
||||||
|
tagName: 'div.menu-panel',
|
||||||
|
|
||||||
|
buildAttributes(attrs) {
|
||||||
|
if (attrs.maxWidth) {
|
||||||
|
return { 'data-max-width': attrs.maxWidth };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
return h('div.panel-body', h('div.panel-body-contents.clearfix', attrs.contents()));
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,109 @@
|
||||||
|
import RawHtml from 'discourse/widgets/raw-html';
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
|
||||||
|
const LIKED_TYPE = 5;
|
||||||
|
const INVITED_TYPE = 8;
|
||||||
|
const GROUP_SUMMARY_TYPE = 16;
|
||||||
|
|
||||||
|
createWidget('notification-item', {
|
||||||
|
tagName: 'li',
|
||||||
|
|
||||||
|
buildClasses(attrs) {
|
||||||
|
const classNames = [];
|
||||||
|
if (attrs.get('read')) { classNames.push('read'); }
|
||||||
|
if (attrs.is_warning) { classNames.push('is-warning'); }
|
||||||
|
return classNames;
|
||||||
|
},
|
||||||
|
|
||||||
|
url() {
|
||||||
|
const attrs = this.attrs;
|
||||||
|
const data = attrs.data;
|
||||||
|
|
||||||
|
const badgeId = data.badge_id;
|
||||||
|
if (badgeId) {
|
||||||
|
let badgeSlug = data.badge_slug;
|
||||||
|
|
||||||
|
if (!badgeSlug) {
|
||||||
|
const badgeName = data.badge_name;
|
||||||
|
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = data.username;
|
||||||
|
username = username ? "?username=" + username.toLowerCase() : "";
|
||||||
|
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username);
|
||||||
|
}
|
||||||
|
|
||||||
|
const topicId = attrs.topic_id;
|
||||||
|
if (topicId) {
|
||||||
|
return Discourse.Utilities.postUrl(attrs.slug, topicId, attrs.post_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrs.notification_type === INVITED_TYPE) {
|
||||||
|
return Discourse.getURL('/users/' + data.display_username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.group_id) {
|
||||||
|
return Discourse.getURL('/users/' + data.username + '/messages/group/' + data.group_name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
description() {
|
||||||
|
const data = this.attrs.data;
|
||||||
|
const badgeName = data.badge_name;
|
||||||
|
if (badgeName) { return Discourse.Utilities.escapeExpression(badgeName); }
|
||||||
|
|
||||||
|
const title = data.topic_title;
|
||||||
|
return Ember.isEmpty(title) ? "" : Discourse.Utilities.escapeExpression(title);
|
||||||
|
},
|
||||||
|
|
||||||
|
text() {
|
||||||
|
const attrs = this.attrs;
|
||||||
|
const data = attrs.data;
|
||||||
|
|
||||||
|
const notificationType = attrs.notification_type;
|
||||||
|
|
||||||
|
const lookup = this.site.get('notificationLookup');
|
||||||
|
const notName = lookup[notificationType];
|
||||||
|
const scope = (notName === 'custom') ? data.message : `notifications.${notName}`;
|
||||||
|
|
||||||
|
if (notificationType === GROUP_SUMMARY_TYPE) {
|
||||||
|
const count = data.inbox_count;
|
||||||
|
const group_name = data.group_name;
|
||||||
|
return I18n.t(scope, { count, group_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = data.display_username;
|
||||||
|
const description = this.description();
|
||||||
|
if (notificationType === LIKED_TYPE && data.count > 1) {
|
||||||
|
const count = data.count - 2;
|
||||||
|
const username2 = data.username2;
|
||||||
|
if (count===0) {
|
||||||
|
return I18n.t('notifications.liked_2', {description, username, username2});
|
||||||
|
} else {
|
||||||
|
return I18n.t('notifications.liked_many', {description, username, username2, count});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return I18n.t(scope, {description, username});
|
||||||
|
},
|
||||||
|
|
||||||
|
html() {
|
||||||
|
const contents = new RawHtml({ html: `<div>${Discourse.Emoji.unescape(this.text())}</div>` });
|
||||||
|
const url = this.url();
|
||||||
|
return url ? h('a', { attributes: { href: url, 'data-auto-route': true } }, contents) : contents;
|
||||||
|
},
|
||||||
|
|
||||||
|
click(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.attrs.set('read', true);
|
||||||
|
const id = this.attrs.id;
|
||||||
|
Discourse.setTransientHeader("Discourse-Clear-Notifications", id);
|
||||||
|
if (document && document.cookie) {
|
||||||
|
document.cookie = `cn=${id}; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
|
||||||
|
}
|
||||||
|
this.sendWidgetEvent('linkClicked');
|
||||||
|
DiscourseURL.routeTo(this.url());
|
||||||
|
}
|
||||||
|
});
|
|
@ -7,6 +7,7 @@ const MAX_GUTTER_LINKS = 5;
|
||||||
|
|
||||||
export default createWidget('post-gutter', {
|
export default createWidget('post-gutter', {
|
||||||
tagName: 'div.gutter',
|
tagName: 'div.gutter',
|
||||||
|
buildKey: (attrs) => `post-gutter-${attrs.id}`,
|
||||||
|
|
||||||
defaultState() {
|
defaultState() {
|
||||||
return { collapsed: true };
|
return { collapsed: true };
|
||||||
|
|
|
@ -51,6 +51,7 @@ createWidget('select-post', {
|
||||||
|
|
||||||
createWidget('reply-to-tab', {
|
createWidget('reply-to-tab', {
|
||||||
tagName: 'a.reply-to-tab',
|
tagName: 'a.reply-to-tab',
|
||||||
|
buildKey: attrs => `reply-to-tab-${attrs.id}`,
|
||||||
|
|
||||||
defaultState() {
|
defaultState() {
|
||||||
return { loading: false };
|
return { loading: false };
|
||||||
|
@ -61,7 +62,7 @@ createWidget('reply-to-tab', {
|
||||||
|
|
||||||
return [iconNode('mail-forward'),
|
return [iconNode('mail-forward'),
|
||||||
' ',
|
' ',
|
||||||
avatarImg.call(this,'small',{
|
avatarImg('small', {
|
||||||
template: attrs.replyToAvatarTemplate,
|
template: attrs.replyToAvatarTemplate,
|
||||||
username: attrs.replyToUsername
|
username: attrs.replyToUsername
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4,9 +4,13 @@ export default class RawHtml {
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
return $(this.html)[0];
|
const $html = $(this.html);
|
||||||
|
this.decorate($html);
|
||||||
|
return $html[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decorate() { }
|
||||||
|
|
||||||
update(prev) {
|
update(prev) {
|
||||||
if (prev.html === this.html) { return; }
|
if (prev.html === this.html) { return; }
|
||||||
return this.init();
|
return this.init();
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { searchContextDescription } from 'discourse/lib/search';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
|
||||||
|
createWidget('search-term', {
|
||||||
|
tagName: 'input',
|
||||||
|
buildId: () => 'search-term',
|
||||||
|
|
||||||
|
buildAttributes(attrs) {
|
||||||
|
return { type: 'text',
|
||||||
|
value: attrs.value || '',
|
||||||
|
placeholder: attrs.contextEnabled ? "" : I18n.t('search.title') };
|
||||||
|
},
|
||||||
|
|
||||||
|
keyUp(e) {
|
||||||
|
if (e.which === 13) {
|
||||||
|
return this.sendWidgetAction('fullSearch');
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = this.attrs.value;
|
||||||
|
const newVal = $(`#${this.buildId()}`).val();
|
||||||
|
|
||||||
|
if (newVal !== val) {
|
||||||
|
this.sendWidgetAction('searchTermChanged', newVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createWidget('search-context', {
|
||||||
|
tagName: 'div.search-context',
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
const service = this.container.lookup('search-service:main');
|
||||||
|
const ctx = service.get('searchContext');
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
if (ctx) {
|
||||||
|
const description = searchContextDescription(Ember.get(ctx, 'type'),
|
||||||
|
Ember.get(ctx, 'user.username') || Ember.get(ctx, 'category.name'));
|
||||||
|
result.push(h('label', [
|
||||||
|
h('input', { type: 'checkbox', checked: attrs.contextEnabled }),
|
||||||
|
' ',
|
||||||
|
description
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(this.attach('link', { action: 'showSearchHelp',
|
||||||
|
label: 'show_help',
|
||||||
|
className: 'show-help' }));
|
||||||
|
result.push(h('div.clearfix'));
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
click() {
|
||||||
|
const val = $('.search-context input').is(':checked');
|
||||||
|
if (val !== this.attrs.contextEnabled) {
|
||||||
|
this.sendWidgetAction('searchContextChanged', val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { avatarImg } from 'discourse/widgets/post';
|
||||||
|
import { dateNode } from 'discourse/helpers/node';
|
||||||
|
import RawHtml from 'discourse/widgets/raw-html';
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||||
|
|
||||||
|
class Highlighted extends RawHtml {
|
||||||
|
constructor(html, term) {
|
||||||
|
super({ html: `<span>${html}</span>` });
|
||||||
|
this.term = term;
|
||||||
|
}
|
||||||
|
|
||||||
|
decorate($html) {
|
||||||
|
$html.highlight(this.term.split(/\s+/), { className: 'search-highlight' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSearchResult(type, linkField, fn) {
|
||||||
|
return createWidget(`search-result-${type}`, {
|
||||||
|
html(attrs) {
|
||||||
|
return attrs.results.map(r => {
|
||||||
|
return h('li', this.attach('link', {
|
||||||
|
href: r.get(linkField),
|
||||||
|
contents: () => fn.call(this, r, attrs.term),
|
||||||
|
className: 'search-link'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function postResult(result, link, term) {
|
||||||
|
const html = [link];
|
||||||
|
|
||||||
|
if (!this.site.mobileView) {
|
||||||
|
html.push(h('span.blurb', [ dateNode(result.created_at),
|
||||||
|
' - ',
|
||||||
|
new Highlighted(result.blurb, term) ]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
createSearchResult('user', 'path', function(u) {
|
||||||
|
return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', u.username ];
|
||||||
|
});
|
||||||
|
|
||||||
|
createSearchResult('topic', 'url', function(result, term) {
|
||||||
|
const topic = result.topic;
|
||||||
|
const link = h('span.topic', [
|
||||||
|
this.attach('topic-status', { topic, disableActions: true }),
|
||||||
|
h('span.topic-title', new Highlighted(topic.get('fancyTitle'), term)),
|
||||||
|
this.attach('category-link', { category: topic.get('category'), link: false })
|
||||||
|
]);
|
||||||
|
|
||||||
|
return postResult.call(this, result, link, term);
|
||||||
|
});
|
||||||
|
|
||||||
|
createSearchResult('post', 'url', function(result, term) {
|
||||||
|
return postResult.call(this, result, I18n.t('search.post_format', result), term);
|
||||||
|
});
|
||||||
|
|
||||||
|
createSearchResult('category', 'url', function (c) {
|
||||||
|
return this.attach('category-link', { category: c, link: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
createWidget('search-menu-results', {
|
||||||
|
tagName: 'div.results',
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
if (attrs.noResults) {
|
||||||
|
return h('div.no-results', I18n.t('search.no_results'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = attrs.results;
|
||||||
|
const resultTypes = results.resultTypes || [];
|
||||||
|
return resultTypes.map(rt => {
|
||||||
|
const more = [];
|
||||||
|
|
||||||
|
const moreArgs = {
|
||||||
|
className: 'filter',
|
||||||
|
contents: () => [I18n.t('show_more'), ' ', iconNode('chevron-down')]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rt.moreUrl) {
|
||||||
|
more.push(this.attach('link', $.extend(moreArgs, { href: rt.moreUrl })));
|
||||||
|
} else if (rt.more) {
|
||||||
|
more.push(this.attach('link', $.extend(moreArgs, { action: "moreOfType",
|
||||||
|
actionParam: rt.type,
|
||||||
|
className: "filter filter-type"})));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
h('ul', this.attach(rt.componentName, { results: rt.results, term: attrs.term })),
|
||||||
|
h('div.no-results', more)
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,166 @@
|
||||||
|
import { searchForTerm, isValidSearchTerm } from 'discourse/lib/search';
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
|
||||||
|
// Helps with debouncing and cancelling promises
|
||||||
|
const SearchHelper = {
|
||||||
|
_activeSearch: null,
|
||||||
|
_cancelSearch: null,
|
||||||
|
|
||||||
|
// for cancelling debounced search
|
||||||
|
cancel() {
|
||||||
|
if (this._activeSearch) {
|
||||||
|
this._activeSearch.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._cancelSearch = true;
|
||||||
|
Ember.run.later(() => this._cancelSearch = false, 400);
|
||||||
|
},
|
||||||
|
|
||||||
|
perform(widget) {
|
||||||
|
if (this._cancelSearch){
|
||||||
|
this._cancelSearch = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._activeSearch) {
|
||||||
|
this._activeSearch.abort();
|
||||||
|
this._activeSearch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state } = widget;
|
||||||
|
const { term, typeFilter, contextEnabled } = state;
|
||||||
|
const searchContext = contextEnabled ? widget.searchContext() : null;
|
||||||
|
const fullSearchUrl = widget.fullSearchUrl();
|
||||||
|
|
||||||
|
this._activeSearch = searchForTerm(term, { typeFilter, searchContext, fullSearchUrl });
|
||||||
|
this._activeSearch.then(content => {
|
||||||
|
state.noResults = content.resultTypes.length === 0;
|
||||||
|
state.results = content;
|
||||||
|
}).finally(() => {
|
||||||
|
state.loading = false;
|
||||||
|
widget.scheduleRerender();
|
||||||
|
this._activeSearch = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createWidget('search-menu', {
|
||||||
|
tagName: 'div.search-menu',
|
||||||
|
buildKey: () => 'search-menu',
|
||||||
|
|
||||||
|
defaultState() {
|
||||||
|
return { loading: false,
|
||||||
|
results: {},
|
||||||
|
noResults: false,
|
||||||
|
term: null,
|
||||||
|
typeFilter: null };
|
||||||
|
},
|
||||||
|
|
||||||
|
fullSearchUrl() {
|
||||||
|
const state = this.state;
|
||||||
|
const contextEnabled = this.attrs.contextEnabled;
|
||||||
|
|
||||||
|
const ctx = contextEnabled ? this.searchContext() : null;
|
||||||
|
const type = Ember.get(ctx, 'type');
|
||||||
|
|
||||||
|
if (contextEnabled && type === 'topic') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = '/search?q=' + encodeURIComponent(state.term);
|
||||||
|
if (contextEnabled) {
|
||||||
|
if (ctx.id.toString().toLowerCase() === this.currentUser.username_lower &&
|
||||||
|
type === "private_messages") {
|
||||||
|
url += ' in:private';
|
||||||
|
} else {
|
||||||
|
url += encodeURIComponent(" " + type + ":" + ctx.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Discourse.getURL(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
panelContents() {
|
||||||
|
const { state } = this;
|
||||||
|
const contextEnabled = this.attrs.contextEnabled;
|
||||||
|
|
||||||
|
const results = [this.attach('search-term', { value: state.term, contextEnabled }),
|
||||||
|
this.attach('search-context', { contextEnabled })];
|
||||||
|
|
||||||
|
if (state.loading) {
|
||||||
|
results.push(h('div.searching', h('div.spinner')));
|
||||||
|
} else {
|
||||||
|
results.push(this.attach('search-menu-results', { term: state.term,
|
||||||
|
noResults: state.noResults,
|
||||||
|
results: state.results }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
searchService() {
|
||||||
|
if (!this._searchService) {
|
||||||
|
this._searchService = this.container.lookup('search-service:main');
|
||||||
|
}
|
||||||
|
return this._searchService;
|
||||||
|
},
|
||||||
|
|
||||||
|
searchContext() {
|
||||||
|
if (!this._searchContext) {
|
||||||
|
this._searchContext = this.searchService().get('searchContext');
|
||||||
|
}
|
||||||
|
return this._searchContext;
|
||||||
|
},
|
||||||
|
|
||||||
|
html() {
|
||||||
|
return this.attach('menu-panel', { maxWidth: 500, contents: () => this.panelContents() });
|
||||||
|
},
|
||||||
|
|
||||||
|
clickOutside() {
|
||||||
|
this.sendWidgetAction('toggleSearchMenu');
|
||||||
|
},
|
||||||
|
|
||||||
|
triggerSearch() {
|
||||||
|
const { state } = this;
|
||||||
|
|
||||||
|
state.noResults = false;
|
||||||
|
if (isValidSearchTerm(state.term)) {
|
||||||
|
this.searchService().set('highlightTerm', state.term);
|
||||||
|
state.loading = true;
|
||||||
|
Ember.run.debounce(SearchHelper, SearchHelper.perform, this, 400);
|
||||||
|
} else {
|
||||||
|
state.results = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
moreOfType(type) {
|
||||||
|
this.state.typeFilter = type;
|
||||||
|
this.triggerSearch();
|
||||||
|
},
|
||||||
|
|
||||||
|
searchContextChanged(enabled) {
|
||||||
|
this.state.typeFilter = null;
|
||||||
|
this.sendWidgetAction('searchMenuContextChanged', enabled);
|
||||||
|
this.state.contextEnabled = enabled;
|
||||||
|
this.triggerSearch();
|
||||||
|
},
|
||||||
|
|
||||||
|
searchTermChanged(term) {
|
||||||
|
this.state.typeFilter = null;
|
||||||
|
this.state.term = term;
|
||||||
|
this.triggerSearch();
|
||||||
|
},
|
||||||
|
|
||||||
|
fullSearch() {
|
||||||
|
if (!isValidSearchTerm(this.state.term)) { return; }
|
||||||
|
|
||||||
|
SearchHelper.cancel();
|
||||||
|
const url = this.fullSearchUrl();
|
||||||
|
if (url) {
|
||||||
|
this.sendWidgetEvent('linkClicked');
|
||||||
|
DiscourseURL.routeTo(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -125,6 +125,7 @@ createWidget('topic-map-link', {
|
||||||
|
|
||||||
createWidget('topic-map-expanded', {
|
createWidget('topic-map-expanded', {
|
||||||
tagName: 'section.topic-map-expanded',
|
tagName: 'section.topic-map-expanded',
|
||||||
|
buildKey: attrs => `topic-map-expanded-${attrs.id}`,
|
||||||
|
|
||||||
defaultState() {
|
defaultState() {
|
||||||
return { allLinksShown: false };
|
return { allLinksShown: false };
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
|
||||||
|
function renderIcon(name, key, canAct) {
|
||||||
|
const iconArgs = key === 'unpinned' ? { 'class': 'unpinned' } : null,
|
||||||
|
icon = iconNode(name, iconArgs);
|
||||||
|
|
||||||
|
const attributes = { title: Discourse.Utilities.escapeExpression(I18n.t(`topic_statuses.${key}.help`)) };
|
||||||
|
return h(`${canAct ? 'a' : 'span'}.topic-status`, attributes, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createWidget('topic-status', {
|
||||||
|
html(attrs) {
|
||||||
|
const topic = attrs.topic;
|
||||||
|
const canAct = this.currentUser && !attrs.disableActions;
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
const renderIconIf = (conditionProp, name, key) => {
|
||||||
|
if (!topic.get(conditionProp)) { return; }
|
||||||
|
result.push(renderIcon(name, key, canAct));
|
||||||
|
};
|
||||||
|
|
||||||
|
renderIconIf('is_warning', 'envelope', 'warning');
|
||||||
|
|
||||||
|
if (topic.get('closed') && topic.get('archived')) {
|
||||||
|
renderIcon('lock', 'locked_and_archived');
|
||||||
|
} else {
|
||||||
|
renderIconIf('topic.closed', 'lock', 'locked');
|
||||||
|
renderIconIf('topic.archived', 'lock', 'archived');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIconIf('topic.pinned', 'thumb-tack', 'pinned');
|
||||||
|
renderIconIf('topic.unpinned', 'thumb-tack', 'unpinned');
|
||||||
|
renderIconIf('topic.invisible', 'eye-slash', 'invisible');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
|
||||||
|
createWidget('user-menu-links', {
|
||||||
|
tagName: 'div.menu-links-header',
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
const { currentUser, siteSettings } = this;
|
||||||
|
|
||||||
|
const isAnon = currentUser.is_anonymous;
|
||||||
|
const allowAnon = siteSettings.allow_anonymous_posting &&
|
||||||
|
currentUser.trust_level >= siteSettings.anonymous_posting_min_trust_level ||
|
||||||
|
isAnon;
|
||||||
|
|
||||||
|
const path = attrs.path;
|
||||||
|
const glyphs = [{ label: 'user.bookmarks',
|
||||||
|
className: 'user-bookmarks-link',
|
||||||
|
icon: 'bookmark',
|
||||||
|
href: `${path}/activity/bookmarks` }];
|
||||||
|
|
||||||
|
if (siteSettings.enable_private_messages) {
|
||||||
|
glyphs.push({ label: 'user.private_messages',
|
||||||
|
className: 'user-pms-link',
|
||||||
|
icon: 'envelope',
|
||||||
|
href: `${path}/messages` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileLink = {
|
||||||
|
route: 'user',
|
||||||
|
model: currentUser,
|
||||||
|
className: 'user-activity-link',
|
||||||
|
icon: 'user',
|
||||||
|
rawLabel: currentUser.username
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentUser.is_anonymous) {
|
||||||
|
profileLink.label = 'user.profile';
|
||||||
|
profileLink.rawLabel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = [profileLink];
|
||||||
|
if (allowAnon) {
|
||||||
|
if (!isAnon) {
|
||||||
|
glyphs.push({ action: 'toggleAnonymous',
|
||||||
|
label: 'switch_to_anon',
|
||||||
|
className: 'enable-anonymous',
|
||||||
|
icon: 'user-secret' });
|
||||||
|
} else {
|
||||||
|
links.push({ className: 'disable-anonymous',
|
||||||
|
action: 'toggleAnonymous',
|
||||||
|
label: 'switch_from_anon' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// preferences always goes last
|
||||||
|
glyphs.push({ label: 'user.preferences',
|
||||||
|
className: 'user-preferences-link',
|
||||||
|
icon: 'gear',
|
||||||
|
href: `${path}/preferences` });
|
||||||
|
|
||||||
|
return h('ul.menu-links-row', [
|
||||||
|
links.map(l => h('li', this.attach('link', l))),
|
||||||
|
h('li.glyphs', glyphs.map(l => this.attach('link', $.extend(l, { hideLabel: true })))),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createWidget('user-menu', {
|
||||||
|
tagName: 'div.user-menu',
|
||||||
|
|
||||||
|
panelContents() {
|
||||||
|
const path = this.currentUser.get('path');
|
||||||
|
|
||||||
|
return [this.attach('user-menu-links', { path }),
|
||||||
|
this.attach('user-notifications', { path }),
|
||||||
|
h('div.logout-link', [
|
||||||
|
h('hr'),
|
||||||
|
h('ul.menu-links',
|
||||||
|
h('li', this.attach('link', { action: 'logout',
|
||||||
|
className: 'logout',
|
||||||
|
icon: 'sign-out',
|
||||||
|
label: 'user.log_out' })))
|
||||||
|
])];
|
||||||
|
},
|
||||||
|
|
||||||
|
html() {
|
||||||
|
return this.attach('menu-panel', { contents: () => this.panelContents() });
|
||||||
|
},
|
||||||
|
|
||||||
|
clickOutside() {
|
||||||
|
this.sendWidgetAction('toggleUserMenu');
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
import { dateNode } from 'discourse/helpers/node';
|
||||||
|
|
||||||
|
createWidget('large-notification-item', {
|
||||||
|
buildClasses(attrs) {
|
||||||
|
const result = ['item', 'notification'];
|
||||||
|
if (!attrs.get('read')) {
|
||||||
|
result.push('unread');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
html(attrs) {
|
||||||
|
return [this.attach('notification-item', attrs),
|
||||||
|
h('span.time', dateNode(attrs.created_at))];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createWidget('user-notifications-large', {
|
||||||
|
html(attrs) {
|
||||||
|
const notifications = attrs.notifications;
|
||||||
|
return notifications.map(n => this.attach('large-notification-item', n));
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { createWidget } from 'discourse/widgets/widget';
|
||||||
|
import { headerHeight } from 'discourse/components/site-header';
|
||||||
|
import { h } from 'virtual-dom';
|
||||||
|
|
||||||
|
export default createWidget('user-notifications', {
|
||||||
|
tagName: 'div.notifications',
|
||||||
|
buildKey: () => 'user-notifications',
|
||||||
|
|
||||||
|
defaultState() {
|
||||||
|
return { notifications: [], loading: false };
|
||||||
|
},
|
||||||
|
|
||||||
|
notificationsChanged() {
|
||||||
|
this.refreshNotifications(this.state);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshNotifications(state) {
|
||||||
|
if (this.loading) { return; }
|
||||||
|
|
||||||
|
// estimate (poorly) the amount of notifications to return
|
||||||
|
let limit = Math.round(($(window).height() - headerHeight()) / 55);
|
||||||
|
// we REALLY don't want to be asking for negative counts of notifications
|
||||||
|
// less than 5 is also not that useful
|
||||||
|
if (limit < 5) { limit = 5; }
|
||||||
|
if (limit > 40) { limit = 40; }
|
||||||
|
|
||||||
|
const stale = this.store.findStale('notification', {recent: true, limit }, {cacheKey: 'recent-notifications'});
|
||||||
|
|
||||||
|
if (stale.hasResults) {
|
||||||
|
const results = stale.results;
|
||||||
|
let content = results.get('content');
|
||||||
|
|
||||||
|
// we have to truncate to limit, otherwise we will render too much
|
||||||
|
if (content && (content.length > limit)) {
|
||||||
|
content = content.splice(0, limit);
|
||||||
|
results.set('content', content);
|
||||||
|
results.set('totalRows', limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.notifications = results;
|
||||||
|
} else {
|
||||||
|
state.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stale.refresh().then(notifications => {
|
||||||
|
this.currentUser.set('unread_notifications', 0);
|
||||||
|
state.notifications = notifications;
|
||||||
|
}).catch(() => {
|
||||||
|
state.notifications = [];
|
||||||
|
}).finally(() => {
|
||||||
|
state.loading = false;
|
||||||
|
this.scheduleRerender();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
html(attrs, state) {
|
||||||
|
if (!state.notifications.length) {
|
||||||
|
this.refreshNotifications(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
if (state.loading) {
|
||||||
|
result.push(h('div.spinner-container', h('div.spinner')));
|
||||||
|
} else if (state.notifications.length) {
|
||||||
|
|
||||||
|
const notificationItems = state.notifications.map(n => this.attach('notification-item', n));
|
||||||
|
const href = `${attrs.path}/notifications`;
|
||||||
|
|
||||||
|
result.push(h('hr'));
|
||||||
|
result.push(h('ul', [
|
||||||
|
notificationItems,
|
||||||
|
h('li.read.last.heading',
|
||||||
|
h('a', { attributes: { href } }, [I18n.t('notifications.more'), '...'])
|
||||||
|
)
|
||||||
|
]));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import { WidgetClickHook, WidgetClickOutsideHook } from 'discourse/widgets/click-hook';
|
import { WidgetClickHook, WidgetClickOutsideHook, WidgetKeyUpHook } from 'discourse/widgets/hooks';
|
||||||
import { h } from 'virtual-dom';
|
import { h } from 'virtual-dom';
|
||||||
import DecoratorHelper from 'discourse/widgets/decorator-helper';
|
import DecoratorHelper from 'discourse/widgets/decorator-helper';
|
||||||
|
|
||||||
|
@ -66,6 +66,11 @@ function drawWidget(builder, attrs, state) {
|
||||||
if (this.buildAttributes) {
|
if (this.buildAttributes) {
|
||||||
properties.attributes = this.buildAttributes(attrs);
|
properties.attributes = this.buildAttributes(attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.keyUp) {
|
||||||
|
properties['widget-key-up'] = new WidgetKeyUpHook(this);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.clickOutside) {
|
if (this.clickOutside) {
|
||||||
properties['widget-click-outside'] = new WidgetClickOutsideHook(this);
|
properties['widget-click-outside'] = new WidgetClickOutsideHook(this);
|
||||||
}
|
}
|
||||||
|
@ -119,9 +124,17 @@ export default class Widget {
|
||||||
|
|
||||||
this.key = this.buildKey ? this.buildKey(attrs) : null;
|
this.key = this.buildKey ? this.buildKey(attrs) : null;
|
||||||
|
|
||||||
|
// Helps debug widgets
|
||||||
|
if (Ember.Test) {
|
||||||
|
if (Object.keys(this.defaultState(attrs)).length > 0 && !this.key) {
|
||||||
|
Ember.warn(`you need a key when using state ${this.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.site = container.lookup('site:main');
|
this.site = container.lookup('site:main');
|
||||||
this.siteSettings = container.lookup('site-settings:main');
|
this.siteSettings = container.lookup('site-settings:main');
|
||||||
this.currentUser = container.lookup('current-user:main');
|
this.currentUser = container.lookup('current-user:main');
|
||||||
|
this.capabilities = container.lookup('capabilities:main');
|
||||||
this.store = container.lookup('store:main');
|
this.store = container.lookup('store:main');
|
||||||
this.appEvents = container.lookup('app-events:main');
|
this.appEvents = container.lookup('app-events:main');
|
||||||
this.keyValueStore = container.lookup('key-value-store:main');
|
this.keyValueStore = container.lookup('key-value-store:main');
|
||||||
|
@ -143,7 +156,7 @@ export default class Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
render(prev) {
|
render(prev) {
|
||||||
if (prev && prev.state) {
|
if (prev && prev.key && prev.key === this.key) {
|
||||||
this.state = prev.state;
|
this.state = prev.state;
|
||||||
} else {
|
} else {
|
||||||
this.state = this.defaultState(this.attrs, this.state);
|
this.state = this.defaultState(this.attrs, this.state);
|
||||||
|
@ -166,7 +179,7 @@ export default class Widget {
|
||||||
|
|
||||||
const refreshAction = dirtyOpts.onRefresh;
|
const refreshAction = dirtyOpts.onRefresh;
|
||||||
if (refreshAction) {
|
if (refreshAction) {
|
||||||
this.sendWidgetAction(refreshAction);
|
this.sendWidgetAction(refreshAction, dirtyOpts.refreshArg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +256,7 @@ export default class Widget {
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
// TODO: Use ember closure actions
|
// TODO: Use ember closure actions
|
||||||
const actions = target._actions || target.actionHooks;
|
const actions = target._actions || target.actionHooks || {};
|
||||||
const method = actions[actionName];
|
const method = actions[actionName];
|
||||||
if (method) {
|
if (method) {
|
||||||
promise = method.call(target, param);
|
promise = method.call(target, param);
|
||||||
|
@ -276,6 +289,16 @@ export default class Widget {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendWidgetEvent(name) {
|
||||||
|
const methodName = `${name}Event`;
|
||||||
|
return this.rerenderResult(() => {
|
||||||
|
const widget = this._findAncestorWithProperty(methodName);
|
||||||
|
if (widget) {
|
||||||
|
return widget[methodName]();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sendWidgetAction(name, param) {
|
sendWidgetAction(name, param) {
|
||||||
return this.rerenderResult(() => {
|
return this.rerenderResult(() => {
|
||||||
const widget = this._findAncestorWithProperty(name);
|
const widget = this._findAncestorWithProperty(name);
|
||||||
|
|
|
@ -63,12 +63,11 @@
|
||||||
//= require ./discourse/components/combo-box
|
//= require ./discourse/components/combo-box
|
||||||
//= require ./discourse/components/edit-category-panel
|
//= require ./discourse/components/edit-category-panel
|
||||||
//= require ./discourse/views/button
|
//= require ./discourse/views/button
|
||||||
//= require ./discourse/components/search-result
|
|
||||||
//= require ./discourse/components/dropdown-button
|
//= require ./discourse/components/dropdown-button
|
||||||
//= require ./discourse/components/notifications-button
|
//= require ./discourse/components/notifications-button
|
||||||
//= require ./discourse/components/topic-notifications-button
|
//= require ./discourse/components/topic-notifications-button
|
||||||
//= require ./discourse/lib/link-mentions
|
//= require ./discourse/lib/link-mentions
|
||||||
//= require ./discourse/views/header
|
//= require ./discourse/components/site-header
|
||||||
//= require ./discourse/lib/utilities
|
//= require ./discourse/lib/utilities
|
||||||
//= require ./discourse/dialects/dialect
|
//= require ./discourse/dialects/dialect
|
||||||
//= require ./discourse/lib/emoji/emoji
|
//= require ./discourse/lib/emoji/emoji
|
||||||
|
|
|
@ -131,7 +131,7 @@
|
||||||
right: 65px;
|
right: 65px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flagged-posts {
|
.flagged-posts, .queued-posts {
|
||||||
background: $danger;
|
background: $danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { acceptance } from "helpers/qunit-helpers";
|
|
||||||
|
|
||||||
acceptance("Hamburger Menu - Staff", { loggedIn: true });
|
|
||||||
|
|
||||||
test("Menu Items", (assert) => {
|
|
||||||
visit("/");
|
|
||||||
click("#toggle-hamburger-menu");
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(exists(".hamburger-panel .admin-link"));
|
|
||||||
assert.ok(exists(".hamburger-panel .flagged-posts-link"));
|
|
||||||
assert.ok(exists(".hamburger-panel .flagged-posts.badge-notification"), "it displays flag notifications");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { acceptance } from "helpers/qunit-helpers";
|
|
||||||
|
|
||||||
acceptance("Hamburger Menu");
|
|
||||||
|
|
||||||
test("Menu Items", (assert) => {
|
|
||||||
visit("/");
|
|
||||||
click("#toggle-hamburger-menu");
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(!exists(".hamburger-panel .admin-link"), 'does not have admin link');
|
|
||||||
assert.ok(!exists(".hamburger-panel .flagged-posts-link"), 'does not have flagged posts link');
|
|
||||||
|
|
||||||
assert.ok(exists(".hamburger-panel .latest-topics-link"), 'last link to latest');
|
|
||||||
assert.ok(exists(".hamburger-panel .badge-link"), 'has link to badges');
|
|
||||||
assert.ok(exists(".hamburger-panel .user-directory-link"), 'has user directory link');
|
|
||||||
assert.ok(exists(".hamburger-panel .faq-link"), 'has faq link');
|
|
||||||
assert.ok(exists(".hamburger-panel .about-link"), 'has about link');
|
|
||||||
assert.ok(exists(".hamburger-panel .categories-link"), 'has categories link');
|
|
||||||
|
|
||||||
assert.ok(exists('.hamburger-panel .category-link'), 'has at least one category');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { acceptance } from "helpers/qunit-helpers";
|
|
||||||
acceptance("Header (Anonymous)");
|
|
||||||
|
|
||||||
test("header", () => {
|
|
||||||
visit("/");
|
|
||||||
andThen(() => {
|
|
||||||
ok(exists("header"), "is rendered");
|
|
||||||
ok(exists(".logo-big"), "it renders the large logo by default");
|
|
||||||
not(exists("#notifications-dropdown li"), "no notifications at first");
|
|
||||||
not(exists("#user-dropdown:visible"), "initially user dropdown is closed");
|
|
||||||
not(exists("#search-dropdown:visible"), "initially search box is closed");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logo changing
|
|
||||||
andThen(() => {
|
|
||||||
controllerFor('header').set("showExtraInfo", true);
|
|
||||||
});
|
|
||||||
|
|
||||||
andThen(() => {
|
|
||||||
ok(exists(".logo-small"), "it shows the small logo when `showExtraInfo` is enabled");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Search
|
|
||||||
click("#search-button");
|
|
||||||
andThen(() => {
|
|
||||||
ok(exists(".search-menu:visible"), "after clicking a button search box opens");
|
|
||||||
not(exists(".search-menu .heading"), "initially, immediately after opening, search box is empty");
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { acceptance } from "helpers/qunit-helpers";
|
|
||||||
|
|
||||||
acceptance("Header (Staff)", { loggedIn: true });
|
|
||||||
|
|
||||||
test("header", () => {
|
|
||||||
visit("/");
|
|
||||||
|
|
||||||
// User dropdown
|
|
||||||
click("#current-user");
|
|
||||||
andThen(() => {
|
|
||||||
ok(exists(".user-menu:visible"), "is lazily rendered after user opens it");
|
|
||||||
ok(exists(".user-menu .menu-links-header"), "has showing / hiding user-dropdown links correctly bound");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -12,6 +12,7 @@ test("search", (assert) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
fillIn('#search-term', 'dev');
|
fillIn('#search-term', 'dev');
|
||||||
|
keyEvent('#search-term', 'keyup', 16);
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
assert.ok(exists('.search-menu .results ul li'), 'it shows results');
|
assert.ok(exists('.search-menu .results ul li'), 'it shows results');
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
import componentTest from 'helpers/component-test';
|
|
||||||
moduleForComponent('menu-panel', {integration: true});
|
|
||||||
|
|
||||||
componentTest('as a dropdown', {
|
|
||||||
template: `
|
|
||||||
<div id='outside-area'>click me</div>
|
|
||||||
|
|
||||||
<div class='menu-selected'></div>
|
|
||||||
|
|
||||||
{{#menu-panel visible=panelVisible markActive=".menu-selected" force="drop-down"}}
|
|
||||||
Some content
|
|
||||||
{{/menu-panel}}
|
|
||||||
`,
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
this.set('panelVisible', false);
|
|
||||||
},
|
|
||||||
|
|
||||||
test(assert) {
|
|
||||||
assert.ok(exists(".menu-panel.hidden"), "hidden by default");
|
|
||||||
|
|
||||||
this.set('panelVisible', true);
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(!exists(".menu-panel.hidden"), "toggling visible makes it appear");
|
|
||||||
});
|
|
||||||
|
|
||||||
click('#outside-area');
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(exists(".menu-panel.hidden"), "clicking the body hides the menu");
|
|
||||||
assert.equal(this.get('panelVisible'), false, 'it updates the bound variable');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
componentTest('as a slide-in', {
|
|
||||||
template: `
|
|
||||||
<div id='outside-area'>click me</div>
|
|
||||||
<div class='menu-selected'></div>
|
|
||||||
|
|
||||||
{{#menu-panel visible=panelVisible markActive=".menu-selected" force="slide-in"}}
|
|
||||||
Some content
|
|
||||||
{{/menu-panel}}
|
|
||||||
`,
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
this.set('panelVisible', false);
|
|
||||||
},
|
|
||||||
|
|
||||||
test(assert) {
|
|
||||||
assert.ok(exists(".menu-panel.hidden"), "hidden by default");
|
|
||||||
|
|
||||||
this.set('panelVisible', true);
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(!exists(".menu-panel.hidden"), "toggling visible makes it appear");
|
|
||||||
});
|
|
||||||
|
|
||||||
click('#outside-area');
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(exists(".menu-panel.hidden"), "clicking the body hides the menu");
|
|
||||||
assert.equal(this.get('panelVisible'), false, 'it updates the bound variable');
|
|
||||||
this.set('panelVisible', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { blank, present } from 'helpers/qunit-helpers';
|
import { blank, present } from 'helpers/qunit-helpers';
|
||||||
|
|
||||||
moduleFor('controller:topic', 'controller:topic', {
|
moduleFor('controller:topic', 'controller:topic', {
|
||||||
needs: ['controller:header', 'controller:modal', 'controller:composer', 'controller:quote-button',
|
needs: ['controller:modal', 'controller:composer', 'controller:quote-button',
|
||||||
'controller:topic-progress', 'controller:application']
|
'controller:topic-progress', 'controller:application']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import AppEvents from 'discourse/lib/app-events';
|
import AppEvents from 'discourse/lib/app-events';
|
||||||
import createStore from 'helpers/create-store';
|
import createStore from 'helpers/create-store';
|
||||||
import { autoLoadModules } from 'discourse/initializers/auto-load-modules';
|
import { autoLoadModules } from 'discourse/initializers/auto-load-modules';
|
||||||
|
import TopicTrackingState from 'discourse/models/topic-tracking-state';
|
||||||
|
|
||||||
export default function(name, opts) {
|
export default function(name, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
@ -22,11 +23,19 @@ export default function(name, opts) {
|
||||||
|
|
||||||
autoLoadModules();
|
autoLoadModules();
|
||||||
|
|
||||||
if (opts.setup) {
|
|
||||||
const store = createStore();
|
const store = createStore();
|
||||||
this.currentUser = Discourse.User.create();
|
if (!opts.anonymous) {
|
||||||
this.container.register('store:main', store, { instantiate: false });
|
const currentUser = Discourse.User.create({ username: 'eviltrout' });
|
||||||
|
this.currentUser = currentUser;
|
||||||
this.container.register('current-user:main', this.currentUser, { instantiate: false });
|
this.container.register('current-user:main', this.currentUser, { instantiate: false });
|
||||||
|
this.container.register('topic-tracking-state:main',
|
||||||
|
TopicTrackingState.create({ currentUser }),
|
||||||
|
{ instantiate: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.register('store:main', store, { instantiate: false });
|
||||||
|
|
||||||
|
if (opts.setup) {
|
||||||
opts.setup.call(this, store);
|
opts.setup.call(this, store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import sessionFixtures from 'fixtures/session-fixtures';
|
import sessionFixtures from 'fixtures/session-fixtures';
|
||||||
import siteFixtures from 'fixtures/site-fixtures';
|
import siteFixtures from 'fixtures/site-fixtures';
|
||||||
import HeaderView from 'discourse/views/header';
|
import HeaderComponent from 'discourse/components/site-header';
|
||||||
|
|
||||||
function currentUser() {
|
function currentUser() {
|
||||||
return Discourse.User.create(sessionFixtures['/session/current.json'].current_user);
|
return Discourse.User.create(sessionFixtures['/session/current.json'].current_user);
|
||||||
|
@ -41,7 +41,7 @@ function acceptance(name, options) {
|
||||||
Discourse.Utilities.avatarImg = () => "";
|
Discourse.Utilities.avatarImg = () => "";
|
||||||
|
|
||||||
// For now don't do scrolling stuff in Test Mode
|
// For now don't do scrolling stuff in Test Mode
|
||||||
HeaderView.reopen({examineDockHeader: Ember.K});
|
HeaderComponent.reopen({examineDockHeader: Ember.K});
|
||||||
|
|
||||||
var siteJson = siteFixtures['site.json'].site;
|
var siteJson = siteFixtures['site.json'].site;
|
||||||
if (options) {
|
if (options) {
|
||||||
|
|
|
@ -7,8 +7,8 @@ widgetTest('listing actions', {
|
||||||
setup() {
|
setup() {
|
||||||
this.set('args', {
|
this.set('args', {
|
||||||
actionsSummary: [
|
actionsSummary: [
|
||||||
{action: 'off_topic', description: 'very off topic'},
|
{id: 1, action: 'off_topic', description: 'very off topic'},
|
||||||
{action: 'spam', description: 'suspicious message'}
|
{id: 2, action: 'spam', description: 'suspicious message'}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { moduleForWidget, widgetTest } from 'helpers/widget-test';
|
||||||
|
|
||||||
|
moduleForWidget('hamburger-menu');
|
||||||
|
|
||||||
|
widgetTest('prioritize faq', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.faq_url = 'http://example.com/faq';
|
||||||
|
this.currentUser.set('read_faq', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.faq-priority').length);
|
||||||
|
assert.ok(!this.$('.faq-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('prioritize faq - user has read', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.faq_url = 'http://example.com/faq';
|
||||||
|
this.currentUser.set('read_faq', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!this.$('.faq-priority').length);
|
||||||
|
assert.ok(this.$('.faq-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('staff menu - not staff', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.currentUser.set('staff', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!this.$('.admin-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('staff menu', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.currentUser.setProperties({ staff: true, site_flagged_posts_count: 3 });
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.admin-link').length);
|
||||||
|
assert.ok(this.$('.flagged-posts-link').length);
|
||||||
|
assert.equal(this.$('.flagged-posts').text(), '3');
|
||||||
|
assert.ok(!this.$('.settings-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('staff menu - admin', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.currentUser.setProperties({ staff: true, admin: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.settings-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
widgetTest('queued posts', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.currentUser.setProperties({
|
||||||
|
staff: true,
|
||||||
|
show_queued_posts: true,
|
||||||
|
post_queue_new_count: 5
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.queued-posts-link').length);
|
||||||
|
assert.equal(this.$('.queued-posts').text(), '5');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('queued posts - disabled', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.currentUser.setProperties({ staff: true, show_queued_posts: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!this.$('.queued-posts-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
widgetTest('logged in links', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.new-topics-link').length);
|
||||||
|
assert.ok(this.$('.unread-topics-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('general links', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
anonymous: true,
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.latest-topics-link').length);
|
||||||
|
assert.ok(!this.$('.new-topics-link').length);
|
||||||
|
assert.ok(!this.$('.unread-topics-link').length);
|
||||||
|
assert.ok(this.$('.top-topics-link').length);
|
||||||
|
assert.ok(this.$('.badge-link').length);
|
||||||
|
assert.ok(this.$('.category-link').length > 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('badges link - disabled', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.enable_badges = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!this.$('.badge-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('badges link', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.badge-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('user directory link', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.user-directory-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('user directory link - disabled', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.enable_user_directory = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!this.$('.user-directory-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('general links', {
|
||||||
|
template: '{{mount-widget widget="hamburger-menu"}}',
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.about-link').length);
|
||||||
|
assert.ok(this.$('.keyboard-shortcuts-link').length);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { moduleForWidget, widgetTest } from 'helpers/widget-test';
|
||||||
|
|
||||||
|
moduleForWidget('header');
|
||||||
|
|
||||||
|
widgetTest('rendering basics', {
|
||||||
|
template: '{{mount-widget widget="header"}}',
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('header.d-header').length);
|
||||||
|
assert.ok(this.$('#site-logo').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('sign up / login buttons', {
|
||||||
|
template: '{{mount-widget widget="header" showCreateAccount="showCreateAccount" showLogin="showLogin" args=args}}',
|
||||||
|
anonymous: true,
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.set('args', { canSignUp: true });
|
||||||
|
this.on('showCreateAccount', () => this.signupShown = true);
|
||||||
|
this.on('showLogin', () => this.loginShown = true);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('button.sign-up-button').length);
|
||||||
|
assert.ok(this.$('button.login-button').length);
|
||||||
|
|
||||||
|
click('button.sign-up-button');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(this.signupShown);
|
||||||
|
});
|
||||||
|
|
||||||
|
click('button.login-button');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(this.loginShown);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,19 +1,19 @@
|
||||||
import componentTest from 'helpers/component-test';
|
import { moduleForWidget, widgetTest } from 'helpers/widget-test';
|
||||||
|
|
||||||
moduleForComponent('home-logo', {integration: true});
|
moduleForWidget('home-logo');
|
||||||
|
|
||||||
const bigLogo = '/images/d-logo-sketch.png?test';
|
const bigLogo = '/images/d-logo-sketch.png?test';
|
||||||
const smallLogo = '/images/d-logo-sketch-small.png?test';
|
const smallLogo = '/images/d-logo-sketch-small.png?test';
|
||||||
const mobileLogo = '/images/d-logo-sketch.png?mobile';
|
const mobileLogo = '/images/d-logo-sketch.png?mobile';
|
||||||
const title = "Cool Forum";
|
const title = "Cool Forum";
|
||||||
|
|
||||||
componentTest('basics', {
|
widgetTest('basics', {
|
||||||
template: '{{home-logo minimized=minimized}}',
|
template: '{{mount-widget widget="home-logo" args=args}}',
|
||||||
setup() {
|
setup() {
|
||||||
this.siteSettings.logo_url = bigLogo;
|
this.siteSettings.logo_url = bigLogo;
|
||||||
this.siteSettings.logo_small_url= smallLogo;
|
this.siteSettings.logo_small_url= smallLogo;
|
||||||
this.siteSettings.title = title;
|
this.siteSettings.title = title;
|
||||||
this.set('minimized', false);
|
this.set('args', { minimized: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
test(assert) {
|
test(assert) {
|
||||||
|
@ -23,23 +23,32 @@ componentTest('basics', {
|
||||||
assert.ok(this.$('img#site-logo.logo-big').length === 1);
|
assert.ok(this.$('img#site-logo.logo-big').length === 1);
|
||||||
assert.equal(this.$('#site-logo').attr('src'), bigLogo);
|
assert.equal(this.$('#site-logo').attr('src'), bigLogo);
|
||||||
assert.equal(this.$('#site-logo').attr('alt'), title);
|
assert.equal(this.$('#site-logo').attr('alt'), title);
|
||||||
|
|
||||||
this.set('minimized', true);
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(this.$('img.logo-small').length === 1);
|
|
||||||
assert.equal(this.$('img.logo-small').attr('src'), smallLogo);
|
|
||||||
assert.equal(this.$('img.logo-small').attr('alt'), title);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest('no logo', {
|
widgetTest('basics - minmized', {
|
||||||
template: '{{home-logo minimized=minimized}}',
|
template: '{{mount-widget widget="home-logo" args=args}}',
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.logo_url = bigLogo;
|
||||||
|
this.siteSettings.logo_small_url= smallLogo;
|
||||||
|
this.siteSettings.title = title;
|
||||||
|
this.set('args', { minimized: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('img.logo-small').length === 1);
|
||||||
|
assert.equal(this.$('img.logo-small').attr('src'), smallLogo);
|
||||||
|
assert.equal(this.$('img.logo-small').attr('alt'), title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('no logo', {
|
||||||
|
template: '{{mount-widget widget="home-logo" args=args}}',
|
||||||
setup() {
|
setup() {
|
||||||
this.siteSettings.logo_url = '';
|
this.siteSettings.logo_url = '';
|
||||||
this.siteSettings.logo_small_url = '';
|
this.siteSettings.logo_small_url = '';
|
||||||
this.siteSettings.title = title;
|
this.siteSettings.title = title;
|
||||||
this.set('minimized', false);
|
this.set('args', { minimized: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
test(assert) {
|
test(assert) {
|
||||||
|
@ -47,16 +56,25 @@ componentTest('no logo', {
|
||||||
|
|
||||||
assert.ok(this.$('h2#site-text-logo.text-logo').length === 1);
|
assert.ok(this.$('h2#site-text-logo.text-logo').length === 1);
|
||||||
assert.equal(this.$('#site-text-logo').text(), title);
|
assert.equal(this.$('#site-text-logo').text(), title);
|
||||||
|
|
||||||
this.set('minimized', true);
|
|
||||||
andThen(() => {
|
|
||||||
assert.ok(this.$('i.fa-home').length === 1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest('mobile logo', {
|
widgetTest('no logo - minimized', {
|
||||||
template: "{{home-logo}}",
|
template: '{{mount-widget widget="home-logo" args=args}}',
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.logo_url = '';
|
||||||
|
this.siteSettings.logo_small_url = '';
|
||||||
|
this.siteSettings.title = title;
|
||||||
|
this.set('args', { minimized: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('i.fa-home').length === 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('mobile logo', {
|
||||||
|
template: '{{mount-widget widget="home-logo" args=args}}',
|
||||||
setup() {
|
setup() {
|
||||||
this.siteSettings.mobile_logo_url = mobileLogo;
|
this.siteSettings.mobile_logo_url = mobileLogo;
|
||||||
this.siteSettings.logo_small_url= smallLogo;
|
this.siteSettings.logo_small_url= smallLogo;
|
||||||
|
@ -69,8 +87,8 @@ componentTest('mobile logo', {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest('mobile without logo', {
|
widgetTest('mobile without logo', {
|
||||||
template: "{{home-logo}}",
|
template: '{{mount-widget widget="home-logo" args=args}}',
|
||||||
setup() {
|
setup() {
|
||||||
this.siteSettings.logo_url = bigLogo;
|
this.siteSettings.logo_url = bigLogo;
|
||||||
this.site.mobileView = true;
|
this.site.mobileView = true;
|
||||||
|
@ -81,10 +99,3 @@ componentTest('mobile without logo', {
|
||||||
assert.equal(this.$('#site-logo').attr('src'), bigLogo);
|
assert.equal(this.$('#site-logo').attr('src'), bigLogo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
componentTest("changing url", {
|
|
||||||
template: '{{home-logo targetUrl="https://www.discourse.org"}}',
|
|
||||||
test(assert) {
|
|
||||||
assert.equal(this.$('a').attr('href'), 'https://www.discourse.org');
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -6,6 +6,7 @@ widgetTest("duplicate links", {
|
||||||
template: '{{mount-widget widget="post-gutter" args=args}}',
|
template: '{{mount-widget widget="post-gutter" args=args}}',
|
||||||
setup() {
|
setup() {
|
||||||
this.set('args', {
|
this.set('args', {
|
||||||
|
id: 2,
|
||||||
links: [
|
links: [
|
||||||
{ title: "Evil Trout Link", url: "http://eviltrout.com" },
|
{ title: "Evil Trout Link", url: "http://eviltrout.com" },
|
||||||
{ title: "Evil Trout Link", url: "http://dupe.eviltrout.com" }
|
{ title: "Evil Trout Link", url: "http://dupe.eviltrout.com" }
|
||||||
|
@ -21,6 +22,7 @@ widgetTest("collapsed links", {
|
||||||
template: '{{mount-widget widget="post-gutter" args=args}}',
|
template: '{{mount-widget widget="post-gutter" args=args}}',
|
||||||
setup() {
|
setup() {
|
||||||
this.set('args', {
|
this.set('args', {
|
||||||
|
id: 1,
|
||||||
links: [
|
links: [
|
||||||
{ title: "Link 1", url: "http://eviltrout.com?1" },
|
{ title: "Link 1", url: "http://eviltrout.com?1" },
|
||||||
{ title: "Link 2", url: "http://eviltrout.com?2" },
|
{ title: "Link 2", url: "http://eviltrout.com?2" },
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { moduleForWidget, widgetTest } from 'helpers/widget-test';
|
||||||
|
|
||||||
|
moduleForWidget('user-menu');
|
||||||
|
|
||||||
|
widgetTest('basics', {
|
||||||
|
template: '{{mount-widget widget="user-menu"}}',
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.user-menu').length);
|
||||||
|
assert.ok(this.$('.user-activity-link').length);
|
||||||
|
assert.ok(this.$('.user-bookmarks-link').length);
|
||||||
|
assert.ok(this.$('.user-preferences-link').length);
|
||||||
|
assert.ok(this.$('.notifications').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('log out', {
|
||||||
|
template: '{{mount-widget widget="user-menu" logout="logout"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.on('logout', () => this.loggedOut = true);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.logout').length);
|
||||||
|
|
||||||
|
click('.logout');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(this.loggedOut);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('private messages - disabled', {
|
||||||
|
template: '{{mount-widget widget="user-menu"}}',
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.enable_private_messages = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!this.$('.user-pms-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('private messages - enabled', {
|
||||||
|
template: '{{mount-widget widget="user-menu"}}',
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.enable_private_messages = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.user-pms-link').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('anonymous', {
|
||||||
|
template: '{{mount-widget widget="user-menu" toggleAnonymous="toggleAnonymous"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.currentUser.setProperties({ is_anonymous: false, trust_level: 3 });
|
||||||
|
this.siteSettings.allow_anonymous_posting = true;
|
||||||
|
this.siteSettings.anonymous_posting_min_trust_level = 3;
|
||||||
|
|
||||||
|
this.on('toggleAnonymous', () => this.anonymous = true);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.enable-anonymous').length);
|
||||||
|
click('.enable-anonymous');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(this.anonymous);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('anonymous - disabled', {
|
||||||
|
template: '{{mount-widget widget="user-menu"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.siteSettings.allow_anonymous_posting = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(!this.$('.enable-anonymous').length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetTest('anonymous - switch back', {
|
||||||
|
template: '{{mount-widget widget="user-menu" toggleAnonymous="toggleAnonymous"}}',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.currentUser.setProperties({ is_anonymous: true });
|
||||||
|
this.siteSettings.allow_anonymous_posting = true;
|
||||||
|
|
||||||
|
this.on('toggleAnonymous', () => this.anonymous = true);
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert) {
|
||||||
|
assert.ok(this.$('.disable-anonymous').length);
|
||||||
|
click('.disable-anonymous');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(this.anonymous);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -89,6 +89,7 @@ widgetTest('widget state', {
|
||||||
setup() {
|
setup() {
|
||||||
createWidget('state-test', {
|
createWidget('state-test', {
|
||||||
tagName: 'button.test',
|
tagName: 'button.test',
|
||||||
|
buildKey: () => `button-test`,
|
||||||
|
|
||||||
defaultState() {
|
defaultState() {
|
||||||
return { clicks: 0 };
|
return { clicks: 0 };
|
||||||
|
@ -121,6 +122,7 @@ widgetTest('widget update with promise', {
|
||||||
setup() {
|
setup() {
|
||||||
createWidget('promise-test', {
|
createWidget('promise-test', {
|
||||||
tagName: 'button.test',
|
tagName: 'button.test',
|
||||||
|
buildKey: () => 'promise-test',
|
||||||
|
|
||||||
html(attrs, state) {
|
html(attrs, state) {
|
||||||
return state.name || "No name";
|
return state.name || "No name";
|
||||||
|
|
Loading…
Reference in New Issue