diff --git a/.gitignore b/.gitignore index 67dcad40e5..8a1f8652b9 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ bootsnap-compile-cache/ !/plugins/discourse-details/ !/plugins/discourse-nginx-performance-report !/plugins/discourse-narrative-bot +!/plugins/discourse-presence /plugins/*/auto_generated/ /spec/fixtures/plugins/my_plugin/auto_generated diff --git a/.travis.yml b/.travis.yml index 791ff459c5..4438315558 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ before_install: - git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday - git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies - git clone --depth=1 https://github.com/discourse/discourse-slack-official.git plugins/discourse-slack-official + - git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration install: - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi" diff --git a/.tx/config b/.tx/config index 77c2f93321..d5da845ad7 100644 --- a/.tx/config +++ b/.tx/config @@ -38,6 +38,18 @@ source_file = plugins/discourse-narrative-bot/config/locales/server.en.yml source_lang = en type = YML +[discourse-org.presenceclientenyml] +file_filter = plugins/discourse-presence/config/locales/client..yml +source_file = plugins/discourse-presence/config/locales/client.en.yml +source_lang = en +type = YML + +[discourse-org.presenceserverenyml] +file_filter = plugins/discourse-presence/config/locales/server..yml +source_file = plugins/discourse-presence/config/locales/server.en.yml +source_lang = en +type = YML + [discourse-org.403html] file_filter = public/403..html source_file = public/403.html diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index 1bd414e330..03be28e034 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -122,7 +122,7 @@
- {{notifications-button i18nPrefix='groups.notifications' notificationLevel=model.default_notification_level}} + {{notifications-button i18nPrefix='groups.notifications' value=model.default_notification_level}}
diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index 7a016f150f..e00fbfedf5 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -174,6 +174,8 @@ {{/if}} +{{plugin-outlet name="admin-user-details" args=(hash model=model)}} +

{{i18n 'admin.user.permissions'}}

@@ -466,7 +468,7 @@ {{/if}}
-
+
{{#unless model.anonymizeForbidden}} {{d-button label="admin.user.anonymize" @@ -487,7 +489,7 @@ {{#if model.deleteExplanation}}
-
+
{{d-icon "exclamation-triangle"}} {{model.deleteExplanation}}
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 61c5442a93..e5d4e93784 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -66,7 +66,6 @@ //= require ./discourse/controllers/navigation/default //= require ./discourse/components/edit-category-panel //= require ./discourse/components/dropdown-button -//= require ./discourse/components/notifications-button //= require ./discourse/lib/link-mentions //= require ./discourse/components/site-header //= require ./discourse/components/d-editor diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 60fe773995..fcf3ce404e 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -1,6 +1,16 @@ import { h } from 'virtual-dom'; let _renderers = []; +const REPLACEMENTS = { + 'd-tracking': 'circle', + 'd-muted': 'times-circle', + 'd-regular': 'circle-o', + 'd-watching': 'exclamation-circle', + 'd-watching-first': 'dot-circle-o', + 'd-drop-expanded': 'caret-down', + 'd-drop-collapsed': 'caret-right', +}; + export function renderIcon(renderType, id, params) { for (let i=0; i<_renderers.length; i++) { let renderer = _renderers[i]; @@ -23,6 +33,11 @@ export function iconNode(id, params) { return renderIcon('node', id, params); } +// TODO: Improve how helpers are registered for vdom compliation +if (typeof Discourse !== "undefined") { + Discourse.__widget_helpers.iconNode = iconNode; +} + export function registerIconRenderer(renderer) { _renderers.unshift(renderer); } @@ -42,6 +57,8 @@ registerIconRenderer({ name: 'font-awesome', string(id, params) { + id = REPLACEMENTS[id] || id; + let tagName = params.tagName || 'i'; let html = `<${tagName} class='${faClasses(id, params)}'`; if (params.title) { html += ` title='${I18n.t(params.title)}'`; } @@ -54,6 +71,8 @@ registerIconRenderer({ }, node(id, params) { + id = REPLACEMENTS[id] || id; + let tagName = params.tagName || 'i'; const properties = { diff --git a/app/assets/javascripts/discourse.js.es6 b/app/assets/javascripts/discourse.js.es6 index 1c572af7ba..f4bb1fdb02 100644 --- a/app/assets/javascripts/discourse.js.es6 +++ b/app/assets/javascripts/discourse.js.es6 @@ -7,6 +7,8 @@ const Discourse = Ember.Application.extend({ rootElement: '#main', _docTitle: document.title, RAW_TEMPLATES: {}, + __widget_helpers: {}, + showingSignup: false, getURL(url) { if (!url) return url; diff --git a/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 b/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 index 6a6c1a3353..1e5cb05748 100644 --- a/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 @@ -1,39 +1,49 @@ -import { iconHTML } from 'discourse-common/lib/icon-library'; -import DropdownButton from 'discourse/components/dropdown-button'; +import DropdownSelectBoxComponent from "discourse/components/dropdown-select-box"; +import { iconHTML } from "discourse-common/lib/icon-library"; import computed from "ember-addons/ember-computed-decorators"; -export default DropdownButton.extend({ - buttonExtraClasses: 'no-text', - title: '', - text: iconHTML('bars') + ' ' + iconHTML('caret-down'), - classNames: ['category-notification-menu', 'category-admin-menu'], +export default DropdownSelectBoxComponent.extend({ + classNames: ["categories-admin-dropdown"], - @computed() - dropDownContent() { - const includeReorder = this.get('siteSettings.fixed_category_positions'); + icon: `${iconHTML('bars')}${iconHTML('caret-down')}`.htmlSafe(), + + generatedHeadertext: null, + + @computed + content() { const items = [ - { id: 'create', - title: I18n.t('category.create'), - description: I18n.t('category.create_long'), - icon: 'plus' } + { + id: "create", + text: I18n.t("category.create"), + description: I18n.t("category.create_long"), + icon: "plus" + } ]; + + const includeReorder = this.get("siteSettings.fixed_category_positions"); if (includeReorder) { items.push({ - id: 'reorder', - title: I18n.t('categories.reorder.title'), - description: I18n.t('categories.reorder.title_long'), - icon: 'random' + id: "reorder", + text: I18n.t("categories.reorder.title"), + description: I18n.t("categories.reorder.title_long"), + icon: "random" }); } + return items; }, actionNames: { - create: 'createCategory', - reorder: 'reorderCategories' + create: "createCategory", + reorder: "reorderCategories" }, - clicked(id) { - this.sendAction('actionNames.' + id); + actions: { + onSelectRow(content) { + this._super(content); + + this.sendAction(`actionNames.${this.get("value")}`); + this.set("value", null); + } } }); diff --git a/app/assets/javascripts/discourse/components/category-drop.js.es6 b/app/assets/javascripts/discourse/components/category-drop.js.es6 index 7a79c6b915..379e326ac8 100644 --- a/app/assets/javascripts/discourse/components/category-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/category-drop.js.es6 @@ -12,7 +12,7 @@ export default Ember.Component.extend({ @computed('expanded') expandIcon(expanded) { - return expanded ? 'caret-down' : 'caret-right'; + return expanded ? 'd-drop-expanded' : 'd-drop-collapsed'; }, allCategoriesUrl: function() { diff --git a/app/assets/javascripts/discourse/components/category-notifications-button.js.es6 b/app/assets/javascripts/discourse/components/category-notifications-button.js.es6 index 7be7e1bc97..0595b116fb 100644 --- a/app/assets/javascripts/discourse/components/category-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/category-notifications-button.js.es6 @@ -1,13 +1,28 @@ -import NotificationsButton from 'discourse/components/notifications-button'; +import NotificationOptionsComponent from "discourse/components/notifications-button"; +import computed from "ember-addons/ember-computed-decorators"; +import { iconHTML } from "discourse-common/lib/icon-library"; -export default NotificationsButton.extend({ - classNames: ['notification-options', 'category-notification-menu'], - buttonIncludesText: false, - hidden: Em.computed.alias('category.deleted'), - notificationLevel: Em.computed.alias('category.notification_level'), - i18nPrefix: 'category.notifications', +export default NotificationOptionsComponent.extend({ + classNames: ["category-notifications-button"], - clicked(id) { - this.get('category').setNotification(id); + hidden: Ember.computed.or("category.deleted", "site.isMobileDevice"), + + i18nPrefix: "category.notifications", + + value: Em.computed.alias("category.notification_level"), + + @computed("value") + icon() { + return `${this._super()}${iconHTML("caret-down")}`.htmlSafe(); + }, + + generatedHeadertext: null, + + actions: { + onSelectRow(content) { + this._super(content); + + this.get("category").setNotification(this.get("value")); + } } }); diff --git a/app/assets/javascripts/discourse/components/category-select-box.js.es6 b/app/assets/javascripts/discourse/components/category-select-box.js.es6 index 185dfe673f..e725a7cb1d 100644 --- a/app/assets/javascripts/discourse/components/category-select-box.js.es6 +++ b/app/assets/javascripts/discourse/components/category-select-box.js.es6 @@ -26,6 +26,12 @@ export default SelectBoxComponent.extend({ this.set("content", this.get("categories")); this._scopeCategories(); } + + if (Ember.isNone(this.get("value"))) { + if (this.siteSettings.allow_uncategorized_topics && this.get("allowUncategorized") !== false) { + this.set("value", Category.findUncategorized().id); + } + } }, filterFunction: function(content) { diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index ac3c82689f..44f0130acb 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -376,7 +376,7 @@ export default Ember.Component.extend({ return resolve([translations[full]]); } - const match = term.match(/^:?(.*?):t(\d)?$/); + const match = term.match(/^:?(.*?):t([2-6])?$/); if (match) { let name = match[1]; let scale = match[2]; diff --git a/app/assets/javascripts/discourse/components/dropdown-select-box.js.es6 b/app/assets/javascripts/discourse/components/dropdown-select-box.js.es6 new file mode 100644 index 0000000000..b268da2aa8 --- /dev/null +++ b/app/assets/javascripts/discourse/components/dropdown-select-box.js.es6 @@ -0,0 +1,33 @@ +import computed from "ember-addons/ember-computed-decorators"; +import SelectBoxComponent from "discourse/components/select-box"; + +export default SelectBoxComponent.extend({ + classNames: ["dropdown-select-box"], + wrapper: false, + verticalOffset: 3, + collectionHeight: "auto", + fullWidthOnMobile: true, + selectBoxHeaderComponent: "dropdown-select-box/dropdown-header", + + @computed + templateForRow: function() { + return (rowComponent) => { + let template = ""; + const content = rowComponent.get("content"); + + const icon = rowComponent.icon(); + if (icon) { + template += `
${icon}
`; + } + + template += ` +
+ ${Handlebars.escapeExpression(Ember.get(content, this.get("textKey")))} + ${Handlebars.escapeExpression(content.description)} +
+ `; + + return template; + }; + } +}); diff --git a/app/assets/javascripts/discourse/components/dropdown-select-box/dropdown-header.js.es6 b/app/assets/javascripts/discourse/components/dropdown-select-box/dropdown-header.js.es6 new file mode 100644 index 0000000000..9e6499c04f --- /dev/null +++ b/app/assets/javascripts/discourse/components/dropdown-select-box/dropdown-header.js.es6 @@ -0,0 +1,7 @@ +import SelectBoxHeaderComponent from "discourse/components/select-box/select-box-header"; + +export default SelectBoxHeaderComponent.extend({ + layoutName: "components/dropdown-select-box/dropdown-header", + + classNames: ["dropdown-header"] +}); diff --git a/app/assets/javascripts/discourse/components/group-notifications-button.js.es6 b/app/assets/javascripts/discourse/components/group-notifications-button.js.es6 index ef2386086f..3d597f8b56 100644 --- a/app/assets/javascripts/discourse/components/group-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/group-notifications-button.js.es6 @@ -1,11 +1,17 @@ -import NotificationsButton from 'discourse/components/notifications-button'; +import NotificationOptionsComponent from "discourse/components/notifications-button"; -export default NotificationsButton.extend({ - classNames: ['notification-options', 'group-notification-menu'], - notificationLevel: Em.computed.alias('group.group_user.notification_level'), - i18nPrefix: 'groups.notifications', +export default NotificationOptionsComponent.extend({ + classNames: ["group-notifications-button"], - clicked(id) { - this.get('group').setNotification(id, this.get('user.id')); + value: Em.computed.alias("group.group_user.notification_level"), + + i18nPrefix: "groups.notifications", + + actions: { + onSelectRow(content) { + this._super(content); + + this.get("group").setNotification(this.get("value"), this.get("user.id")); + } } }); diff --git a/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 index f92992e6f1..d8fd285316 100644 --- a/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 @@ -3,5 +3,5 @@ import { showEntrance } from "discourse/components/topic-list-item"; export default Ember.Component.extend({ click: showEntrance, attributeBindings: ['topic.id:data-topic-id'], - classNameBindings: [':latest-topic-list-item', 'topic.archived'] + classNameBindings: [':latest-topic-list-item', 'topic.archived', 'topic.visited'] }); diff --git a/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 b/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 index 833c4a1dfb..55ede8c675 100644 --- a/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 +++ b/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 @@ -2,6 +2,6 @@ import { showEntrance } from 'discourse/components/topic-list-item'; export default Ember.Component.extend({ tagName: 'tr', - classNameBindings: [':category-topic-link', 'topic.archived'], + classNameBindings: [':category-topic-link', 'topic.archived', 'topic.visited'], click: showEntrance }); diff --git a/app/assets/javascripts/discourse/components/notifications-button.js.es6 b/app/assets/javascripts/discourse/components/notifications-button.js.es6 index 2c1cacc860..2c54defb29 100644 --- a/app/assets/javascripts/discourse/components/notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/notifications-button.js.es6 @@ -1,50 +1,68 @@ -import DropdownButton from 'discourse/components/dropdown-button'; -import { allLevels, buttonDetails } from 'discourse/lib/notification-levels'; -import { iconHTML } from 'discourse-common/lib/icon-library'; -import computed from 'ember-addons/ember-computed-decorators'; +import DropdownSelectBoxComponent from "discourse/components/dropdown-select-box"; +import { iconHTML } from "discourse-common/lib/icon-library"; +import computed from "ember-addons/ember-computed-decorators"; +import { buttonDetails } from "discourse/lib/notification-levels"; +import { allLevels } from "discourse/lib/notification-levels"; -export default DropdownButton.extend({ - classNames: ['notification-options'], - title: '', - buttonIncludesText: true, - activeItem: Em.computed.alias('notificationLevel'), - i18nPrefix: '', - i18nPostfix: '', +export default DropdownSelectBoxComponent.extend({ + classNames: ["notifications-button"], + + i18nPrefix: "", + i18nPostfix: "", + textKey: "key", + showFullTitle: true, + fullWidthOnMobile: true, + content: allLevels, + + value: Em.computed.alias("notificationLevel"), + + @computed("selectedDetails") + icon(details) { + return iconHTML(details.icon, {class: details.key}).htmlSafe(); + }, + + @computed("selectedDetails.key", "i18nPrefix") + selectedTitle(key, prefix) { + return I18n.t(`${prefix}.${key}.title`); + }, + + @computed("value") + selectedDetails(value) { + return buttonDetails(value); + }, + + @computed("selectedTitle", "showFullTitle") + generatedHeadertext(selectedTitle, showFullTitle) { + return showFullTitle ? selectedTitle : null; + }, @computed - dropDownContent() { - const prefix = this.get('i18nPrefix'); - const postfix = this.get('i18nPostfix'); - - return allLevels.map(l => { - const start = `${prefix}.${l.key}${postfix}`; - return { - id: l.id, - title: I18n.t(`${start}.title`), - description: I18n.t(`${start}.description`), - icon: l.icon, - iconClass: l.key.dasherize(), - }; - }); + titleForRow: function() { + return (rowComponent) => { + const notificationLevel = rowComponent.get(`content.${this.get("idKey")}`); + const details = buttonDetails(notificationLevel); + return I18n.t(`${this.get("i18nPrefix")}.${details.key}.title`); + }; }, - @computed('notificationLevel') - text(notificationLevel) { - const details = buttonDetails(notificationLevel); - const { key } = details; - const icon = iconHTML(details.icon, { class: key.dasherize() }); + @computed + templateForRow: function() { + return (rowComponent) => { + const content = rowComponent.get("content"); + const start = `${this.get("i18nPrefix")}.${content.key}${this.get("i18nPostfix")}`; + const title = I18n.t(`${start}.title`); + const description = I18n.t(`${start}.description`); - if (this.get('buttonIncludesText')) { - const prefix = this.get('i18nPrefix'); - const postfix = this.get('i18nPostfix'); - const text = I18n.t(`${prefix}.${key}${postfix}.title`); - return `${icon} ${text}`; - } else { - return `${icon} `; - } - }, - - clicked(id) { - this.set("notificationLevel", id); + return ` +
+ + ${iconHTML(content.icon, { class: content.key.dasherize() })} +
+
+ ${Handlebars.escapeExpression(title)} + ${Handlebars.escapeExpression(description)} +
+ `; + }; } }); diff --git a/app/assets/javascripts/discourse/components/pinned-button.js.es6 b/app/assets/javascripts/discourse/components/pinned-button.js.es6 index fe6dac72de..3d287deb2a 100644 --- a/app/assets/javascripts/discourse/components/pinned-button.js.es6 +++ b/app/assets/javascripts/discourse/components/pinned-button.js.es6 @@ -1,67 +1,22 @@ -import { iconHTML } from 'discourse-common/lib/icon-library'; -import computed from 'ember-addons/ember-computed-decorators'; -import DropdownButton from 'discourse/components/dropdown-button'; +import computed from "ember-addons/ember-computed-decorators"; -export default DropdownButton.extend({ - descriptionKey: 'help', - classNames: ['pinned-options'], - title: '', - buttonExtraClasses: 'btn-icon-text', +export default Ember.Component.extend({ + descriptionKey: "help", - longDescription: function(){ - const topic = this.get('topic'); - const globally = topic.get('pinned_globally') ? '_globally' : ''; - const key = 'topic_statuses.' + (topic.get('pinned') ? 'pinned' + globally : 'unpinned') + '.help'; + classNames: ["pinned-button"], + + classNameBindings: ["hidden:is-hidden"], + + @computed("topic.pinned_globally", "topic.pinned") + reasonText(pinnedGlobally, pinned) { + const globally = pinnedGlobally ? "_globally" : ""; + const pinnedKey = pinned ? `pinned${globally}` : "unpinned"; + const key = `topic_statuses.${pinnedKey}.help`; return I18n.t(key); - }.property('topic.pinned'), - - target: Em.computed.alias('topic'), - - hidden: function(){ - const topic = this.get('topic'); - return topic.get('deleted') || (!topic.get('pinned') && !topic.get('unpinned')); - }.property('topic.pinned', 'topic.deleted', 'topic.unpinned'), - - activeItem: function(){ - return this.get('topic.pinned') ? 'pinned' : 'unpinned'; - }.property('topic.pinned'), - - dropDownContent: function() { - const globally = this.get('topic.pinned_globally') ? '_globally' : ''; - return [ - {id: 'pinned', - title: I18n.t('topic_statuses.pinned' + globally + '.title'), - description: I18n.t('topic_statuses.pinned' + globally + '.help'), - icon: 'thumb-tack' }, - {id: 'unpinned', - title: I18n.t('topic_statuses.unpinned.title'), - description: I18n.t('topic_statuses.unpinned.help'), - icon: 'thumb-tack', - iconClass: 'unpinned' } - ]; - }.property(), - - @computed('topic.pinned', 'topic.pinned_globally') - text(pinned, pinnedGlobally) { - const globally = pinnedGlobally ? '_globally' : ''; - const state = pinned ? 'pinned' + globally : 'unpinned'; - - const icon = iconHTML( - 'thumb-tack', - { tagName: 'span', class: (state === 'unpinned' ? 'unpinned' : null) } - ); - - return icon + - I18n.t('topic_statuses.' + state + '.title') + ""; }, - clicked(id) { - const topic = this.get('topic'); - if(id==='unpinned'){ - topic.clearPin(); - } else { - topic.rePin(); - } + @computed("topic.pinned", "topic.deleted", "topic.unpinned") + hidden(pinned, deleted, unpinned) { + return deleted || (!pinned && !unpinned); } - }); diff --git a/app/assets/javascripts/discourse/components/pinned-options.js.es6 b/app/assets/javascripts/discourse/components/pinned-options.js.es6 new file mode 100644 index 0000000000..2daf5bea98 --- /dev/null +++ b/app/assets/javascripts/discourse/components/pinned-options.js.es6 @@ -0,0 +1,73 @@ +import DropdownSelectBoxComponent from "discourse/components/dropdown-select-box"; +import computed from "ember-addons/ember-computed-decorators"; +import { observes } from "ember-addons/ember-computed-decorators"; +import { iconHTML } from "discourse-common/lib/icon-library"; + +export default DropdownSelectBoxComponent.extend({ + classNames: ["pinned-options"], + + @computed("topic.pinned") + value(pinned) { + return pinned ? "pinned" : "unpinned"; + }, + + @observes("topic.pinned") + _pinnedChanged() { + this.set("value", this.get("topic.pinned") ? "pinned" : "unpinned"); + }, + + @computed("topic.pinned_globally") + content(pinnedGlobally) { + const globally = pinnedGlobally ? "_globally" : ""; + + return [ + { + id: "pinned", + text: I18n.t("topic_statuses.pinned" + globally + ".title"), + description: I18n.t('topic_statuses.pinned' + globally + '.help'), + icon: "thumb-tack" + }, + { + id: "unpinned", + text: I18n.t("topic_statuses.unpinned.title"), + icon: "thumb-tack", + description: I18n.t('topic_statuses.unpinned.help'), + iconClass: "unpinned" + } + ]; + }, + + @computed("topic.pinned", "topic.pinned_globally") + icon(pinned, pinnedGlobally) { + const globally = pinnedGlobally ? "_globally" : ""; + const state = pinned ? `pinned${globally}` : "unpinned"; + + return iconHTML( + "thumb-tack", + { class: (state === "unpinned" ? "unpinned" : null) } + ); + }, + + @computed("topic.pinned", "topic.pinned_globally") + generatedHeadertext(pinned, pinnedGlobally) { + const globally = pinnedGlobally ? "_globally" : ""; + const state = pinned ? `pinned${globally}` : "unpinned"; + const title = I18n.t(`topic_statuses.${state}.title`); + + return `${title}${iconHTML("caret-down")}`.htmlSafe(); + }, + + actions: { + onSelectRow(content) { + this._super(content); + + const topic = this.get("topic"); + + if (this.get("value") === "unpinned") { + topic.clearPin(); + } else { + topic.rePin(); + } + } + } +}); diff --git a/app/assets/javascripts/discourse/components/select-box.js.es6 b/app/assets/javascripts/discourse/components/select-box.js.es6 index 8a48b9e4d3..d2e0c25b99 100644 --- a/app/assets/javascripts/discourse/components/select-box.js.es6 +++ b/app/assets/javascripts/discourse/components/select-box.js.es6 @@ -1,18 +1,17 @@ import { on, observes } from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators"; -import { iconHTML } from "discourse-common/lib/icon-library"; export default Ember.Component.extend({ layoutName: "components/select-box", - classNames: "select-box", - classNameBindings: ["expanded:is-expanded"], + classNameBindings: ["expanded:is-expanded", "hidden:is-hidden"], expanded: false, focused: false, filterFocused: false, renderBody: false, wrapper: true, + hidden: false, tabindex: 0, scrollableParentSelector: ".modal-body", @@ -24,9 +23,9 @@ export default Ember.Component.extend({ clearable: false, value: null, + highlightedValue: null, selectedContent: null, noContentLabel: I18n.t("select_box.no_content"), - lastHovered: null, clearSelectionLabel: null, idKey: "id", @@ -43,13 +42,17 @@ export default Ember.Component.extend({ selectBoxHeaderComponent: "select-box/select-box-header", selectBoxCollectionComponent: "select-box/select-box-collection", - minWidth: 220, - maxCollectionHeight: 200, + collectionHeight: 200, verticalOffset: 0, horizontalOffset: 0, + fullWidthOnMobile: false, castInteger: false, + click(event) { + event.stopPropagation(); + }, + filterFunction: function(content) { return (selectBox) => { const filter = selectBox.get("filter").toLowerCase(); @@ -59,33 +62,37 @@ export default Ember.Component.extend({ }; }, + @computed titleForRow: function() { return (rowComponent) => { return rowComponent.get(`content.${this.get("textKey")}`); }; - }.property(), + }, + @computed shouldHighlightRow: function() { return (rowComponent) => { - if (Ember.isNone(this.get("value")) && Ember.isNone(this.get("lastHovered"))) { - return false; - } - const id = this._castInteger(rowComponent.get(`content.${this.get("idKey")}`)); - if (Ember.isNone(this.get("lastHovered"))) { - return id === this.get("value"); - } else { - return id === this.get("lastHovered"); - } + return id === this.get("highlightedValue"); }; - }.property(), + }, + @computed("value", "idKey") + shouldSelectRow(value, idKey) { + return (rowComponent) => { + const id = this._castInteger(rowComponent.get(`content.${idKey}`)); + return id === value; + }; + }, + + @computed templateForRow: function() { return (rowComponent) => { let template = ""; - if (rowComponent.get("content.icon")) { - template += iconHTML(Handlebars.escapeExpression(rowComponent.get("content.icon"))); + const icon = rowComponent.icon(); + if (icon) { + template += icon; } const text = rowComponent.get(`content.${this.get("textKey")}`); @@ -93,29 +100,50 @@ export default Ember.Component.extend({ return template; }; - }.property(), + }, applyDirection() { - const offsetTop = this.$()[0].getBoundingClientRect().top; - const windowHeight = $(window).height(); + this.$().removeClass("is-above is-below is-left-aligned is-right-aligned"); + let options = { left: "auto", bottom: "auto", left: "auto", top: "auto" }; const headerHeight = this.$(".select-box-header").outerHeight(false); const filterHeight = this.$(".select-box-filter").outerHeight(false); + const bodyHeight = this.$(".select-box-body").outerHeight(false); + const windowWidth = $(window).width(); + const windowHeight = $(window).height(); + const boundingRect = this.$()[0].getBoundingClientRect(); + const offsetTop = boundingRect.top; - if (windowHeight - (offsetTop + this.get("maxCollectionHeight") + filterHeight + headerHeight) < 0) { - this.$().addClass("is-reversed"); - this.$(".select-box-body").css({ - left: this.get("horizontalOffset"), - top: "auto", - bottom: headerHeight + this.get("verticalOffset") - }); + if (this.get("fullWidthOnMobile") && this.site.isMobileDevice) { + const margin = 10; + const relativeLeft = this.$().offset().left - $(window).scrollLeft(); + options.left = margin - relativeLeft; + options.width = windowWidth - margin * 2; + options.maxWidth = options.minWidth = "unset"; } else { - this.$().removeClass("is-reversed"); - this.$(".select-box-body").css({ - left: this.get("horizontalOffset"), - top: headerHeight + this.get("verticalOffset"), - bottom: "auto" - }); + const offsetLeft = boundingRect.left; + const bodyWidth = this.$(".select-box-body").outerWidth(false); + const hasRightSpace = (windowWidth - (this.get("horizontalOffset") + offsetLeft + filterHeight + bodyWidth) > 0); + + if (hasRightSpace) { + this.$().addClass("is-left-aligned"); + options.left = this.get("horizontalOffset"); + } else { + this.$().addClass("is-right-aligned"); + options.right = this.get("horizontalOffset"); + } } + + const componentHeight = this.get("verticalOffset") + bodyHeight + headerHeight; + const hasBelowSpace = windowHeight - offsetTop - componentHeight > 0; + if (hasBelowSpace) { + this.$().addClass("is-below"); + options.top = headerHeight + this.get("verticalOffset"); + } else { + this.$().addClass("is-above"); + options.bottom = headerHeight + this.get("verticalOffset"); + } + + this.$(".select-box-body").css(options); }, init() { @@ -136,7 +164,7 @@ export default Ember.Component.extend({ @on("willDestroyElement") _removeDocumentListeners: function() { - $(document).off("click.select-box", "keydown.select-box"); + $(document).off("click.select-box"); $(window).off("resize.select-box"); }, @@ -156,12 +184,9 @@ export default Ember.Component.extend({ this._removeFixedPosition(); } - this.$().css("min-width", this.get("minWidth")); - const computedWidth = this.$().outerWidth(false); const computedHeight = this.$().outerHeight(false); - this.$(".select-box-header").css("height", computedHeight); this.$(".select-box-filter").css("height", computedHeight); if (this.get("expanded")) { @@ -169,14 +194,14 @@ export default Ember.Component.extend({ this._applyFixedPosition(computedWidth, computedHeight); } - this.$(".select-box-body").css("width", computedWidth); - this.$(".select-box-collection").css("max-height", this.get("maxCollectionHeight")); + this.$(".select-box-collection").css("max-height", this.get("collectionHeight")); - this.applyDirection(); - - if (this.get("wrapper")) { - this._positionSelectBoxWrapper(); - } + Ember.run.schedule("afterRender", () => { + this.applyDirection(); + if (this.get("wrapper")) { + this._positionSelectBoxWrapper(); + } + }); } else { if (this.get("wrapper")) { this.$(".select-box-wrapper").hide(); @@ -184,8 +209,42 @@ export default Ember.Component.extend({ } }, + keyDown(event) { + const keyCode = event.keyCode || event.which; + + if (this.get("expanded")) { + if ((keyCode === 13 || keyCode === 9) && Ember.isPresent(this.get("highlightedValue"))) { + event.preventDefault(); + this.send("onSelectRow", this.get("highlightedContent")); + } + + if (keyCode === 9) { + this.set("expanded", false); + } + + if (keyCode === 27) { + this.set("expanded", false); + event.stopPropagation(); + } + + if (keyCode === 38) { + event.preventDefault(); + const self = this; + Ember.run.throttle(self, this._handleUpArrow, 50); + } + + if (keyCode === 40) { + event.preventDefault(); + const self = this; + Ember.run.throttle(self, this._handleDownArrow, 50); + } + } + }, + @on("didRender") _setupDocumentListeners: function() { + $(document).off("click.select-box"); + $(document) .on("click.select-box", (event) => { if (this.isDestroying || this.isDestroyed) { return; } @@ -196,13 +255,6 @@ export default Ember.Component.extend({ if (!$target.closest($element).length) { this.set("expanded", false); } - }) - .on("keydown.select-box", (event) => { - const keyCode = event.keyCode || event.which; - - if (this.get("expanded") && keyCode === 9) { - this.set("expanded", false); - } }); $(window).on("resize.select-box", () => this.set("expanded", false) ); @@ -222,17 +274,12 @@ export default Ember.Component.extend({ const keyCode = event.keyCode || event.which; if (keyCode === 13 || keyCode === 40) { - this.setProperties({expanded: true, focused: false}); - return false; - } - - if (keyCode === 27) { - this.$(".select-box-offscreen").blur(); - return false; + this.setProperties({ expanded: true, focused: false }); + event.stopPropagation(); } if (keyCode >= 65 && keyCode <= 90) { - this.setProperties({expanded: true, focused: false}); + this.setProperties({ expanded: true, focused: false }); Ember.run.schedule("afterRender", () => { this.$(".filter-query").focus().val(String.fromCharCode(keyCode)); }); @@ -243,7 +290,7 @@ export default Ember.Component.extend({ @observes("expanded") _expandedChanged: function() { if (this.get("expanded")) { - this.setProperties({ focused: false, renderBody: true }); + this.setProperties({ highlightedValue: null, renderBody: true, focused: false }); if (this.get("filterable")) { Ember.run.schedule("afterRender", () => this.$(".filter-query").focus()); @@ -251,17 +298,37 @@ export default Ember.Component.extend({ }; }, - @computed("value", "content.[]") - selectedContent(value, content) { + @computed("value", "content.[]", "idKey") + selectedContent(value, content, idKey) { if (Ember.isNone(value)) { return null; } return content.find((c) => { - return this._castInteger(c[this.get("idKey")]) === value; + return this._castInteger(Ember.get(c, idKey)) === value; }); }, + @computed("highlightedValue", "content.[]", "idKey") + highlightedContent(highlightedValue, content, idKey) { + if (Ember.isNone(highlightedValue)) { + return null; + } + + return content.find((c) => { + return this._castInteger(Ember.get(c, idKey)) === highlightedValue; + }); + }, + + @computed("headerText", "selectedContent", "textKey") + selectedTitle(headerText, selectedContent, textKey) { + if (Ember.isNone(selectedContent)) { + return headerText; + } + + return selectedContent[textKey]; + }, + @computed("headerText", "dynamicHeaderText", "selectedContent", "textKey", "clearSelectionLabel") generatedHeadertext(headerText, dynamic, selectedContent, textKey, clearSelectionLabel) { if (dynamic && !Ember.isNone(selectedContent)) { @@ -275,14 +342,18 @@ export default Ember.Component.extend({ return headerText; }, - @computed("content.[]", "filter") - filteredContent(content, filter) { + @computed("content.[]", "filter", "idKey") + filteredContent(content, filter, idKey) { let filteredContent; if (Ember.isEmpty(filter)) { filteredContent = content; } else { filteredContent = this.filterFunction(content)(this); + + if (!Ember.isEmpty(filteredContent)) { + this.set("highlightedValue", filteredContent[0][idKey]); + } } return filteredContent; @@ -302,19 +373,20 @@ export default Ember.Component.extend({ this.set("filter", filter); }, + onHoverRow(content) { + const id = this._castInteger(Ember.get(content, this.get("idKey"))); + this.set("highlightedValue", id); + }, + onSelectRow(content) { this.setProperties({ - value: this._castInteger(content[this.get("idKey")]), + value: this._castInteger(Ember.get(content, this.get("idKey"))), expanded: false }); }, onClearSelection() { this.setProperties({ value: null, expanded: false }); - }, - - onHoverRow(content) { - this.set("lastHovered", this._castInteger(content[this.get("idKey")])); } }, @@ -362,5 +434,45 @@ export default Ember.Component.extend({ }); this.get("scrollableParent").off("scroll.select-box"); + }, + + _handleDownArrow() { + this._handleArrow("down"); + }, + + _handleUpArrow() { + this._handleArrow("up"); + }, + + _handleArrow(direction) { + const content = this.get("filteredContent"); + const idKey = this.get("idKey"); + const selectedContent = content.findBy(idKey, this.get("highlightedValue")); + const currentIndex = content.indexOf(selectedContent); + + if (direction === "down") { + if (currentIndex < 0) { + this.set("highlightedValue", this._castInteger(Ember.get(content[0], idKey))); + } else if(currentIndex + 1 < content.length) { + this.set("highlightedValue", this._castInteger(Ember.get(content[currentIndex + 1], idKey))); + } + } else { + if (currentIndex <= 0) { + this.set("highlightedValue", this._castInteger(Ember.get(content[0], idKey))); + } else if(currentIndex - 1 < content.length) { + this.set("highlightedValue", this._castInteger(Ember.get(content[currentIndex - 1], idKey))); + } + } + + Ember.run.schedule("afterRender", () => { + const $highlightedRow = this.$(".select-box-row.is-highlighted"); + + if ($highlightedRow.length === 0) { return; } + + const $collection = this.$(".select-box-collection"); + const rowOffset = $highlightedRow.offset(); + const bodyOffset = $collection.offset(); + $collection.scrollTop(rowOffset.top - bodyOffset.top); + }); } }); diff --git a/app/assets/javascripts/discourse/components/select-box/select-box-header.js.es6 b/app/assets/javascripts/discourse/components/select-box/select-box-header.js.es6 index 57d5f066ab..033af31237 100644 --- a/app/assets/javascripts/discourse/components/select-box/select-box-header.js.es6 +++ b/app/assets/javascripts/discourse/components/select-box/select-box-header.js.es6 @@ -9,8 +9,9 @@ export default Ember.Component.extend({ this._setCaretIcon(); }, - click() { + click(event) { this.sendAction("onToggle"); + event.stopPropagation(); }, _setCaretIcon() { diff --git a/app/assets/javascripts/discourse/components/select-box/select-box-row.js.es6 b/app/assets/javascripts/discourse/components/select-box/select-box-row.js.es6 index 4c47769e19..f0e359dafb 100644 --- a/app/assets/javascripts/discourse/components/select-box/select-box-row.js.es6 +++ b/app/assets/javascripts/discourse/components/select-box/select-box-row.js.es6 @@ -1,4 +1,5 @@ import computed from 'ember-addons/ember-computed-decorators'; +import { iconHTML } from "discourse-common/lib/icon-library"; export default Ember.Component.extend({ layoutName: "components/select-box/select-box-row", @@ -9,7 +10,7 @@ export default Ember.Component.extend({ attributeBindings: ["title"], - classNameBindings: ["isHighlighted:is-highlighted"], + classNameBindings: ["isHighlighted:is-highlighted", "isSelected:is-selected"], @computed("titleForRow") title(titleForRow) { @@ -21,11 +22,26 @@ export default Ember.Component.extend({ return templateForRow(this); }, - @computed("shouldHighlightRow", "lastHovered", "value") + @computed("shouldHighlightRow", "highlightedValue") isHighlighted(shouldHighlightRow) { return shouldHighlightRow(this); }, + @computed("shouldSelectRow", "value") + isSelected(shouldSelectRow) { + return shouldSelectRow(this); + }, + + icon() { + if (this.get("content.icon")) { + const iconName = this.get("content.icon"); + const iconClass = this.get("content.iconClass"); + return iconHTML(iconName, { class: iconClass }); + } + + return null; + }, + mouseEnter() { this.sendAction("onHover", this.get("content")); }, diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6 index 9285f5355b..56731c96df 100644 --- a/app/assets/javascripts/discourse/components/site-header.js.es6 +++ b/app/assets/javascripts/discourse/components/site-header.js.es6 @@ -62,6 +62,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, { this.dispatch('notifications:changed', 'user-notifications'); this.dispatch('header:keyboard-trigger', 'header'); + this.dispatch('search-autocomplete:after-complete', 'search-term'); this.appEvents.on('dom:clean', () => { // For performance, only trigger a re-render if any menu panels are visible diff --git a/app/assets/javascripts/discourse/components/tag-drop.js.es6 b/app/assets/javascripts/discourse/components/tag-drop.js.es6 index 698106c061..201d9a134d 100644 --- a/app/assets/javascripts/discourse/components/tag-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/tag-drop.js.es6 @@ -22,7 +22,7 @@ export default Ember.Component.extend({ @computed('expanded') expandedIcon(expanded) { - return expanded ? 'caret-down' : 'caret-right'; + return expanded ? 'd-drop-expanded' : 'd-drop-collapsed'; }, @computed('tagId') diff --git a/app/assets/javascripts/discourse/components/tag-notifications-button.js.es6 b/app/assets/javascripts/discourse/components/tag-notifications-button.js.es6 index b4588df710..267c40d3ee 100644 --- a/app/assets/javascripts/discourse/components/tag-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/tag-notifications-button.js.es6 @@ -1,11 +1,24 @@ -import NotificationsButton from 'discourse/components/notifications-button'; +import NotificationOptionsComponent from "discourse/components/notifications-button"; +import computed from "ember-addons/ember-computed-decorators"; +import { iconHTML } from "discourse-common/lib/icon-library"; -export default NotificationsButton.extend({ - classNames: ['notification-options', 'tag-notification-menu'], - buttonIncludesText: false, - i18nPrefix: 'tagging.notifications', +export default NotificationOptionsComponent.extend({ + classNames: ["tag-notifications-button"], - clicked(id) { - this.sendAction('action', id); + i18nPrefix: "tagging.notifications", + + @computed("value") + icon() { + return `${this._super()}${iconHTML("caret-down")}`.htmlSafe(); + }, + + generatedHeadertext: null, + + actions: { + onSelectRow(content) { + this._super(content); + + this.sendAction("action", this.get("value")); + } } }); diff --git a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 index 13473c0e0f..e370f0d337 100644 --- a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 @@ -15,7 +15,7 @@ export default Ember.Component.extend({ @computed showAdminButton() { - return !this.site.mobileView && this.currentUser.get('canManageTopic'); + return !this.site.mobileView && this.currentUser && this.currentUser.get('canManageTopic'); }, @computed('topic.message_archived') diff --git a/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 b/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 index af7ac64574..2395e43c78 100644 --- a/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 @@ -8,7 +8,7 @@ export default SelectBoxComponent.extend({ dynamicHeaderText: false, - maxCollectionHeight: 300, + collectionHeight: 300, init() { this._super(); diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 index f4227cca1e..0d887ba932 100644 --- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 @@ -23,7 +23,7 @@ export function showEntrance(e) { export default Ember.Component.extend(bufferedRender({ rerenderTriggers: ['bulkSelectEnabled', 'topic.pinned'], tagName: 'tr', - classNameBindings: [':topic-list-item', 'unboundClassNames'], + classNameBindings: [':topic-list-item', 'unboundClassNames', 'topic.visited'], attributeBindings: ['data-topic-id'], 'data-topic-id': Em.computed.alias('topic.id'), diff --git a/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 b/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 index b2f5ff0511..c174495ba7 100644 --- a/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 @@ -1,21 +1,9 @@ -import MountWidget from 'discourse/components/mount-widget'; -import { observes } from 'ember-addons/ember-computed-decorators'; +export default Ember.Component.extend({ + layoutName: "components/topic-notifications-button", -export default MountWidget.extend({ - classNames: ['topic-notifications-container'], - widget: 'topic-notifications-button', + classNames: ["topic-notifications-button"], - buildArgs() { - return { topic: this.get('topic'), appendReason: true, showFullTitle: true }; - }, + showFullTitle: true, - @observes('topic.details.notification_level') - _queueRerender() { - this.queueRerender(); - }, - - didInsertElement() { - this._super(); - this.dispatch('topic-notifications-button:changed', 'topic-notifications-button'); - } + appendReason: true, }); diff --git a/app/assets/javascripts/discourse/components/topic-notifications-options.js.es6 b/app/assets/javascripts/discourse/components/topic-notifications-options.js.es6 new file mode 100644 index 0000000000..03b4a17534 --- /dev/null +++ b/app/assets/javascripts/discourse/components/topic-notifications-options.js.es6 @@ -0,0 +1,52 @@ +import NotificationOptionsComponent from "discourse/components/notifications-button"; +import { on } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; +import { topicLevels, buttonDetails } from "discourse/lib/notification-levels"; + +export default NotificationOptionsComponent.extend({ + classNames: ["topic-notifications-options"], + + content: topicLevels, + + i18nPrefix: "topic.notifications", + + value: Ember.computed.alias("topic.details.notification_level"), + + @on("didInsertElement") + _bindGlobalLevelChanged() { + this.appEvents.on("topic-notifications-button:changed", (msg) => { + if (msg.type === "notification") { + if (this.get("value") !== msg.id) { + this.get("topic.details").updateNotifications(msg.id); + } + } + }); + }, + + @on("willDestroyElement") + _unbindGlobalLevelChanged() { + this.appEvents.off("topic-notifications-button:changed"); + }, + + @computed("value", "showFullTitle") + generatedHeadertext(value, showFullTitle) { + if (showFullTitle) { + const details = buttonDetails(value); + return I18n.t(`topic.notifications.${details.key}.title`); + } else { + return null; + } + }, + + actions: { + onSelectRow(content) { + const notificationLevelId = Ember.get(content, this.get("idKey")); + + if (notificationLevelId !== this.get("value")) { + this.get("topic.details").updateNotifications(notificationLevelId); + } + + this._super(content); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 index cc72ba7f2c..cfa9340e94 100644 --- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 @@ -84,6 +84,5 @@ export default MountWidget.extend(Docking, { } this.dispatch('topic:current-post-scrolled', 'timeline-scrollarea'); - this.dispatch('topic-notifications-button:changed', 'topic-notifications-button'); } }); diff --git a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 index 252b42732d..16ea751834 100644 --- a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 @@ -4,7 +4,7 @@ import Category from 'discourse/models/category'; export default Ember.Component.extend(bufferedRender({ classNames: ['topic-status-info'], - delayedRerender: null, + _delayedRerender: null, rerenderTriggers: [ 'statusType', @@ -58,12 +58,14 @@ export default Ember.Component.extend(bufferedRender({ buffer.push(''); // TODO Sam: concerned this can cause a heavy rerender loop - this.set('delayedRerender', Em.run.later(this, this.rerender, rerenderDelay)); + if (!Ember.testing) { + this._delayedRerender = Ember.run.later(this, this.rerender, rerenderDelay); + } }, willDestroyElement() { - if( this.delayedRerender ) { - Em.run.cancel(this.get('delayedRerender')); + if (this._delayedRerender) { + Em.run.cancel(this._delayedRerender); } }, diff --git a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 index ef7d23eba7..4b2a19d457 100644 --- a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 @@ -25,7 +25,7 @@ function addBulkButton(action, key, opts) { addBulkButton('showChangeCategory', 'change_category', {icon: 'pencil'}); addBulkButton('closeTopics', 'close_topics', {icon: 'lock'}); addBulkButton('archiveTopics', 'archive_topics', {icon: 'folder'}); -addBulkButton('showNotificationLevel', 'notification_level', {icon: 'circle-o'}); +addBulkButton('showNotificationLevel', 'notification_level', {icon: 'd-regular'}); addBulkButton('resetRead', 'reset_read', {icon: 'backward'}); addBulkButton('unlistTopics', 'unlist_topics', { icon: 'eye-slash', diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 79d10418bb..bb8500f08d 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -827,7 +827,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { this.messageBus.subscribe(`/topic/${this.get('model.id')}`, data => { const topic = this.get('model'); - if (data.notification_level_change) { + if (Ember.isPresent(data.notification_level_change)) { topic.set('details.notification_level', data.notification_level_change); topic.set('details.notifications_reason_id', data.notifications_reason_id); return; diff --git a/app/assets/javascripts/discourse/helpers/topic-link.js.es6 b/app/assets/javascripts/discourse/helpers/topic-link.js.es6 index b71649495b..ac85252c61 100644 --- a/app/assets/javascripts/discourse/helpers/topic-link.js.es6 +++ b/app/assets/javascripts/discourse/helpers/topic-link.js.es6 @@ -7,10 +7,6 @@ registerUnbound('topic-link', (topic, args) => { topic.get('lastUnreadUrl'); const classes = ['title']; - if (topic.get('last_read_post_number') === topic.get('highest_post_number')) { - classes.push('visited'); - } - if (args.class) { args.class.split(" ").forEach(c => classes.push(c)); } diff --git a/app/assets/javascripts/discourse/lib/notification-levels.js.es6 b/app/assets/javascripts/discourse/lib/notification-levels.js.es6 index 6d744790e2..adeae1bcaa 100644 --- a/app/assets/javascripts/discourse/lib/notification-levels.js.es6 +++ b/app/assets/javascripts/discourse/lib/notification-levels.js.es6 @@ -9,15 +9,15 @@ export const NotificationLevels = { WATCHING_FIRST_POST, WATCHING, TRACKING, REG export function buttonDetails(level) { switch(level) { case WATCHING_FIRST_POST: - return { id: WATCHING_FIRST_POST, key: 'watching_first_post', icon: 'dot-circle-o' }; + return { id: WATCHING_FIRST_POST, key: 'watching_first_post', icon: 'd-watching-first' }; case WATCHING: - return { id: WATCHING, key: 'watching', icon: 'exclamation-circle' }; + return { id: WATCHING, key: 'watching', icon: 'd-watching' }; case TRACKING: - return { id: TRACKING, key: 'tracking', icon: 'circle' }; + return { id: TRACKING, key: 'tracking', icon: 'd-tracking' }; case MUTED: - return { id: MUTED, key: 'muted', icon: 'times-circle' }; + return { id: MUTED, key: 'muted', icon: 'd-muted' }; default: - return { id: REGULAR, key: 'regular', icon: 'circle-o' }; + return { id: REGULAR, key: 'regular', icon: 'd-regular' }; } } diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index 7df3954b30..5faac346ec 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -35,6 +35,7 @@ export function transformBasicPost(post) { primary_group_flair_bg_color: post.primary_group_flair_bg_color, primary_group_flair_color: post.primary_group_flair_color, wiki: post.wiki, + lastWikiEdit: post.last_wiki_edit, firstPost: post.post_number === 1, post_number: post.post_number, cooked: post.cooked, diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index c06d596630..4c6d31ce15 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -2,6 +2,9 @@ import { ajax } from 'discourse/lib/ajax'; import { default as computed, observes } from "ember-addons/ember-computed-decorators"; import GroupHistory from 'discourse/models/group-history'; import RestModel from 'discourse/models/rest'; +import Category from "discourse/models/category"; +import User from "discourse/models/user"; +import Topic from "discourse/models/topic"; const Group = RestModel.extend({ limit: 50, @@ -44,9 +47,9 @@ const Group = RestModel.extend({ if (ownerIds[member.id]) { member.owner = true; } - return Discourse.User.create(member); + return User.create(member); }), - owners: result.owners.map(owner => Discourse.User.create(owner)), + owners: result.owners.map(owner => User.create(owner)), }); }); }, @@ -207,8 +210,9 @@ const Group = RestModel.extend({ return ajax(`/groups/${this.get('name')}/${type}.json`, { data: data }).then(posts => { return posts.map(p => { - p.user = Discourse.User.create(p.user); - p.topic = Discourse.Topic.create(p.topic); + p.user = User.create(p.user); + p.topic = Topic.create(p.topic); + p.category = Category.findById(p.category_id); return Em.Object.create(p); }); }); diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index f8e75e517f..60282358f0 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -33,6 +33,11 @@ const Topic = RestModel.extend({ message: null, errorLoading: false, + @computed('last_read_post_number', 'highest_post_number') + visited(lastReadPostNumber, highestPostNumber) { + return lastReadPostNumber === highestPostNumber; + }, + @computed('posters.firstObject') creator(poster){ return poster && poster.user; @@ -407,6 +412,10 @@ const Topic = RestModel.extend({ }); }, + @computed('excerpt') + escapedExcerpt(excerpt) { + return emojiUnescape(excerpt); + }, hasExcerpt: Em.computed.notEmpty('excerpt'), diff --git a/app/assets/javascripts/discourse/routes/new-message.js.es6 b/app/assets/javascripts/discourse/routes/new-message.js.es6 index 06d929c552..91269c4a5b 100644 --- a/app/assets/javascripts/discourse/routes/new-message.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-message.js.es6 @@ -34,7 +34,11 @@ export default Discourse.Route.extend({ }); } else { $.cookie('destination_url', window.location.href); - this.replaceWith('login'); + if (Discourse.showingSignup) { + Discourse.showingSignup = false; + } else { + self.replaceWith('login'); + } } } diff --git a/app/assets/javascripts/discourse/routes/new-topic.js.es6 b/app/assets/javascripts/discourse/routes/new-topic.js.es6 index 88dcd61368..0459e75d4c 100644 --- a/app/assets/javascripts/discourse/routes/new-topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-topic.js.es6 @@ -14,7 +14,12 @@ export default Discourse.Route.extend({ } else { // User is not logged in $.cookie('destination_url', window.location.href); - self.replaceWith('login'); + if (Discourse.showingSignup) { + // We're showing the sign up modal + Discourse.showingSignup = false; + } else { + self.replaceWith('login'); + } } } }); diff --git a/app/assets/javascripts/discourse/templates/components/category-drop.hbs b/app/assets/javascripts/discourse/templates/components/category-drop.hbs index d660be87db..ba5062a429 100644 --- a/app/assets/javascripts/discourse/templates/components/category-drop.hbs +++ b/app/assets/javascripts/discourse/templates/components/category-drop.hbs @@ -4,7 +4,7 @@ {{#if category.read_restricted}} {{d-icon "lock"}} {{/if}} - {{category.name}} + {{category.name}} {{else}} {{#if noSubcategories}} diff --git a/app/assets/javascripts/discourse/templates/components/dropdown-select-box/dropdown-header.hbs b/app/assets/javascripts/discourse/templates/components/dropdown-select-box/dropdown-header.hbs new file mode 100644 index 0000000000..be525a9fd7 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/dropdown-select-box/dropdown-header.hbs @@ -0,0 +1,12 @@ + diff --git a/app/assets/javascripts/discourse/templates/components/pinned-button.hbs b/app/assets/javascripts/discourse/templates/components/pinned-button.hbs new file mode 100644 index 0000000000..aaabc2f1bf --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/pinned-button.hbs @@ -0,0 +1,5 @@ +{{pinned-options topic=topic}} + +

+ {{{reasonText}}} +

diff --git a/app/assets/javascripts/discourse/templates/components/select-box.hbs b/app/assets/javascripts/discourse/templates/components/select-box.hbs index 338c850b88..c20f76e2e3 100644 --- a/app/assets/javascripts/discourse/templates/components/select-box.hbs +++ b/app/assets/javascripts/discourse/templates/components/select-box.hbs @@ -9,6 +9,7 @@ {{component selectBoxHeaderComponent text=generatedHeadertext + selectedTitle=selectedTitle focused=focused caretUpIcon=caretUpIcon caretDownIcon=caretDownIcon @@ -26,6 +27,7 @@ icon=filterIcon focused=filterFocused placeholder=filterPlaceholder + tabindex=tabindex }} {{/if}} @@ -35,12 +37,13 @@ selectBoxRowComponent=selectBoxRowComponent templateForRow=templateForRow shouldHighlightRow=shouldHighlightRow + shouldSelectRow=shouldSelectRow titleForRow=titleForRow - lastHovered=lastHovered onSelectRow=(action "onSelectRow") onHoverRow=(action "onHoverRow") onClearSelection=(action "onClearSelection") noContentLabel=noContentLabel + highlightedValue=highlightedValue value=value }} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/select-box/select-box-collection.hbs b/app/assets/javascripts/discourse/templates/components/select-box/select-box-collection.hbs index 1e0aae5268..3d77b5050e 100644 --- a/app/assets/javascripts/discourse/templates/components/select-box/select-box-collection.hbs +++ b/app/assets/javascripts/discourse/templates/components/select-box/select-box-collection.hbs @@ -11,7 +11,8 @@ templateForRow=templateForRow titleForRow=titleForRow shouldHighlightRow=shouldHighlightRow - lastHovered=lastHovered + shouldSelectRow=shouldSelectRow + highlightedValue=highlightedValue onSelect=onSelectRow onHover=onHoverRow value=value diff --git a/app/assets/javascripts/discourse/templates/components/select-box/select-box-filter.hbs b/app/assets/javascripts/discourse/templates/components/select-box/select-box-filter.hbs index 3ba9d15504..c9a06c3fc5 100644 --- a/app/assets/javascripts/discourse/templates/components/select-box/select-box-filter.hbs +++ b/app/assets/javascripts/discourse/templates/components/select-box/select-box-filter.hbs @@ -1,5 +1,5 @@ {{input - tabindex="-1" + tabindex=tabindex class="filter-query" placeholder=placeholder key-up=onFilterChange diff --git a/app/assets/javascripts/discourse/templates/components/select-box/select-box-header.hbs b/app/assets/javascripts/discourse/templates/components/select-box/select-box-header.hbs index cf3812c0db..b07be58017 100644 --- a/app/assets/javascripts/discourse/templates/components/select-box/select-box-header.hbs +++ b/app/assets/javascripts/discourse/templates/components/select-box/select-box-header.hbs @@ -2,7 +2,7 @@ {{d-icon icon class="icon"}} {{/if}} - + {{text}} diff --git a/app/assets/javascripts/discourse/templates/components/topic-notifications-button.hbs b/app/assets/javascripts/discourse/templates/components/topic-notifications-button.hbs new file mode 100644 index 0000000000..17d9953055 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/topic-notifications-button.hbs @@ -0,0 +1,7 @@ +{{topic-notifications-options topic=topic showFullTitle=showFullTitle}} + +{{#if appendReason}} +

+ {{{topic.details.notificationReasonText}}} +

+{{/if}} diff --git a/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs b/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs index 76be2c412f..0a42a6a0c5 100644 --- a/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs @@ -1,6 +1,6 @@ {{#if topic.hasExcerpt}}
- {{{topic.excerpt}}} + {{{topic.escapedExcerpt}}} {{#if topic.excerptTruncated}} {{i18n 'read_more'}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/mobile/components/basic-topic-list.hbs b/app/assets/javascripts/discourse/templates/mobile/components/basic-topic-list.hbs index 8abe6bc1fa..d761215bec 100644 --- a/app/assets/javascripts/discourse/templates/mobile/components/basic-topic-list.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/components/basic-topic-list.hbs @@ -3,7 +3,7 @@ {{#each topics as |t|}} - +
{{/d-section}} - diff --git a/app/assets/javascripts/discourse/templates/preferences/categories.hbs b/app/assets/javascripts/discourse/templates/preferences/categories.hbs index 856feedcf7..27a07fef11 100644 --- a/app/assets/javascripts/discourse/templates/preferences/categories.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/categories.hbs @@ -3,7 +3,7 @@
- + {{category-selector categories=model.watchedCategories blacklist=selectedCategories}}
{{i18n 'user.watched_categories_instructions'}}
@@ -12,7 +12,7 @@
- + {{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
{{i18n 'user.tracked_categories_instructions'}}
@@ -21,13 +21,13 @@
- + {{category-selector categories=model.watchedFirstPostCategories}}
{{i18n 'user.watched_first_post_categories_instructions'}}
- + {{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
{{i18n 'user.muted_categories_instructions'}}
diff --git a/app/assets/javascripts/discourse/templates/preferences/notifications.hbs b/app/assets/javascripts/discourse/templates/preferences/notifications.hbs index c9cf37ac4d..0ddeefe998 100644 --- a/app/assets/javascripts/discourse/templates/preferences/notifications.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/notifications.hbs @@ -29,7 +29,7 @@
- + {{user-selector excludeCurrentUser=true usernames=model.muted_usernames class="user-selector"}}
{{i18n 'user.muted_users_instructions'}}
diff --git a/app/assets/javascripts/discourse/templates/preferences/tags.hbs b/app/assets/javascripts/discourse/templates/preferences/tags.hbs index 390429833f..1c24147a2f 100644 --- a/app/assets/javascripts/discourse/templates/preferences/tags.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/tags.hbs @@ -4,25 +4,25 @@
- + {{tag-chooser tags=model.watched_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
{{i18n 'user.watched_tags_instructions'}}
- + {{tag-chooser tags=model.tracked_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
{{i18n 'user.tracked_tags_instructions'}}
- + {{tag-chooser tags=model.watching_first_post_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
{{i18n 'user.watched_first_post_tags_instructions'}}
- + {{tag-chooser tags=model.muted_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
{{i18n 'user.muted_tags_instructions'}}
diff --git a/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs b/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs index f330d1046b..54947fbdaa 100644 --- a/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs +++ b/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs @@ -3,8 +3,11 @@

{{{stopNotificiationsText}}}

+

- {{i18n "topic.unsubscribe.change_notification_state"}} {{topic-notifications-button topic=model}} + {{i18n "topic.unsubscribe.change_notification_state"}}

+ + {{topic-notifications-button topic=model}}
diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 5634f64102..c2897679c6 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -116,7 +116,6 @@
- {{#unless collapsedInfo}}
diff --git a/app/assets/javascripts/discourse/widgets/component_connector.js.es6 b/app/assets/javascripts/discourse/widgets/component_connector.js.es6 new file mode 100644 index 0000000000..7484fad179 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/component_connector.js.es6 @@ -0,0 +1,35 @@ +export default class ComponentConnector { + constructor(widget, componentName, opts) { + this.widget = widget; + this.opts = opts; + this.componentName = componentName; + } + + init() { + const $elem = $('
'); + const elem = $elem[0]; + const { opts, widget, componentName } = this; + + Ember.run.next(() => { + const mounted = widget._findView(); + + const view = widget + .register + .lookupFactory(`component:${componentName}`) + .create(opts); + + if (Ember.setOwner) { + Ember.setOwner(view, Ember.getOwner(mounted)); + } + + mounted._connected.push(view); + view.renderer.appendTo(view, $elem[0]); + }); + + return elem; + } + + update() { } +} + +ComponentConnector.prototype.type = 'Widget'; diff --git a/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 b/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 index 11036d8541..f3476faec4 100644 --- a/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 @@ -2,21 +2,21 @@ import PostCooked from 'discourse/widgets/post-cooked'; import DecoratorHelper from 'discourse/widgets/decorator-helper'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; -import { iconNode } from 'discourse-common/lib/icon-library'; import DiscourseURL from 'discourse/lib/url'; +import hbs from 'discourse/widgets/hbs-compiler'; createWidget('post-link-arrow', { - html(attrs) { - if (attrs.above) { - return h('a.post-info.arrow', { - attributes: { title: I18n.t('topic.jump_reply_up') } - }, iconNode('arrow-up')); - } else { - return h('a.post-info.arrow', { - attributes: { title: I18n.t('topic.jump_reply_down') } - }, iconNode('arrow-down')); - } - }, + template: hbs` + {{#if attrs.above}} + + {{else}} + + {{/if}} + `, click() { DiscourseURL.routeTo(this.attrs.shareUrl); diff --git a/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6 b/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6 new file mode 100644 index 0000000000..ef58ffc796 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6 @@ -0,0 +1,3 @@ +export default function hbs() { + console.log('Templates should be precompiled server side'); +} diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 index 10f0f97c24..1c7c1e6ce6 100644 --- a/app/assets/javascripts/discourse/widgets/header.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -28,23 +28,26 @@ createWidget('header-notifications', { }, html(attrs) { - const { currentUser } = this; + const { user } = attrs; const contents = [ avatarImg(this.settings.avatarSize, { - template: currentUser.get('avatar_template'), - username: currentUser.get('username') + template: user.get('avatar_template'), + username: user.get('username') }) ]; - const unreadNotifications = currentUser.get('unread_notifications'); + const unreadNotifications = user.get('unread_notifications'); if (!!unreadNotifications) { - contents.push(this.attach('link', { action: attrs.action, - className: 'badge-notification unread-notifications', - rawLabel: unreadNotifications })); + contents.push(this.attach('link', { + action: attrs.action, + className: 'badge-notification unread-notifications', + rawLabel: unreadNotifications, + omitSpan: true + })); } - const unreadPMs = currentUser.get('unread_private_messages'); + const unreadPMs = user.get('unread_private_messages'); if (!!unreadPMs) { - if (!currentUser.get('read_first_notification')) { + if (!user.get('read_first_notification')) { contents.push(h('span.ring')); if (!attrs.active && attrs.ringBackdrop) { contents.push(h('span.ring-backdrop-spotlight')); @@ -55,9 +58,12 @@ createWidget('header-notifications', { } }; - contents.push(this.attach('link', { action: attrs.action, - className: 'badge-notification unread-private-messages', - rawLabel: unreadPMs })); + contents.push(this.attach('link', { + action: attrs.action, + className: 'badge-notification unread-private-messages', + rawLabel: unreadPMs, + omitSpan: true + })); } return contents; @@ -72,9 +78,7 @@ createWidget('user-dropdown', jQuery.extend({ }, html(attrs) { - const { currentUser } = this; - - return h('a.icon', { attributes: { href: currentUser.get('path'), 'data-auto-route': true } }, + return h('a.icon', { attributes: { href: attrs.user.get('path'), 'data-auto-route': true } }, this.attach('header-notifications', attrs)); } }, dropdown)); @@ -106,7 +110,7 @@ createWidget('header-dropdown', jQuery.extend({ }, dropdown)); createWidget('header-icons', { - tagName: 'ul.icons.clearfix', + tagName: 'ul.icons.d-header-icons.clearfix', buildAttributes() { return { role: 'navigation' }; @@ -139,10 +143,13 @@ createWidget('header-icons', { }); const icons = [search, hamburger]; - if (this.currentUser) { - icons.push(this.attach('user-dropdown', { active: attrs.userVisible, - action: 'toggleUserMenu', - ringBackdrop: attrs.ringBackdrop })); + if (attrs.user) { + icons.push(this.attach('user-dropdown', { + active: attrs.userVisible, + action: 'toggleUserMenu', + ringBackdrop: attrs.ringBackdrop, + user: attrs.user + })); } return icons; @@ -204,7 +211,8 @@ export default createWidget('header', { userVisible: state.userVisible, searchVisible: state.searchVisible, ringBackdrop: state.ringBackdrop, - flagCount: attrs.flagCount })]; + flagCount: attrs.flagCount, + user: this.currentUser })]; if (state.searchVisible) { const contextType = this.searchContextType(); diff --git a/app/assets/javascripts/discourse/widgets/link.js.es6 b/app/assets/javascripts/discourse/widgets/link.js.es6 index c6426a8623..5774ccc763 100644 --- a/app/assets/javascripts/discourse/widgets/link.js.es6 +++ b/app/assets/javascripts/discourse/widgets/link.js.es6 @@ -54,7 +54,13 @@ export default createWidget('link', { } if (!attrs.hideLabel) { - result.push(this.label(attrs)); + let label = this.label(attrs); + + if (attrs.omitSpan) { + result.push(label); + } else { + result.push(h('span.d-label', label)); + } } const currentUser = this.currentUser; diff --git a/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 b/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 index 607da731a2..058b64607b 100644 --- a/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 @@ -1,3 +1,4 @@ +import hbs from 'discourse/widgets/hbs-compiler'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; @@ -23,14 +24,17 @@ createWidget('menu-links', { createWidget('menu-panel', { tagName: 'div.menu-panel', + template: hbs` +
+
+ {{yield}} +
+
+ `, 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())); - } }); diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index f5fefe0d91..deda3b167a 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -173,20 +173,18 @@ registerButton('reply', attrs => { registerButton('bookmark', attrs => { if (!attrs.canBookmark) { return; } - let iconClass = 'read-icon'; - let buttonClass = 'bookmark'; - let tooltip = 'bookmarks.not_bookmarked'; + let className = 'bookmark'; if (attrs.bookmarked) { - iconClass += ' bookmarked'; - buttonClass += ' bookmarked'; - tooltip = 'bookmarks.created'; + className += ' bookmarked'; } - return { action: 'toggleBookmark', - title: tooltip, - className: buttonClass, - contents: h('div', { className: iconClass }) }; + return { + action: 'toggleBookmark', + title: attrs.bookmarked ? "bookmarks.created" : "bookmarks.not_bookmarked", + className, + icon: 'bookmark' + }; }); registerButton('admin', attrs => { diff --git a/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 b/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 index f12fb26f34..1579413613 100644 --- a/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 @@ -1,17 +1,18 @@ import { createWidget } from 'discourse/widgets/widget'; -import { h } from 'virtual-dom'; +import hbs from 'discourse/widgets/hbs-compiler'; export default createWidget('post-placeholder', { tagName: 'article.placeholder', - - html() { - return h('div.row', [ - h('div.topic-avatar', h('div.placeholder-avatar')), - h('div.topic-body', [ - h('div.placeholder-text'), - h('div.placeholder-text'), - h('div.placeholder-text') - ]) - ]); - } + template: hbs` +
+
+
+
+
+
+
+
+
+
+ ` }); diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index 20d6168e8c..4b5acec52d 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -171,19 +171,22 @@ createWidget('post-meta-data', { }, iconNode('eye-slash'))); } + const lastWikiEdit = attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit); const createdAt = new Date(attrs.created_at); - if (createdAt) { - result.push(h('div.post-info', - h('a.post-date', { - attributes: { - href: attrs.shareUrl, - 'data-share-url': attrs.shareUrl, - 'data-post-number': attrs.post_number, - } - }, dateNode(createdAt)) - )); + const date = lastWikiEdit ? dateNode(lastWikiEdit) : dateNode(createdAt); + const attributes = { + class: "post-date", + href: attrs.shareUrl, + 'data-share-url': attrs.shareUrl, + 'data-post-number': attrs.post_number + }; + + if (lastWikiEdit) { + attributes["class"] += " last-wiki-edit"; } + result.push(h('div.post-info', h('a', { attributes }, date))); + if (attrs.via_email) { result.push(this.attach('post-email-indicator', attrs)); } diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 index 60441867e1..2aa4ba4ef0 100644 --- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 @@ -2,13 +2,11 @@ import { iconNode } from 'discourse-common/lib/icon-library'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; import { avatarFor } from 'discourse/widgets/post'; +import hbs from 'discourse/widgets/hbs-compiler'; createWidget('pm-remove-group-link', { tagName: 'a.remove-invited', - - html() { - return iconNode('times'); - }, + template: hbs`{{fa-icon "times"}}`, click() { bootbox.confirm(I18n.t("private_message_info.remove_allowed_group", {name: this.attrs.name}), (confirmed) => { @@ -35,10 +33,7 @@ createWidget('pm-map-user-group', { createWidget('pm-remove-link', { tagName: 'a.remove-invited', - - html() { - return iconNode('times'); - }, + template: hbs`{{fa-icon "times"}}`, click() { bootbox.confirm(I18n.t("private_message_info.remove_allowed_user", {name: this.attrs.username}), (confirmed) => { diff --git a/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 index 689e749ee8..c91a9ccb07 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 @@ -5,16 +5,16 @@ import { createWidget } from 'discourse/widgets/widget'; createWidget('search-term', { tagName: 'input', buildId: () => 'search-term', - buildKey: (attrs) => `search-term-${attrs.id}`, + buildKey: () => `search-term`, defaultState() { - this.appEvents.on("search-autocomplete:after-complete", () => { - this.state.afterAutocomplete = true; - }); - return { afterAutocomplete: false }; }, + searchAutocompleteAfterComplete() { + this.state.afterAutocomplete = true; + }, + buildAttributes(attrs) { return { type: 'text', value: attrs.value || '', diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 index bc974d7c85..b4cd572c96 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -130,15 +130,19 @@ createWidget('search-menu-results', { className: "filter filter-type"}))); } - return [ + let resultNode = [ h('ul', this.attach(rt.componentName, { searchContextEnabled: attrs.searchContextEnabled, searchLogId: attrs.results.grouped_search_result.search_log_id, results: rt.results, term: attrs.term })), - h('div.no-results', more) ]; + if (more.length) { + resultNode.push(h('div.no-results', more)); + } + + return resultNode; }); } }); diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index b5c0f840f3..31df484fe0 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -115,26 +115,29 @@ export default createWidget('search-menu', { panelContents() { const contextEnabled = searchData.contextEnabled; - const results = [ + let searchInput = [ this.attach('search-term', { value: searchData.term, contextEnabled }), + ]; + if (searchData.term && searchData.loading) { + searchInput.push(h('div.searching', h('div.spinner'))); + } + + const results = [ + h('div.search-input', searchInput), this.attach('search-context', { contextEnabled, url: this.fullSearchUrl({ expanded: true }) }) ]; - if (searchData.term) { - if (searchData.loading) { - results.push(h('div.searching', h('div.spinner'))); - } else { - results.push(this.attach('search-menu-results', { - term: searchData.term, - noResults: searchData.noResults, - results: searchData.results, - invalidTerm: searchData.invalidTerm, - searchContextEnabled: searchData.contextEnabled, - })); - } + if (searchData.term && !searchData.loading) { + results.push(this.attach('search-menu-results', { + term: searchData.term, + noResults: searchData.noResults, + results: searchData.results, + invalidTerm: searchData.invalidTerm, + searchContextEnabled: searchData.contextEnabled, + })); } return results; diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index 0564351b56..6cfd0f55f0 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -136,7 +136,8 @@ createWidget('topic-map-link', { target: "_blank", 'data-user-id': attrs.user_id, 'data-ignore-post-id': 'true', - title: attrs.url }; + title: attrs.url, + rel: 'nofollow noopener' }; }, html(attrs) { diff --git a/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 b/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 deleted file mode 100644 index 9293c36cc6..0000000000 --- a/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 +++ /dev/null @@ -1,101 +0,0 @@ -import { createWidget } from 'discourse/widgets/widget'; -import { topicLevels, buttonDetails } from 'discourse/lib/notification-levels'; -import { h } from 'virtual-dom'; -import RawHTML from 'discourse/widgets/raw-html'; -import { iconNode } from 'discourse-common/lib/icon-library'; - -createWidget('notification-option', { - buildKey: attrs => `topic-notifications-button-${attrs.id}`, - tagName: 'li', - - html(attrs) { - return h('a', [ - iconNode(attrs.icon, { class: `icon ${attrs.key}`, tagName: 'span' }), - h('div', [ - h('span.title', I18n.t(`topic.notifications.${attrs.key}.title`)), - h('span.desc', I18n.t(`topic.notifications.${attrs.key}.description`)), - ]) - ]); - }, - - click() { - this.sendWidgetAction('notificationLevelChanged', this.attrs.id); - } -}); - -export default createWidget('topic-notifications-button', { - tagName: 'span.btn-group.notification-options', - buildKey: () => `topic-notifications-button`, - - defaultState() { - return { expanded: false }; - }, - - buildClasses(attrs, state) { - if (state.expanded) { return "open"; } - }, - - buildAttributes() { - return { title: I18n.t('topic.notifications.title') }; - }, - - buttonFor(level) { - const details = buttonDetails(level); - - const button = { - className: `toggle-notification-options`, - label: null, - icon: details.icon, - action: 'toggleDropdown', - iconClass: details.key - }; - - if (this.attrs.showFullTitle) { - button.label = `topic.notifications.${details.key}.title`; - } else { - button.className = 'btn toggle-notifications-options notifications-dropdown'; - } - - return this.attach('button', button); - }, - - html(attrs, state) { - const details = attrs.topic.get('details'); - const result = [ this.buttonFor(details.get('notification_level')) ]; - - if (state.expanded) { - result.push(h('ul.dropdown-menu', topicLevels.map(l => this.attach('notification-option', l)))); - } - - if (attrs.appendReason) { - result.push(new RawHTML({ html: `

${details.get('notificationReasonText')}

` })); - } - - return result; - }, - - toggleDropdown() { - this.state.expanded = !this.state.expanded; - }, - - clickOutside() { - if (this.state.expanded) { - this.sendWidgetAction('toggleDropdown'); - } - }, - - notificationLevelChanged(id) { - this.state.expanded = false; - return this.attrs.topic.get('details').updateNotifications(id); - }, - - topicNotificationsButtonChanged(msg) { - switch(msg.type) { - case 'notification': - if (this.attrs.topic.get('details.notification_level') !== msg.id) { - this.notificationLevelChanged(msg.id); - } - break; - } - } -}); diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 index 06ba942197..2fb698a38c 100644 --- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 @@ -1,4 +1,5 @@ import { createWidget } from 'discourse/widgets/widget'; +import ComponentConnector from 'discourse/widgets/component_connector'; import { h } from 'virtual-dom'; import { relativeAge } from 'discourse/lib/formatter'; import { iconNode } from 'discourse-common/lib/icon-library'; @@ -313,7 +314,14 @@ createWidget('timeline-footer-controls', { } if (currentUser) { - controls.push(this.attach('topic-notifications-button', { topic })); + controls.push(new ComponentConnector(this, + 'topic-notifications-button', + { + topic, + appendReason: false, + showFullTitle: false + } + )); } return controls; diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 2d0ea8d587..8b2c34e8ad 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -112,6 +112,10 @@ export function createWidget(name, opts) { opts.html = opts.html || emptyContent; opts.draw = drawWidget; + if (opts.template) { + opts.html = opts.template; + } + Object.keys(opts).forEach(k => result.prototype[k] = opts[k]); return result; } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 index c2706b7e66..ddb92eefdb 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 @@ -176,9 +176,10 @@ function findInlineCloseTag(state, openTag, start, max) { closeTag = parseBBCodeTag(state.src, j, max); if (!closeTag || closeTag.tag !== openTag.tag || !closeTag.closing) { closeTag = null; + } else { + closeTag.start = j; + break; } - closeTag.start = j; - break; } } } diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index 094ad9a5e8..9c7a814940 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -62,6 +62,7 @@ export function buildOptions(state) { lookupImageUrls, censoredWords, allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null, + allowedIframes: siteSettings.allowed_iframes ? siteSettings.allowed_iframes.split('|') : [], markdownIt: true, previewing }; diff --git a/app/assets/javascripts/pretty-text/sanitizer.js.es6 b/app/assets/javascripts/pretty-text/sanitizer.js.es6 index d865c9be5c..f62df9d4bb 100644 --- a/app/assets/javascripts/pretty-text/sanitizer.js.es6 +++ b/app/assets/javascripts/pretty-text/sanitizer.js.es6 @@ -1,7 +1,5 @@ import xss from 'pretty-text/xss'; -const _validIframes = []; - function attr(name, value) { if (value) { return `${name}="${xss.escapeAttrValue(value)}"`; @@ -69,7 +67,8 @@ export function sanitize(text, whiteLister) { text = text.replace(/<([^A-Za-z\/\!]|$)/g, "<$1"); const whiteList = whiteLister.getWhiteList(), - allowedHrefSchemes = whiteLister.getAllowedHrefSchemes(); + allowedHrefSchemes = whiteLister.getAllowedHrefSchemes(), + allowedIframes = whiteLister.getAllowedIframes(); let extraHrefMatchers = null; if (allowedHrefSchemes && allowedHrefSchemes.length > 0) { @@ -85,11 +84,13 @@ export function sanitize(text, whiteLister) { const forTag = whiteList.attrList[tag]; if (forTag) { const forAttr = forTag[name]; - if ((forAttr && (forAttr.indexOf('*') !== -1 || forAttr.indexOf(value) !== -1)) || + if ( + (forAttr && (forAttr.indexOf('*') !== -1 || forAttr.indexOf(value) !== -1)) || (name.indexOf('data-') === 0 && forTag['data-*']) || ((tag === 'a' && name === 'href') && hrefAllowed(value, extraHrefMatchers)) || (tag === 'img' && name === 'src' && (/^data:image.*$/i.test(value) || hrefAllowed(value, extraHrefMatchers))) || - (tag === 'iframe' && name === 'src' && _validIframes.some(i => i.test(value)))) { + (tag === 'iframe' && name === 'src' && allowedIframes.some(i => { return value.toLowerCase().indexOf((i || '').toLowerCase()) === 0;})) + ) { return attr(name, value); } @@ -114,10 +115,3 @@ export function sanitize(text, whiteLister) { .replace(/'/g, "'") .replace(/ \/>/g, '>'); }; - -export function whiteListIframe(regexp) { - _validIframes.push(regexp); -} - -whiteListIframe(/^(https?:)?\/\/www\.google\.com\/maps\/embed\?.+/i); -whiteListIframe(/^(https?:)?\/\/www\.openstreetmap\.org\/export\/embed.html\?.+/i); diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index 30f8f23c96..cc022a827f 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -9,6 +9,7 @@ export default class WhiteLister { this._enabled = { "default": true }; this._allowedHrefSchemes = (options && options.allowedHrefSchemes) || []; + this._allowedIframes = (options && options.allowedIframes) || []; this._rawFeatures = [["default", DEFAULT_LIST]]; this._cache = null; @@ -102,6 +103,10 @@ export default class WhiteLister { getAllowedHrefSchemes() { return this._allowedHrefSchemes; } + + getAllowedIframes() { + return this._allowedIframes; + } } // Only add to `default` when you always want your whitelist to occur. In other words, diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index ccd2ff1c07..4462fca340 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -14,10 +14,24 @@ font: 1.071em/0.9 "FontAwesome"; } } + + .select-box { + align-self: center; + + &.categories-admin-dropdown, &.category-notifications-button, &.tag-notifications-button { + float: right; + margin-left: 8px; + } + } } -html.anon .topic-list a.title:visited:not(.badge-notification) {color: $primary-medium; } -.topic-list a.title.visited:not(.badge-notification) {color: $primary-medium; } +html.anon .topic-list, +.topic-list-item.visited, +.latest-topic-list-item.visited, +.category-topic-link.visited, +#suggested-topics .topic-list tr.visited { + a.title:not(.badge-notification) { color: $primary-medium; } +} .topic-list-main-link { font-size: 1.143em; @@ -47,7 +61,7 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: $primary- vertical-align: top; margin-top: 2px; } - border-bottom: 1px solid $primary-low; + border-bottom: 1px solid $primary-low; &.last-visit { border-bottom: none; @@ -253,7 +267,7 @@ ol.category-breadcrumb { overflow-x: hidden; overflow-y: auto; position: absolute; - border: 1px solid $primary-low; + border: 1px solid $primary-low; background-color: $secondary; z-index: 100; @@ -315,7 +329,7 @@ ol.category-breadcrumb { @include unselectable; font-size: 1.2em; - border: 1px solid $primary-low; + border: 1px solid $primary-low; padding: 5px; background: $secondary; position: absolute; @@ -369,4 +383,3 @@ div.education { @extend .list-cell; border-bottom: 2px solid $primary-low; } - diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 02709d7da8..2cca6788a5 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -1,45 +1,39 @@ -img.avatar { - border-radius: 50%; -} +// Common +// global styles that apply to the Discourse application specifically +// BEWARE: changing these styles implies they take effect anywhere they are seen +// throughout the Discourse application -.container { - @extend .clearfix; -} - -.wrap { - @extend .clearfix; - margin-right: auto; - margin-left: auto; - padding: 0 8px; - .contents { - position: relative; +// Animation Keyframes +@keyframes ping { + from { + transform: scale(0.25); + opacity: 1; + } + to { + transform: scale(2); + opacity: 0; } } -.full-width { - margin-left: 12px; +@keyframes rotate-forever { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } -big { - font-size: 28px; -} - -small { - font-size: 9px; -} - -//setting a static limit on big and small prevents nesting abuse - - -blockquote { - @include post-aside; - clear: both; -} - -a.no-href { - cursor: pointer; +@keyframes background-fade-highlight { + 0% { + background-color: $tertiary-low; + } + 100% { + background-color: transparent; + } } +// Base Elements html { height: 100%; } @@ -52,56 +46,170 @@ body { @include clearfix; } -h1, h2, h3, h4, h5, h6 { +big { + font-size: 28px; +} + +small { + font-size: 9px; +} + +//setting a static limit on big and small prevents nesting abuse +blockquote { + @include post-aside; + clear: both; +} + +h1, +h2, +h3, +h4, +h5, +h6 { margin-top: 0; margin-bottom: .5rem; } -button.ok { - background: $success; - color: $secondary; - @include hover { - background: lighten($success, 10%); +button { + &.ok { + background: $success; color: $secondary; + + @include hover { + background: lighten($success, 10%); + color: $secondary; + } + } + + &.cancel { + background: $danger; + color: $secondary; + + @include hover { + background: lighten($danger, 10%); + color: $secondary; + } } } -button.cancel { - background: $danger; - color: $secondary; - @include hover { - background: lighten($danger, 10%); - color: $secondary; +ul.breadcrumb { + margin: 0 10px 0 10px; +} + +a.no-href { + cursor: pointer; +} + +img.avatar { + border-radius: 50%; +} + +// don't wrap relative dates; we want Jul 26, '15, not: Jul +// 26, +// '15 +span.relative-date { + white-space:nowrap; +} + +label { + display: block; + margin-bottom: 5px; +} + +input { + &[type="radio"], + &[type="checkbox"] { + margin: 3px 0; + line-height: normal; + cursor: pointer; } + &[type="submit"], + &[type="reset"], + &[type="button"], + &[type="radio"], + &[type="checkbox"] { + width: auto; + } + + &.invalid { + background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%)); + } + + .radio &[type="radio"], + .checkbox &[type="checkbox"] { + float: left; + margin-left: -18px; + } +} + +// Common Classes +.radio, +.checkbox { + min-height: 18px; + padding-left: 18px; + + .controls > &:first-child { + padding-top: 5px; + } + + &.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; + } +} + +.radio.inline .radio.inline, +.checkbox.inline .checkbox.inline { + margin-left: 10px; +} + +.container { + @extend .clearfix; +} + +.wrap { + @extend .clearfix; + margin-right: auto; + margin-left: auto; + padding: 0 8px; + + .contents { + position: relative; + } +} + +.boxed { + &.white { + background-color: $secondary; + } +} + +.full-width { + margin-left: 12px; } // the default for table cells in topic list // is scale-color($primary, $lightness: 50%) // numbers get dimmer as they get colder -.coldmap-high { - color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)) !important; -} -.coldmap-med { - color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)) !important; -} -.coldmap-low { - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)) !important; +.coldmap { + &-high { + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)) !important; + } + + &-med { + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)) !important; + } + + &-low { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)) !important; + } } -#loading-message { - position: absolute; - font-size: 2.143em; - text-align: center; - top: 120px; - left: 500px; - color: $primary; -} - .top-space { +.top-space { margin-top: 10px; } -ul.breadcrumb { - margin: 0 10px 0 10px; -} .message { @include border-radius-all(8px); @@ -113,18 +221,6 @@ ul.breadcrumb { } } -#footer { - .container { - height: 50px; - .contents { - padding-top: 10px; - a[href] { - color: $secondary; - } - } - } -} - .clear-transitions { transition:none !important; } @@ -139,10 +235,6 @@ ul.breadcrumb { } } -input[type].invalid { - background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%)); -} - .d-editor-input { resize: none; } @@ -157,43 +249,6 @@ input[type].invalid { top: 60px !important; } -label { - display: block; - margin-bottom: 5px; -} -input { - &[type="radio"], &[type="checkbox"] { - margin: 3px 0; - line-height: normal; - cursor: pointer; - } - &[type="submit"], &[type="reset"], &[type="button"], &[type="radio"], &[type="checkbox"] { - width: auto; - } -} -.radio, .checkbox { - min-height: 18px; - padding-left: 18px; -} -.radio input[type="radio"], .checkbox input[type="checkbox"] { - float: left; - margin-left: -18px; -} -.controls > { - .radio:first-child, .checkbox:first-child { - padding-top: 5px; - } -} -.radio.inline, .checkbox.inline { - display: inline-block; - padding-top: 5px; - margin-bottom: 0; - vertical-align: middle; -} -.radio.inline .radio.inline, .checkbox.inline .checkbox.inline { - margin-left: 10px; -} - .flex-center-align { display: flex; align-items: center; @@ -202,7 +257,10 @@ input { .unread-private-messages { color: $secondary; background: $success; - &.badge-notification[href] {color: $secondary;} + + &.badge-notification[href] { + color: $secondary; + } } .ring-backdrop-spotlight { @@ -256,50 +314,12 @@ input { -webkit-animation-name: ping; } -@-webkit-keyframes ping { - from { - $scale: 0.25; - transform: scale($scale); - -ms-transform: scale($scale); - -webkit-transform: scale($scale); - -o-transform: scale($scale); - -moz-transform: scale($scale); - opacity: 1; - } - to { - $scale: 2; - transform: scale($scale); - -ms-transform: scale($scale); - -webkit-transform: scale($scale); - -o-transform: scale($scale); - -moz-transform: scale($scale); - opacity: 0; - } -} - .fade { opacity: 0; transition: opacity 0.15s linear; -} -.fade.in { - opacity: 1; -} - -@-webkit-keyframes rotate-forever { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@keyframes rotate-forever { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); + &.in { + opacity: 1; } } @@ -328,7 +348,6 @@ input { } .content-list { - h3 { color: $primary-medium; font-size: 1.071em; @@ -340,50 +359,106 @@ input { list-style: none; margin: 0; - li:first-of-type { - border-top: 1px solid $primary-low; - } li { border-bottom: 1px solid $primary-low; - } - li a { - display: block; - padding: 10px; - color: $primary; - - &:hover { - background-color: $primary-low; - color: $primary; + &:first-of-type { + border-top: 1px solid $primary-low; } - &.active { - font-weight: bold; + a { + display: block; + padding: 10px; color: $primary; + + &:hover { + background-color: $primary-low; + color: $primary; + } + + &.active { + font-weight: bold; + color: $primary; + } } } } } -// don't wrap relative dates, we want -// -// Jul 26, '15 -// -// not -// -// Jul -// 26, -// '15 -// -span.relative-date { - white-space:nowrap; +.form-vertical { + input, + textarea, + select, + .input-prepend, + .input-append { + display: inline-block; + margin-bottom: 0; + } + + .control-group { + @include clearfix; + } + + .control-label { + font-weight: bold; + font-size: 1.2em; + line-height: 2; + } + + .controls { + margin-left: 0; + } } -@keyframes background-fade-highlight { - 0% { - background-color: $tertiary-low; +// Special elements +// Special elements +#main { + img.avatar { + &.header { + width: 45px; + height: 45px; + } + + &.medium { + width: 32px; + height: 32px; + } + + &.small { + width: 25px; + height: 25px; + } + + &.tiny { + width: 20px; + height: 20px; + } } - 100% { - background-color: transparent; + + .user-list { + .user { + padding-bottom: 5px; + } + } +} + +#loading-message { + position: absolute; + font-size: 2.143em; + text-align: center; + top: 120px; + left: 500px; + color: $primary; +} + +#footer { + .container { + height: 50px; + .contents { + padding-top: 10px; + a[href] { + color: $secondary; + } + } } } diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 2245734e84..57c776bf7a 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -1,151 +1,163 @@ .d-header { - width: 100%; - position: absolute; - top: 0; - z-index: 1001; - background-color: $header_background; - box-shadow: 0 2px 4px -1px rgba(0,0,0, .25); + width: 100%; + position: absolute; + top: 0; + z-index: 1001; + background-color: $header_background; + box-shadow: 0 2px 4px -1px rgba(0,0,0, .25); - .docked & { - position: fixed; - backface-visibility: hidden; /** do magic for scrolling performance **/ + .docked & { + position: fixed; + backface-visibility: hidden; /** do magic for scrolling performance **/ + } + + .contents { + margin: 8px 0; + } + + .title { + float: left; + a, a:visited { + color: $header_primary; } + } - .contents { - margin: 8px 0; - } + #site-logo { + max-height: 40px; + } - .title { - float: left; - a, a:visited { - color: $header_primary; - } - } + .d-icon-home { + font-size: 1.643em; + } - #site-logo { - max-height: 40px; - } + .panel { + float: right; + position: relative; + } - .d-icon-home { - font-size: 1.643em; - } + .login-button, button.sign-up-button { + float: left; + margin-top: 7px; + padding: 6px 10px; + .fa { margin-right: 3px; } + } - .panel { - float: right; - position: relative; - } + button.login-button { + margin-left: 7px; + } - .login-button, button.sign-up-button { - float: left; - margin-top: 7px; - padding: 6px 10px; - .fa { margin-right: 3px; } - } - - button.login-button { - margin-left: 7px; - } - - .icons { - float: right; - text-align: center; - margin: 0 0 0 5px; - list-style: none; - - > li { - float: left; - } - .icon { - display: block; - padding: 3px; - color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); - text-decoration: none; - cursor: pointer; - border-top: 1px solid transparent; - border-left: 1px solid transparent; - border-right: 1px solid transparent; - transition: all linear .15s; - - - &:hover { - color: $primary; - background-color: $primary-low; - border-top: 1px solid transparent; - border-left: 1px solid transparent; - border-right: 1px solid transparent; - } - &:active { - color: $primary; - background-color: $primary-low; - } - } - .drop-down-visible & { - .active .icon { - position: relative; - color: #7b7b7b; - background-color: $secondary; - cursor: default; - border-top: 1px solid $primary-low; - border-left: 1px solid $primary-low; - border-right: 1px solid $primary-low; - - .badge-notification { - top: -10px; - } - - .flagged-posts { - right: 24px; - } - - &:after { - display: block; - position: absolute; - top: 100%; - left: 0; - z-index: 1101; - width: 100%; - height: 0; - content: ""; - border-top: 1px solid $secondary; - } - &:hover { - border-bottom: none; - } - } - } - - .d-icon { - width: 32px; - height: 32px; - font-size: 1.714em; - line-height: 32px; - display: inline-block; - } - .notifications { - position: relative; - } - .badge-notification, .ring { - position: absolute; - top: -9px; - z-index: 1; - margin-left: 0; - } - .unread-notifications { - right: 0; - background-color: scale-color($tertiary, $lightness: 50%); - } - .unread-private-messages, .ring { - right: 25px; - } - .flagged-posts { - right: 65px; - } - } - .flagged-posts, .queued-posts { - background: $danger; - } + .d-header-icons { + float: right; + } } +.header-dropdown-toggle, .drop-down { + .flagged-posts, .queued-posts { + background: $danger; + } +} + +.d-header-icons { + text-align: center; + margin: 0 0 0 5px; + list-style: none; + + + > li { + float: left; + } + .icon { + position: relative; + display: block; + padding: 3px; + color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); + text-decoration: none; + cursor: pointer; + border-top: 1px solid transparent; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + transition: all linear .15s; + + + &:hover { + color: $primary; + background-color: $primary-low; + border-top: 1px solid transparent; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + } + &:active { + color: $primary; + background-color: $primary-low; + } + } + .drop-down-visible & { + .active .icon { + position: relative; + color: #7b7b7b; + background-color: $secondary; + cursor: default; + border-top: 1px solid $primary-low; + border-left: 1px solid $primary-low; + border-right: 1px solid $primary-low; + + .flagged-posts { + right: 24px; + } + + &:after { + display: block; + position: absolute; + top: 100%; + left: 0; + z-index: 1101; + width: 100%; + height: 0; + content: ""; + border-top: 1px solid $secondary; + } + &:hover { + border-bottom: none; + } + } + } + + .d-icon { + width: 32px; + height: 32px; + font-size: 1.714em; + line-height: 32px; + display: inline-block; + } + .notifications { + position: relative; + } + .ring { + position: absolute; + top: -9px; + z-index: 1; + margin-left: 0; + } + .header-dropdown-toggle { + position: relative; + } + .badge-notification { + position: absolute; + z-index: 1; + left: 0; + top: -9px; + min-width: 6px; + } + .unread-notifications { + left: auto; + right: 0; + background-color: scale-color($tertiary, $lightness: 50%); + } + .unread-private-messages, .ring { + left: auto; + right: 25px; + } +} .highlight-strong { background-color: $highlight-medium; diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 10d2de5b67..8af8e30e60 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -129,13 +129,9 @@ .searching { position: absolute; + top: 0.25em; + right: 1.5em; - top: -3px; - right: 0.75em; - .drop-down-visible & { - top: 0.2em; - right: 1.2em; - } .spinner { width: 10px; height: 10px; diff --git a/app/assets/stylesheets/common/base/notification-options.scss b/app/assets/stylesheets/common/base/notification-options.scss deleted file mode 100644 index 1e560fadf8..0000000000 --- a/app/assets/stylesheets/common/base/notification-options.scss +++ /dev/null @@ -1,7 +0,0 @@ -.fa.muted, .fa.watching-first-post { - color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); -} -.fa.tracking, .fa.watching { - color: $tertiary; - font-weight: normal; -} diff --git a/app/assets/stylesheets/common/base/notifications-button.scss b/app/assets/stylesheets/common/base/notifications-button.scss new file mode 100644 index 0000000000..63d3c2cf13 --- /dev/null +++ b/app/assets/stylesheets/common/base/notifications-button.scss @@ -0,0 +1,9 @@ +.notifications-button.notifications-button.notifications-button { + .d-icon.regular, .d-icon.muted, .d-icon.watching-first-post { + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); + } + .d-icon.tracking, .d-icon.watching { + color: $tertiary; + font-weight: normal; + } +} diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index be2a28cc3f..65c06c6f8e 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -49,6 +49,36 @@ padding-top: 5px; } +.topic-category { + margin-top: 5px; + + .topic-header-extra { + display: inline; + vertical-align: baseline; + margin: 0; + } + + .badge-wrapper { + display: inline-block; + vertical-align: baseline; + margin-top: 0; + + &.bullet .badge-category { + vertical-align: middle; + } + + &.box, &.bullet { + vertical-align: middle; + } + + &.box + .topic-header-extra, + &.bullet + .topic-header-extra, + &.bar + .topic-header-extra { + vertical-align: middle; + } + } +} + .add-tags .select2 { margin: 0; } @@ -187,18 +217,6 @@ header .discourse-tag {color: $tag-color } margin-right: 8px; } -.notification-options.tag-notification-menu { - float: right; - margin-bottom: 10px; -} - -.tag-notification-menu .dropdown-menu { - right: 0; - top: 30px; - bottom: auto; - left: auto; -} - .tag-sort-options { margin-bottom: 20px; a { diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 3f34809d20..4f6d8d4dc2 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -247,7 +247,8 @@ aside.quote { } } - .wiki { + .wiki, + .last-wiki-edit { color: green !important; } @@ -420,3 +421,10 @@ a.mention, a.mention-group { padding-bottom: 15px; } } + +.broken-image, .large-image { + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); + border: 1px solid $primary-low; + font-size: 32px; + padding: 16px; +} diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss index c4d1a7a0e3..f4050b46a6 100644 --- a/app/assets/stylesheets/common/base/topic.scss +++ b/app/assets/stylesheets/common/base/topic.scss @@ -123,7 +123,7 @@ } .topic-unsubscribe { - .notification-options { + .notifications-button { display: inline-block; float: none; line-height: 2em; diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 5d155ddb00..cdcfc202ad 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -58,19 +58,6 @@ } } -.user-info.medium.badge-info { - min-height: 80px; - - .granted-on { - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - } - - .post-link { - display: block; - margin-top: 0.2em; - } -} - .show-badge .badge-user-info { .earned { font-size: 1.3em; diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index ad5bb38f46..134771b80c 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -1,8 +1,266 @@ // Common styles for "/user" section +.user-right { + .list-actions { + margin-bottom: 10px; + + .btn { + margin-right: 10px; + } + } +} + .user-main { .d-icon-heart { color: $love !important; } + + .about { + overflow: hidden; + width: 100%; + + .secondary { + font-size: 0.929em; + + .btn { + padding: 3px 12px; + } + + dl { + margin: 0; + } + + dd { + padding: 0; + overflow: hidden; + text-overflow: ellipsis; + color: $primary; + + &.groups { + span:after { + content: ',' + } + span:last-of-type:after { + content:'' + } + } + } + + dt { + color: $secondary-medium; + margin: 0; + display: inline-block; + } + } + + .details { + background: rgba($secondary, .85); + + blockquote { + background-color: $secondary-low; + border-left-color: $secondary-low; + } + + h1 { + font-size: 2.143em; + font-weight: normal; + i {font-size: .8em;} + } + + h2 { + font-size: 1.214em; + font-weight: normal; + margin-top: 10px; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + h3 { + font-weight: normal; + font-size: 1em; + margin: 5px 0; + + .d-icon:not(:first-of-type) { + margin-left: 10px; + } + } + + .groups { + margin-left: 10px; + display: inline; + } + + img.avatar { + float: left; + } + + .suspended { + color: $danger; + } + + .primary { + width: 100%; + position: relative; + float: left; + + h1 { + font-weight: bold; + } + + .bio { + max-height: 300px; + overflow: auto; + + a[href] { + text-decoration: underline; + } + + img { + max-width: 100%; + } + } + } + } + + .controls { + ul { + list-style-type: none; + } + + a { + padding: 5px 10px; + margin-bottom: 10px; + } + } + + &.collapsed-info { + .controls { + margin-top: 0; + } + + .profile-image { + height: 0; + } + + .details { + margin-top: 0; + background: rgba($secondary, .85); + + .bio { + display: none; + } + + .primary { + text-align: left; + margin-top: 0; + width: 100%; + + .avatar { + float: left; + margin-right: 10px; + width: 45px; + height: 45px; + } + + h1 { + font-size: 1.429em; + } + + h2 { + font-size: 1.071em; + margin-top: 4px; + } + + h3 { + display: none; + } + } + } + } + } + + .staff-counters { + text-align: left; + background: $primary; + + > div { + margin: 0 10px 0 0; + display: inline-block; + padding: 5px 0; + &:first-of-type { + padding-left: 10px; + } + span { + padding: 1px 5px; + border-radius: 10px; + } + } + + a { + color: $secondary; + } + + .active { + font-weight: bold; + } + } + + .pill { + border-radius: 15px; + display: inline-block; + height: 30px; + width: 30px; + text-align: center; + vertical-align: middle; + line-height: 30px; + } + + .helpful-flags { + background-color: green; + } + + .flagged-posts { + background-color: #E49735; + } + + .warnings-received { + background-color: #EC441B; + } + + .deleted-posts { + background-color: #EC441B; + } + + .suspensions { + background-color: #c22020; + } + + .user-field { + clear: both; + margin-bottom: 10px; + + &.text { + padding-top: 18px; + } + + .controls { + label { + width: auto; + text-align: left; + font-weight: normal; + float: auto; + } + + .instructions { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + margin-top: 5px; + margin-bottom: 10px; + font-size: 80%; + line-height: 1.4em; + } + } + } } .user-field { @@ -17,24 +275,26 @@ .public-user-fields { margin-top: 8px; margin-bottom: 8px; + .user-field-name { font-weight: bold; } -} -.collapsed-info .public-user-fields { - display: none; + .collapsed-info & { + display: none; + } } .user-navigation { - .map { height: 50px; } + .avatar { float: left; width: 45px; } + nav.buttons { width: 180px; padding: 0; @@ -44,6 +304,7 @@ box-sizing: border-box; } } + h2 { a { font-size: 1em; @@ -51,7 +312,6 @@ cursor: pointer; } } - } .user-table { @@ -78,69 +338,6 @@ margin-bottom: 15px; } -.user-info { - display: inline-block; - clear: both; - margin-bottom: 1em; - - .user-image { - float: left; - padding-right: 4px; - } - - .user-detail { - float: left; - width: 70%; - padding-left: 5px; - font-size: 13px; - - .name-line { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .username a { - font-weight: bold; - color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); - } - - .name { - margin-left: 5px; - color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); - } - - .title { - margin-top: 3px; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - } - } -} - -.user-info.small { - width: 333px; -} - -.user-info.medium { - width: 480px; - min-height: 60px; - - .user-image { - width: 55px; - } - .user-detail { - width: 380px; - } - - .username, .name { - display: block; - } - - .name { - margin-left: 0; - } -} - .user-nav { margin: 5px 0px; padding-top: 10px; @@ -150,15 +347,6 @@ } } -.user-right { - .list-actions { - margin-bottom: 10px; - .btn { - margin-right: 10px; - } - } -} - .top-section { @include clearfix(); ul { @@ -229,6 +417,11 @@ .topic-info { color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 40%)); } + + @media all and (max-width : 600px) { + float: none; + width: 100%; + } } .replies-section, @@ -252,19 +445,62 @@ } } -@media all -and (max-width : 600px) { - .top-sub-section { - float: none; - width: 100%; +.groups { + .group-link { + color: $tertiary; } } -.user-preferences .tags .select2-container-multi { - border: 1px solid $primary-low; - width: 540px; - border-radius: 0; - .select2-choices { - border: none; +.user-preferences { + textarea { + height: 100px; + } + + .static { + color: $primary; + display: inline-block; + } + + .instructions { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + margin-bottom: 10px; + font-size: 80%; + line-height: 1.4em; + + a[href] { + color: $tertiary; + } + } + + .avatar { + margin-left: 3px; + } + + .warning { + background-color: scale-color($danger, $lightness: 30%); + padding: 5px 8px; + color: $secondary; + width: 520px; + } + + .category-notifications .category-controls, + .tag-notifications .tag-controls { + margin-top: 24px; + } + + .tags .select2-container-multi { + border: 1px solid $primary-low; + width: 540px; + border-radius: 0; + .select2-choices { + border: none; + } + } +} + +.paginated-topics-list { + .user-content { + width: 100%; + margin-top: 0; } } diff --git a/app/assets/stylesheets/common/components/badges.scss b/app/assets/stylesheets/common/components/badges.scss index 1a42c171a3..6c05df9ebd 100644 --- a/app/assets/stylesheets/common/components/badges.scss +++ b/app/assets/stylesheets/common/components/badges.scss @@ -153,18 +153,19 @@ .list-controls { -.category-breadcrumb { - a.badge-category, .dropdown-header { + .category-breadcrumb { + a.badge-category, .dropdown-header { display: inline-block; padding: 5px 8px; line-height: 20px; - &.category-dropdown-button { - margin-left: -4px; - padding: 5px; - width: 13px; - .d-icon-caret-right { - margin-left: 2px; + &.category-dropdown-button { + margin-left: -4px; + padding: 5px; + width: 13px; + + .d-icon-caret-right { + margin-left: 2px; } } } @@ -190,8 +191,6 @@ margin: 0 2px; } } - - } .category-dropdown-menu { diff --git a/app/assets/stylesheets/common/components/categories-admin-dropdown.scss b/app/assets/stylesheets/common/components/categories-admin-dropdown.scss new file mode 100644 index 0000000000..de32ec0a6f --- /dev/null +++ b/app/assets/stylesheets/common/components/categories-admin-dropdown.scss @@ -0,0 +1,6 @@ +.categories-admin-dropdown.categories-admin-dropdown.categories-admin-dropdown { + .select-box-body { + min-width: auto; + width: 250px; + } +} diff --git a/app/assets/stylesheets/common/components/category-select-box.scss b/app/assets/stylesheets/common/components/category-select-box.scss index 48c414d775..58d303ac76 100644 --- a/app/assets/stylesheets/common/components/category-select-box.scss +++ b/app/assets/stylesheets/common/components/category-select-box.scss @@ -1,4 +1,4 @@ -.select-box.category-select-box { +.category-select-box.category-select-box { .select-box-row { display: -webkit-box; display: -ms-flexbox; @@ -23,6 +23,7 @@ } .category-status { + color: $primary; -webkit-box-flex: 0; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -32,7 +33,7 @@ -webkit-box-flex: 0; -ms-flex: 1 1 auto; flex: 1 1 auto; - color: $primary; + color: #919191; font-size: 0.857em; line-height: 16px; } diff --git a/app/assets/stylesheets/common/components/dropdown-select-box.scss b/app/assets/stylesheets/common/components/dropdown-select-box.scss new file mode 100644 index 0000000000..8691886546 --- /dev/null +++ b/app/assets/stylesheets/common/components/dropdown-select-box.scss @@ -0,0 +1,103 @@ +.dropdown-select-box.dropdown-select-box { + display: inline-flex; + height: 30px; + min-width: auto; + + &.is-expanded { + .collection, + .select-box-collection, + .select-box-body { + border-radius: 0; + } + } + + .select-box-body { + border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + background-clip: padding-box; + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + max-width: 300px; + width: 300px; + } + + .select-box-row { + margin: 0; + padding: 10px 5px; + + .icons { + display: flex; + align-items: flex-start; + justify-content: center; + align-self: flex-start; + margin-right: 10px; + margin-top: 2px; + width: 30px; + + .d-icon { + font-size: 1.286em; + align-self: center; + margin-right: 0; + opacity: 1; + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); + } + } + + .texts { + line-height: 18px; + flex: 1; + align-items: flex-start; + display: flex; + flex-wrap: wrap; + flex-direction: column; + + .title { + flex: 1; + font-weight: bold; + display: block; + font-size: 1em; + color: $primary; + padding: 0; + } + + .desc { + flex: 1; + font-size: 0.857em; + font-weight: normal; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 40%));; + white-space: normal; + } + } + + &.is-highlighted { + background: $tertiary-low; + } + + &:hover { + background: $highlight-medium; + } + } + + .select-box-collection { + padding: 0; + } + + .dropdown-header { + padding: 0; + border: 0; + outline: 0; + justify-content: flex-start; + background: none; + + .d-icon + .d-icon { + margin-left: 5px; + } + + .btn { + align-items: center; + justify-content: space-between; + flex-direction: row; + display: inline-flex; + height: 100%; + margin: 0; + } + } +} diff --git a/app/assets/stylesheets/common/components/notifications-button.scss b/app/assets/stylesheets/common/components/notifications-button.scss new file mode 100644 index 0000000000..df613a7a04 --- /dev/null +++ b/app/assets/stylesheets/common/components/notifications-button.scss @@ -0,0 +1,12 @@ +.notifications-button.notifications-button.notifications-button { + .select-box-body { + min-width: 550px; + max-width: 550px; + } + + .select-box-row { + .icons { + align-self: flex-start; + } + } +} diff --git a/app/assets/stylesheets/common/components/pinned-button.scss b/app/assets/stylesheets/common/components/pinned-button.scss new file mode 100644 index 0000000000..642db6e483 --- /dev/null +++ b/app/assets/stylesheets/common/components/pinned-button.scss @@ -0,0 +1,39 @@ +#topic-footer-buttons { + .pinned-button { + min-width: auto; + margin: 10px 0 5px 0; + + &.is-hidden { + display: none; + } + + .btn { + margin: 0; + } + + .reason { + line-height: 16px; + margin: 0 0 0 10px; + } + } +} + +.pinned-button { + display: flex; + justify-content: flex-start; + align-items: center; + margin: 0; + min-width: auto; + + .pinned-options { + display: inline-flex; + } + + .pinned-options.pinned-options.pinned-options { + .select-box-body { + min-width: unset; + max-width: unset; + width: 550px; + } + } +} diff --git a/app/assets/stylesheets/common/components/select-box.scss b/app/assets/stylesheets/common/components/select-box.scss index d56b7d381d..c87f5effa8 100644 --- a/app/assets/stylesheets/common/components/select-box.scss +++ b/app/assets/stylesheets/common/components/select-box.scss @@ -9,13 +9,18 @@ flex-direction: column; position: relative; height: 34px; + min-width: 220px; + + &.is-hidden { + display: none; + } &.small { height: 28px; } &.is-expanded { - z-index: 9999; + z-index: 999; .select-box-wrapper { border: 1px solid $tertiary; @@ -39,14 +44,14 @@ } .collection, { - border-radius: 0 0 3px 3px; + border-radius: inherit; } .select-box-header { border-radius: 3px 3px 0 0; } - &.is-reversed { + &.is-above { .select-box-header { border-radius: 0 0 3px 3px; } @@ -61,7 +66,7 @@ } } - &.is-reversed { + &.is-above { .select-box-body { bottom: 0; top: auto; @@ -90,6 +95,16 @@ -ms-flex-align: center; align-items: center; justify-content: space-between; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding-left: 10px; + padding-right: 10px; + height: inherit; &.is-focused { border: 1px solid $tertiary; @@ -97,10 +112,43 @@ -webkit-box-shadow: $tertiary 0px 0px 6px 0px; box-shadow: $tertiary 0px 0px 6px 0px; } + + .current-selection { + text-align: left; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + color: inherit; + } + + .icon { + margin-right: 5px; + } + + .caret-icon { + margin-left: 5px; + pointer-events: none; + } + + .d-button-label { + display: flex; + align-items: center; + white-space: nowrap; + overflow: hidden; + + .d-icon { + margin-left: 5px; + margin-right: 0; + } + } } .select-box-body { display: none; + width: 100%; background: $secondary; -webkit-box-sizing: border-box; box-sizing: border-box; @@ -119,6 +167,26 @@ -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; + + .text { + margin: 0; + } + + .d-icon { + margin-right: 5px; + } + + &.is-highlighted { + background: $tertiary-low; + } + + &.is-selected { + background: $highlight-medium; + } + + &.is-selected.is-highlighted { + background: $tertiary-low; + } } .select-box-collection { @@ -127,9 +195,9 @@ display: -webkit-box; display: -ms-flexbox; display: flex; - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; @@ -137,15 +205,64 @@ background: $secondary; overflow-x: hidden; overflow-y: auto; - border-radius: 0 0 3px 3px; + border-radius: inherit; margin: 0; padding: 0; -webkit-overflow-scrolling: touch; + + .collection { + padding: 0; + margin: 0; + + &:hover .select-box-row.is-highlighted:hover { + background: $tertiary-low; + } + } + + &::-webkit-scrollbar { + -webkit-appearance: none; + width: 10px; + } + + &::-webkit-scrollbar-thumb { + cursor: pointer; + border-radius: 5px; + background: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + + &::-webkit-scrollbar-track { + background: transparent; + border-radius: 0; + } } .select-box-filter { border-bottom: 1px solid $primary-low; background: $secondary; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 0 10px; + + .filter-query, .filter-query:focus, .filter-query:active { + background: none; + margin: 0; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + outline: none; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + width: 100%; + padding: 5px 0; + } } .select-box-wrapper { @@ -159,132 +276,18 @@ pointer-events: none; border: 1px solid transparent; } -} -.select-box .select-box-header { - height: inherit; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - padding-left: 10px; - padding-right: 10px; - - .current-selection { - text-align: left; - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - color: inherit; - } - - .icon { - margin-right: 5px; - } - - .caret-icon { - margin-left: 5px; - pointer-events: none; - } -} - -.select-box .select-box-collection { - -webkit-box-flex: 0; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - - .collection { - padding: 0; - margin: 0; - } - - &::-webkit-scrollbar { - -webkit-appearance: none; - width: 10px; - } - - &::-webkit-scrollbar-thumb { - cursor: pointer; - border-radius: 5px; - background: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - } - - &::-webkit-scrollbar-track { - background: transparent; - border-radius: 0; - } -} - -.select-box .select-box-row { - .text { - margin: 0; - } - - .d-icon { - margin-right: 5px; - } - - &.is-highlighted { - background: $highlight-medium; - } - - &.is-selected { - a { - background: $highlight-medium; - } - } -} - -.select-box .select-box-filter { - background: $secondary; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - padding: 0 10px; - - .filter-query, .filter-query:focus, .filter-query:active { - background: none; - margin: 0; - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; - outline: none; + .select-box-offscreen, .select-box .select-box-offscreen:focus { + clip: rect(0 0 0 0); + width: 1px; + height: 1px; border: 0; - -webkit-box-shadow: none; - box-shadow: none; - width: 100%; - padding: 5px 0; + margin: 0; + padding: 0; + overflow: hidden; + position: absolute; + outline: 0; + left: 0px; + top: 0px; } } - -.select-box .select-box-offscreen, .select-box .select-box-offscreen:focus { - clip: rect(0 0 0 0); - width: 1px; - height: 1px; - border: 0; - margin: 0; - padding: 0; - overflow: hidden; - position: absolute; - outline: 0; - left: 0px; - top: 0px; -} diff --git a/app/assets/stylesheets/common/components/topic-notifications-button.scss b/app/assets/stylesheets/common/components/topic-notifications-button.scss new file mode 100644 index 0000000000..cf9f081990 --- /dev/null +++ b/app/assets/stylesheets/common/components/topic-notifications-button.scss @@ -0,0 +1,30 @@ +#topic-footer-buttons { + .topic-notifications-button { + min-width: auto; + margin: 10px 0 15px 0; + + .btn { + margin: 0; + } + + .reason { + line-height: 16px; + margin: 0 0 0 10px; + } + } +} + +.topic-notifications-button .topic-notifications-options { + min-width: auto; +} + +.topic-notifications-button { + display: inline-flex; + justify-content: flex-start; + align-items: center; + margin: 0; + + .topic-notifications-options { + display: inline-flex; + } +} diff --git a/app/assets/stylesheets/common/components/user-info.scss b/app/assets/stylesheets/common/components/user-info.scss new file mode 100644 index 0000000000..8c274f7b5f --- /dev/null +++ b/app/assets/stylesheets/common/components/user-info.scss @@ -0,0 +1,68 @@ +// Common styles for "user-info" component +.user-info { + display: inline-block; + clear: both; + margin-bottom: 1em; + + .user-image { + float: left; + padding-right: 4px; + } + + .user-detail { + float: left; + width: 70%; + padding-left: 5px; + font-size: 13px; + + .name-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .username a { + font-weight: bold; + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); + } + + .name { + margin-left: 5px; + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); + } + + .title { + margin-top: 3px; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + } + + &.small { + width: 333px; + } + + &.medium { + min-height: 60px; + + .username, .name { + display: block; + } + + .name { + margin-left: 0; + } + + &.badge-info { + min-height: 80px; + + .granted-on { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + + .post-link { + display: block; + margin-top: 0.2em; + } + } + } +} diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index afba6f72cd..0aed64cad0 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -121,12 +121,6 @@ bottom: 10px; left: 10px; - .dropdown-menu { - position: absolute; - left: 0; - bottom: 30px; - } - button, .btn-group { float: none; display: inline-block; @@ -177,18 +171,6 @@ } - &.timeline-fullscreen .topic-timeline .timeline-footer-controls ul.dropdown-menu { - width: auto; - min-width: 250px; - right: auto; - .desc { - display: none; - } - .title { - padding-left: 0; - } - } - .topic-timeline { margin-left: 3em; width: 150px; @@ -212,11 +194,8 @@ margin-right: 0.5em; } - ul.dropdown-menu { - right: 0.5em; - top: auto; - bottom: 25px; - left: auto; + button:last-child { + margin-right: 0; } } diff --git a/app/assets/stylesheets/desktop/components/user-info.scss b/app/assets/stylesheets/desktop/components/user-info.scss new file mode 100644 index 0000000000..f1b7260d19 --- /dev/null +++ b/app/assets/stylesheets/desktop/components/user-info.scss @@ -0,0 +1,14 @@ +.user-info { + &.medium { + width: 480px; + + .user-image { + width: 55px; + } + + .user-detail { + width: 380px; + } + + } +} diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index aa120ec8a9..268ee3beae 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -1,96 +1,43 @@ +// Desktop // global styles that apply to the Discourse application specifically // BEWARE: changing these styles implies they take effect anywhere they are seen // throughout the Discourse application -@media all -and (max-width : 570px) { - body { - min-width: 0; - } - .wrap, - .full-width { - min-width: 0; - } +// Base Elements +body.widget-dragging { + cursor: ns-resize; } header { margin-bottom: 15px; } -body.widget-dragging { - cursor: ns-resize; +// Common classes +.boxed { + height: 100%; } -body { +.grippie { + cursor: row-resize; + padding: 4px 0px; - .boxed { - height: 100%; - &.white { - background-color: $secondary; - } - } - #main { - a.star { - color: $secondary-medium; - &:before { - font-family: "FontAwesome"; - content: "\f005"; - } - @include hover { - opacity: 0.6; - } - - &:active { - opacity: 1; - } - } - img.avatar { - &.header { - width: 45px; - height: 45px; - } - &.medium { - width: 32px; - height: 32px; - } - &.small { - width: 25px; - height: 25px; - } - &.tiny { - width: 20px; - height: 20px; - } - } - .user-list { - .user { - padding-bottom: 5px; - } - } - } - - $grippie-border-color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 50%)); - - .grippie { - cursor: row-resize; - padding: 4px 0px; - } - - .grippie:before { + &:before { content: ''; display: block; width: 27px; margin: auto; - border-top: 3px double $grippie-border-color; + border-top: 3px double dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 50%)); } } .topic-statuses { float: left; padding: 0; + .topic-status { padding: 0 2px 0 0; margin: 0; + i { font-size: 1.071em; } @@ -101,406 +48,515 @@ body { } } -body { - .form-vertical input, .form-vertical textarea, .form-vertical select, .form-vertical .input-prepend, .form-vertical .input-append { - display: inline-block; - margin-bottom: 0; - } - .form-vertical { - .control-group { - margin-bottom: 24px; - &:before { - display: table; - content: ""; - } - &:after { - display: table; - content: ""; - clear: both; - } - } - .control-label { - font-weight: bold; - font-size: 1.2em; - line-height: 2; - } - .controls { - margin-left: 0; - } +.form-vertical { + .control-group { + margin-bottom: 24px; } } + +/***********************/ /* bootstrap carryover */ +/***********************/ +code, +pre { + font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; +} +/* page not found styles */ +h1.page-not-found { + line-height: 30px; +} -body { +.page-not-found { + margin: 20px 0 40px 0; - input, textarea, select { - color: $primary; - } - - code, pre { - font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; - } - - /* page not found styles */ - h1.page-not-found { - line-height: 30px; - } - - .page-not-found { - margin: 20px 0 40px 0; - } - - .page-not-found-search { + &-search { margin-top: 20px; } - .popular-topics-title, .recent-topics-title { - margin-bottom: 10px; - } - - .not-found-topic { - > a { margin-right: 10px; line-height: 2;} - } - - .page-not-found-topics .span8 { + &-topics .span8 { line-height: 1.5em; margin-right: 20px; } +} - // this removes the unwanted top margin on a paragraph under a heading - h1+p, h2+p, h3+p, h4+p, h5+p, h6+p { +.popular-topics-title, +.recent-topics-title { + margin-bottom: 10px; +} + +.not-found-topic { + > a { + margin-right: 10px; + line-height: 2; + } +} + +// this removes the unwanted top margin on a paragraph under a heading +h1, +h2, +h3, +h4, +h5, +h6 { + + p { margin-top:0px; } +} +form { + margin: 0 0 18px; +} - form { - margin: 0 0 18px; +label, +input, +button, +select, +textarea { + font-size: 0.929em; + font-weight: normal; + line-height: 18px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +input, +select, +textarea { + color: $primary; + + &[class*="span"] { + float: none; + margin-left: 0; } - label, input, button, select, textarea { - font-size: 0.929em; - font-weight: normal; - line-height: 18px; + + &[disabled], + &[readonly] { + cursor: not-allowed; + background-color: $primary-low; + border-color: $primary-low; } - input, button, select, textarea { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + + &:focus:required:invalid { + color: $danger; + border-color: $danger; } - select, textarea { + + &:focus:required:invalid:focus { + border-color: $danger; + box-shadow: 0 0 6px $danger; + } +} + +select, +textarea { + display: inline-block; + padding: 4px; + margin-bottom: 9px; + font-size: 0.929em; + line-height: 18px; + color: $primary; +} + +input { + width: 210px; + + &[type="text"], + &[type="password"], + &[type="datetime"], + &[type="datetime-local"], + &[type="date"], + &[type="month"], + &[type="time"], + &[type="week"], + &[type="number"], + &[type="email"], + &[type="url"], + &[type="search"], + &[type="tel"], + &[type="color"] { display: inline-block; + height: 18px; padding: 4px; margin-bottom: 9px; font-size: 0.929em; line-height: 18px; color: $primary; - } - input { - &[type="text"], &[type="password"], &[type="datetime"], &[type="datetime-local"], &[type="date"], &[type="month"], &[type="time"], &[type="week"], &[type="number"], &[type="email"], &[type="url"], &[type="search"], &[type="tel"], &[type="color"] { - display: inline-block; - height: 18px; - padding: 4px; - margin-bottom: 9px; - font-size: 0.929em; - line-height: 18px; - color: $primary; - } - } - input { - width: 210px; - } - textarea { - width: 210px; - height: auto; - background-color:$secondary; - border: 1px solid $primary-low; + background-color: $secondary; + border: 1px solid $primary-low; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - } - input { - &[type="text"], &[type="password"], &[type="datetime"], &[type="datetime-local"], &[type="date"], &[type="month"], &[type="time"], &[type="week"], &[type="number"], &[type="email"], &[type="url"], &[type="search"], &[type="tel"], &[type="color"] { - background-color: $secondary; - border: 1px solid $primary-low; - border-radius: 3px; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - } - } - textarea:focus { - border-color: $tertiary; - outline: 0; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; - } - input { - &[type="text"]:focus, &[type="password"]:focus, &[type="datetime"]:focus, &[type="datetime-local"]:focus, &[type="date"]:focus, &[type="month"]:focus, &[type="time"]:focus, &[type="week"]:focus, &[type="number"]:focus, &[type="email"]:focus, &[type="url"]:focus, &[type="search"]:focus, &[type="tel"]:focus, &[type="color"]:focus { + + &:focus { border-color: $tertiary; outline: 0; box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; } } +} - select, input[type="file"] { - line-height: 28px; - } +textarea { + width: 210px; + height: auto; + background-color:$secondary; + border: 1px solid $primary-low; + border-radius: 3px; + box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - select { - width: 220px; - border: 1px solid $primary-low; - &[multiple], &[size] { - height: auto; - } + &:focus { + border-color: $tertiary; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; } +} - .input-xxlarge { - width: 530px; +select, +input[type="file"] { + line-height: 28px; +} + +select { + width: 220px; + border: 1px solid $primary-low; + + &[multiple], + &[size] { + height: auto; } - input[class*="span"], select[class*="span"], textarea[class*="span"] { - float: none; - margin-left: 0; - } - .input-append { - input[class*="span"] { - display: inline-block; - } - } - .input-prepend { - input[class*="span"] { - display: inline-block; - } - } - input, textarea { - margin-left: 0; - } - input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] { - cursor: not-allowed; - background-color: $primary-low; - border-color: $primary-low; - } - input { - &[type="radio"][disabled], &[type="checkbox"][disabled], &[type="radio"][readonly], &[type="checkbox"][readonly] { +} + +input, +textarea { + margin-left: 0; +} + +input { + &[type="radio"], + &[type="checkbox"] { + &[disabled], + &[readonly] { background-color: transparent; } } - .controls-dropdown { - margin-bottom: 10px; +} + +.input { + &-xxlarge { + width: 530px; } - .control-group { - margin-bottom: 9px; - &.warning { - > label { - color: $danger; + + &-prepend, + &-append { + margin-bottom: 5px; + + input[class*="span"] { + display: inline-block; + } + + input, + select { + position: relative; + margin-bottom: 0; + vertical-align: middle; + border-radius: 0 3px 3px 0; + + &:focus { + z-index: 2; } - .checkbox, .radio, input, select, textarea { - color: $danger; - border-color: $danger; + } + + .add-on { + display: inline-block; + width: auto; + height: 18px; + min-width: 16px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + text-align: center; + vertical-align: middle; + background-color: $secondary; + border: 1px solid $primary-low; + } + + .add-on, + .btn { + margin-left: -1px; + border-radius: 0; + } + + .active { + background-color: scale-color($danger, $lightness: 30%); + border-color: $danger; + } + } + + &-prepend { + .add-on, + .btn { + margin-right: -1px; + + &:first-child { + border-radius: 3px 0 0 3px; } - .checkbox:focus, .radio:focus, input:focus, select:focus, textarea:focus { + } + } + + &-append { + input, + select { + border-radius: 3px 0 0 3px; + } + + .add-on, + .btn { + &:last-child { + border-radius: 0 3px 3px 0; + } + } + } +} + +.input-prepend.input-append { + input, + select { + border-radius: 0; + } + + .add-on, + .btn { + &:first-child { + margin-right: -1px; + border-radius: 3px 0 0 3px; + } + } + + .add-on, + .btn { + &:last-child { + margin-left: -1px; + border-radius: 0 3px 3px 0; + } + } +} + +.controls-dropdown { + margin-bottom: 10px; +} + +.control-group { + margin-bottom: 9px; + + &.warning, + &.error { + > label { + color: $danger; + } + + .checkbox, + .radio, + input, + select, + textarea { + color: $danger; + border-color: $danger; + + &:focus { border-color: scale-color($danger, $lightness: -30%); box-shadow: 0 0 6px $danger; } - .input-prepend .add-on, .input-append .add-on { + } + + } + + &.warning { + .input-prepend, + .input-append { + .add-on { color: $danger; background-color: $danger; border-color: $danger; } } - &.error { - > label { - color: $danger; - } - .checkbox, .radio, input, select, textarea { - color: $danger; - border-color: $danger; - } - .checkbox:focus, .radio:focus, input:focus, select:focus, textarea:focus { - border-color: scale-color($danger, $lightness: -30%); - box-shadow: 0 0 6px $danger; - } - .input-prepend .add-on, .input-append .add-on { + } + + &.error { + .input-prepend, + .input-append { + .add-on { color: $danger; background-color: scale-color($danger, $lightness: 30%); border-color: scale-color($danger, $lightness: -20%); } } - &.success { - > label { - color: $success; - } - .checkbox, .radio, input, select, textarea { - color: $success; - border-color: $success; - } - .checkbox:focus, .radio:focus, input:focus, select:focus, textarea:focus { + } + + &.success { + > label { + color: $success; + } + + .checkbox, + .radio, + input, + select, + textarea { + color: $success; + border-color: $success; + + &:focus { border-color: $success; box-shadow: 0 0 6px $success; } - .input-prepend .add-on, .input-append .add-on { + } + + .input-prepend, + .input-append { + .add-on { color: $success; background-color: scale-color($success, $lightness: 90%); border-color: $success; } } } - input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid { - color: $danger; - border-color: $danger; - } - input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus { - border-color: $danger; - box-shadow: 0 0 6px $danger; - } +} - .input-prepend, .input-append { - margin-bottom: 5px; - } - .input-prepend input, .input-append input, .input-prepend select, .input-append select { - position: relative; - margin-bottom: 0; - vertical-align: middle; - border-radius: 0 3px 3px 0; - } - .input-prepend input:focus, .input-append input:focus, .input-prepend select:focus, .input-append select:focus { - z-index: 2; - } - .input-prepend .add-on, .input-append .add-on { - display: inline-block; - width: auto; - height: 18px; - min-width: 16px; - padding: 4px 5px; - font-weight: normal; - line-height: 18px; - text-align: center; - vertical-align: middle; - background-color: $secondary; - border: 1px solid $primary-low; - } - .input-prepend .add-on, .input-append .add-on, .input-prepend .btn, .input-append .btn { - margin-left: -1px; - border-radius: 0; - } - .input-prepend .active, .input-append .active { - background-color: scale-color($danger, $lightness: 30%); - border-color: $danger; - } - .input-prepend { - .add-on, .btn { - margin-right: -1px; - } - .add-on:first-child, .btn:first-child { - border-radius: 3px 0 0 3px; - } - } - .input-append { - input, select { - border-radius: 3px 0 0 3px; - } - .add-on:last-child, .btn:last-child { - border-radius: 0 3px 3px 0; - } - } - .input-prepend.input-append { - input, select { - border-radius: 0; - } - .add-on:first-child, .btn:first-child { - margin-right: -1px; - border-radius: 3px 0 0 3px; - } - .add-on:last-child, .btn:last-child { - margin-left: -1px; - border-radius: 0 3px 3px 0; - } - } - - .form-horizontal input, .form-horizontal textarea, .form-horizontal select, .form-horizontal .input-prepend, .form-horizontal .input-append { - display: inline-block; - margin-bottom: 0; - } - .form-horizontal .hide { +.form-horizontal { + .hide { display: none; } - .form-horizontal { - .control-group { - margin-bottom: 18px; - &:before { - display: table; - content: ""; - } - &:after { - display: table; - content: ""; - clear: both; - } - } - .control-indent { - margin-left: 20px; - margin-bottom: 10px; - } - .control-label { - float: left; - width: 140px; - text-align: right; - font-weight: bold; - } - .controls { - margin-left: 160px; - } + input, + textarea, + select, + .input-prepend, + .input-append { + display: inline-block; + margin-bottom: 0; } - .bootbox.modal { - .modal-footer { - a.btn-primary { - color: $secondary; - } + .control-group { + @include clearfix; + margin-bottom: 18px; + } + + .control-indent { + margin-left: 20px; + margin-bottom: 10px; + } + + .control-label { + float: left; + width: 140px; + text-align: right; + font-weight: bold; + } + + .controls { + margin-left: 160px; + } +} + +.bootbox.modal { + .modal-footer { + a.btn-primary { + color: $secondary; } } } /* bootstrap columns */ - -.row:before, -.row:after { - display: table; - content: ""; -} -.row:after { - clear: both; +.row { + @include clearfix; } -.span24 { - width: 1236px; - float: left; +.span { + &4 { + width: 196px; + margin-right: 12px; + float: left; + } + + &6 { + width: 27.027%; + float: left; + } + + &8 { + width: 404px; + float: left; + } + + &10 { + width: 508px; + float: left; + } + + &13 { + width: 59.8198%; + float: left; + } + + &15 { + /* intentionally no width set here, do not add one */ + margin-left: 12px; + float: left; + } + + &24 { + width: 1236px; + float: left; + color: amarillo; + } } -.span15 { - /* intentionally no width set here, do not add one */ - margin-left: 12px; - float: left; + +.offset { + &2 { + margin-left: 116px; + } + + &1 { + margin-left: 64px; + } } -.span13 { - width: 59.8198%; - float: left; + +// Special elements +#main { + a.star { + color: $secondary-medium; + + &:before { + font-family: "FontAwesome"; + content: "\f005"; + } + + @include hover { + opacity: 0.6; + } + + &:active { + opacity: 1; + } + } } -.span10 { - width: 508px; - float: left; -} -.span8 { - width: 404px; - float: left; -} -.span6 { - width: 27.027%; - float: left; -} -.span4 { - width: 196px; - margin-right: 12px; - float: left; -} -.offset2 { - margin-left: 116px; -} -.offset1 { - margin-left: 64px; + +// Media Queries +@media all +and (max-width : 570px) { + + body { + min-width: 0; + } + + .wrap, + .full-width { + min-width: 0; + } } diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 0754158f88..6d7094999e 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -278,7 +278,7 @@ button.dismiss-read { margin-left: 10px; } -.category-notification-menu, .tags-admin-menu { +.tags-admin-menu { .dropdown-menu { right: 0; top: 30px; diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 5a05da205b..cc7bc92f3b 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -161,22 +161,10 @@ nav.post-controls { box-shadow: none; } - &.bookmark {padding: 8px 11px; } - - .read-icon { - &:before { - font-family: "FontAwesome"; - content: "\f02e"; - } - &.unseen { - &:before { - content: "\f097"; - } - } + &.bookmark { + padding: 8px 11px; &.bookmarked { - &:before { - color: $tertiary; - } + color: $tertiary; } } } @@ -463,16 +451,6 @@ a.star { } } -.topic-notifications-container { - .btn { - @include topic-footer-button; - } - .notification-options p { - @include topic-footer-buttons-text; - display: inline-block; - } -} - #topic-footer-button { width: 757px; } diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 6a54197484..32124e6ca3 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -1,7 +1,12 @@ // Desktop styles for "/user" section -.groups { - .group-link { - color: $tertiary; +.user-right { + display: table-cell; + + &, + > .user-stream { + > .alert:first-child { + margin-top: 10px; + } } } @@ -18,81 +23,6 @@ } } -.user-preferences { - display: table-cell; - vertical-align: top; - padding-top: 10px; - padding-left: 30px; - - h3 { - color: $primary; - margin: 20px 0 10px 0; - } - - input.category-selector, input.user-selector, input.tag-chooser { - width: 530px; - } - - textarea { - width: 530px; - height: 100px; - } - - input[type=text] { - @include small-width { - width: 450px; - } - } - - .static { - color: $primary; - display: inline-block; - } - .instructions { - display: inline-block; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-top: 0; - margin-bottom: 10px; - font-size: 80%; - line-height: 1.4em; - } - .form-horizontal .instructions { - margin-left: 160px; - } - .avatar { - margin-left: 3px; - } - .instructions a[href] { - color: $tertiary; - } - .warning { - background-color: scale-color($danger, $lightness: 30%); - padding: 5px 8px; - color: $secondary; - width: 520px; - } - - .pref-mailing-list-mode .controls { - select { - width: 400px; - } - } - - .notifications, .category-notifications, .tag-notifications, .user-custom-preferences-outlet { - .controls select { - width: 280px; - } - } - - .category-notifications .category-controls, .tag-notifications .tag-controls { - margin-top: 24px; - } -} - -.user-main .user-preferences .user-field.text { - padding-top: 0; -} - .form-horizontal .control-group.category { margin-top: 18px; } @@ -101,6 +31,7 @@ width: 100%; display: table; table-layout: fixed; + .wrapper { display: table-row; } @@ -138,12 +69,6 @@ } } -.viewing-self .user-main .about.collapsed-info { - .secondary, .staff-counters { - display: inherit; - } -} - .user-content { padding: 10px 8px; background-color: $secondary; @@ -206,8 +131,6 @@ .about { background-position: center center; background-size: cover; - width: 100%; - overflow: hidden; &.group { .details { @@ -221,108 +144,33 @@ background: scale-color($secondary, $lightness: -5%); border-top: 1px solid $primary-low; border-bottom: 1px solid $primary-low; - font-size: 0.929em; - - .btn { - padding: 3px 12px; - } - - dl dd { - display: inline; - margin: 0 10px 0 0; - padding: 0; - } - - dl dt { - display: inline-block; - margin: 0 5px 0 0; - padding: 0; - } dl { - margin: 0; padding: 8px 10px; } dd { - overflow: hidden; - text-overflow: ellipsis; - color: $primary; - } - - dd.groups { - span:after { - content: ',' - } - span:last-of-type:after { - content:'' - } + display: inline; + margin: 0 10px 0 0; } dt { - color: $secondary-medium; - margin: 0; + margin: 0 5px 0 0; + padding: 0; } } .details { padding: 0 0 4px 0; - background: rgba($secondary, .85); margin-top: -200px; transition: margin .15s linear; - blockquote { - background-color: $secondary-low; - border-left-color: $secondary-low; - } - - h1 { - font-size: 2.143em; - font-weight: normal; - i {font-size: .8em;} - } - - h2 { - font-size: 1.214em; - font-weight: normal; - margin-top: 10px; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - h3 { - font-weight: normal; - font-size: 1em; - margin: 5px 0; - i:not(:first-of-type) { - margin-left: 10px; - } - } - - .groups { - margin-left: 10px; - display: inline; - } - img.avatar { margin: 0 20px 10px 0; - float: left; transition: all .1s linear; } - .suspended { - color: $danger; - } - .primary { - width: 100%; - position: relative; - float: left; - - h1 {font-weight: bold;} - .primary-textual { padding: 3px; a[href] { @@ -331,21 +179,11 @@ } .bio { - max-height: 300px; - overflow: auto; max-width: 750px; - a[href] { - text-decoration: underline; - } - a.mention { text-decoration: none; } - - img { - max-width: 100%; - } } } } @@ -361,13 +199,9 @@ float: right; text-align: right; width: 180px; - ul { - list-style-type: none; - } + a { - padding: 5px 10px; width: 140px; - margin-bottom: 10px; } .right { @@ -378,176 +212,113 @@ &.collapsed-info { .controls { - margin-top: 0; width: auto; + ul { - li {display: inline;} + + li { + display: inline; + } + a { padding: 5px 10px; margin-bottom: 10px; width: auto; - } + } } } - .staff-counters { - display: none; - } - - .secondary { - display: none; - } - - .profile-image { - height: 0; - } - .details { padding: 0 0 2px 0; - margin-top: 0; - background: rgba($secondary, .85); border-bottom: 1px solid $primary-low; - .bio { - display: none; - } - - .primary { - text-align: left; - margin-top: 0; - width: 100%; - - .avatar { - float: left; - margin-right: 10px; - width: 45px; - height: 45px; - } - - h1 { - font-size: 1.429em; - } - - h2 { - font-size: 1.071em; - margin-top: 4px; - } - - h3 { - display: none; - } - } } &.has-background { - .details { padding: 12px 15px 2px 15px;} + .details { + padding: 12px 15px 2px 15px; + } } } } .staff-counters { - text-align: left; - background: $primary; color: $secondary; margin-bottom: 20px; - a { - color: $secondary; - } - > div { - margin: 0 10px 0 0; - display: inline-block; - padding: 5px 0; - &:first-of-type { - padding-left: 10px; - } - span { - padding: 1px 5px; - border-radius: 10px; - } - } - .active { - font-weight: bold; - } - } - - .pill { - border-radius: 15px; - display: inline-block; - height: 30px; - width: 30px; - text-align: center; - vertical-align: middle; - line-height: 30px; - } - .helpful-flags { - background-color: green; - } - .flagged-posts { - background-color: #E49735; - } - .warnings-received { - background-color: #EC441B; - } - .deleted-posts { - background-color: #EC441B; - } - .suspensions { - background-color: #c22020; - } - - .user-field.text { - padding-top: 18px; } .user-field { input[type=text] { width: 530px; } + .controls { - label { - width: auto; - text-align: left; - font-weight: normal; - float: auto; - } .instructions { display: block; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-top: 5px; - margin-bottom: 10px; - font-size: 80%; - line-height: 1.4em; } } - clear: both; - margin-bottom: 10px; } - .group-notification-menu .dropdown-menu { - top: 30px; - bottom: auto; - left: auto; - right: 10px; + + .viewing-self & .about.collapsed-info { + .secondary, .staff-counters { + display: inherit; + } } } -.paginated-topics-list { - .user-content { - width: 100%; - margin-top: 0; - } -} - -.user-right { +.user-preferences { display: table-cell; + vertical-align: top; + padding-top: 10px; + padding-left: 30px; - &, - > .user-stream { - > .alert:first-child { - margin-top: 10px; + h3 { + color: $primary; + margin: 20px 0 10px 0; + } + + textarea { + width: 530px; + } + + input { + &.category-selector, + &.user-selector, + &.tag-chooser { + width: 530px; + } + + &[type=text] { + @include small-width { + width: 450px; + } } } - .group-notification-menu { - float: right; - margin-bottom: 5px; + .instructions { + display: inline-block; + margin-top: 0; + } + + .form-horizontal .instructions { + margin-left: 160px; + } + + .pref-mailing-list-mode .controls { + select { + width: 400px; + } + } + + .notifications, + .category-notifications, + .tag-notifications, + .user-custom-preferences-outlet { + .controls select { + width: 280px; + } + } + + .user-main & .user-field.text { + padding-top: 0; } } diff --git a/app/assets/stylesheets/mobile/components/user-info.scss b/app/assets/stylesheets/mobile/components/user-info.scss new file mode 100644 index 0000000000..8cf359ea18 --- /dev/null +++ b/app/assets/stylesheets/mobile/components/user-info.scss @@ -0,0 +1,14 @@ +// Mobile styles for "user-info" component +.user-info { + &.medium { + width: 300px; + + .user-image { + width: auto; + } + + .user-detail { + width: 240px; + } + } +} diff --git a/app/assets/stylesheets/mobile/discourse.scss b/app/assets/stylesheets/mobile/discourse.scss index b4337cfe67..fce24121a2 100644 --- a/app/assets/stylesheets/mobile/discourse.scss +++ b/app/assets/stylesheets/mobile/discourse.scss @@ -1,98 +1,75 @@ +// Mobile // global styles that apply to the Discourse application specifically // BEWARE: changing these styles implies they take effect anywhere they are seen // throughout the Discourse application +// Base Elements body { background-color: $secondary; - textarea { - background-color:$secondary; - } } -// This sets the space between the application content and the edge of the -// screen. This value is required in 'mobile/header.scss' to set the position -// of the drop-down menu. -$mobile-wrapper-padding: 10px; -.wrap { - padding: 0 $mobile-wrapper-padding; -} - -body { - - .boxed { - .contents { - padding: 10px 0 0 0; - } - &.white { - background-color: $secondary; - } - } - #main { - position: relative; - img.avatar { - &.header { - width: 45px; - height: 45px; - } - &.medium { - width: 32px; - height: 32px; - } - &.small { - width: 25px; - height: 25px; - } - &.tiny { - width: 20px; - height: 20px; - } - } - .user-list { - .user { - padding-bottom: 5px; - } - } - } - .control-group { - margin-bottom: 9px; - } +textarea { + background-color:$secondary; } blockquote { /* 13px left is intentional here to properly align with post quotes */ padding: 10px 8px 10px 13px; + p { margin: 0 0 10px 0; + + &:last-of-type { + margin-bottom:0; + } } - p:last-of-type { - margin-bottom:0; + +} + +h1.page-not-found { + line-height: 24px; + margin: 5px 0 -5px 0; +} + +h2 { + &.popular-topics-title { + margin-bottom: 6px; + font-size: 1.2em; } + + &.recent-topics-title { + margin-bottom: 6px; + font-size: 1.2em; + } +} + +// Common classes +.wrap { + padding: 0 10px; +} + +.boxed { + .contents { + padding: 10px 0 0 0; + } +} + +.control-group { + margin-bottom: 9px; } .topic-statuses { display: inline-block; + .topic-status { i { color: $secondary-medium; } } -} -.topic-statuses:empty { - display: none; -} -// Styles used before the user is logged into discourse. For example, activating their -// account or changing their email. - -#simple-container { - width: 90%; -} - -// somehow the image logo assumption inherits margins from earlier in the CSS stack -// we must remove margins for text site titles -h2#site-text-logo -{ - margin: 0 0 0 10px; + &:empty { + display: none; + } } // categories should not be bold on mobile; they fight with the topic title too much @@ -101,22 +78,6 @@ h2#site-text-logo } .mobile-view .mobile-nav { - &.messages-nav, &.notifications-nav, &.activity-nav, &.preferences-nav { - position: absolute; - right: 0px; - top: -55px; - } -} - - -.mobile-view .mobile-nav { - a .fa { - margin-right: 8px; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - } - a { - color: $primary; - } margin: 0; padding: 0; background: $primary-low; @@ -125,6 +86,24 @@ h2#site-text-logo position: relative; width: 45%; + &.messages-nav, + &.notifications-nav, + &.activity-nav, + &.preferences-nav { + position: absolute; + right: 0px; + top: -55px; + } + + a { + color: $primary; + + .fa { + margin-right: 8px; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + } + > li > a { padding: 8px 10px; height: 100%; @@ -132,6 +111,7 @@ h2#site-text-logo box-sizing: border-box; display: block; } + .d-icon-caret-down { position: absolute; right: 0px; @@ -139,81 +119,63 @@ h2#site-text-logo .drop { display: none; - } - .drop.expanded { - left: 0; - display: block; - position: absolute; - z-index: 10000000; - background-color: $secondary; - width: 100%; - list-style: none; - margin: 0; - padding: 5px; - border: 1px solid #eaeaea; - box-sizing: border-box; - li { - margin: 5px 0; - padding: 0; - a { - height: 100%; - display: block; - padding: 5px 8px; + &.expanded { + left: 0; + display: block; + position: absolute; + z-index: 10000000; + background-color: $secondary; + width: 100%; + list-style: none; + margin: 0; + padding: 5px; + border: 1px solid #eaeaea; + box-sizing: border-box; + + li { + margin: 5px 0; + padding: 0; + + a { + height: 100%; + display: block; + padding: 5px 8px; + } } } } } /* page not found styles */ -h1.page-not-found { - line-height: 24px; - margin: 5px 0 -5px 0; +.page-not-found { + &-topics a.badge-wrapper { + margin-left: 8px; + } + + &-search h2 { + font-size: 1.2em; + } } -.page-not-found-topics a.badge-wrapper { - margin-left: 8px; -} - -h2.popular-topics-title { - margin-bottom: 6px; - font-size: 1.2em; -} - -h2.recent-topics-title { - margin-bottom: 6px; - font-size: 1.2em; -} - -.page-not-found-search h2 { - font-size: 1.2em; -} - - - .form-vertical { - input, textarea, select, .input-prepend, .input-append { - display: inline-block; - margin-bottom: 0; - } - .control-group { margin-bottom: 12px; - &:before { - display: table; - content: ""; - } - &:after { - display: table; - content: ""; - clear: both; - } - } - .control-label { - font-weight: bold; - font-size: 1.2em; - line-height: 2; - } - .controls { - margin-left: 0; } } + +// Special elements +#main { + position: relative; +} + +// Styles used before the user is logged into discourse. For example, activating +// their account or changing their email. +#simple-container { + width: 90%; +} + +// somehow the image logo assumption inherits margins from earlier in the CSS +// stack we must remove margins for text site titles +h2#site-text-logo { + margin: 0 0 0 10px; +} diff --git a/app/assets/stylesheets/mobile/header.scss b/app/assets/stylesheets/mobile/header.scss index ab112d8bda..528f26b721 100644 --- a/app/assets/stylesheets/mobile/header.scss +++ b/app/assets/stylesheets/mobile/header.scss @@ -28,22 +28,22 @@ } } - .icons { - .badge-notification { - top: -5px; - color: $secondary; - } - - .active .icon { - &:after { margin-top: -1px; } - } - } - button.sign-up-button { display:none; } } +.d-header-icons { + .badge-notification { + top: -5px; + color: $secondary; + } + + .active .icon { + &:after { margin-top: -1px; } + } +} + #main-outlet { padding-top: 60px; } diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 7e8c56da2d..cfe5b09e04 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -52,6 +52,12 @@ margin-left: 8px; } } + + .select-box { + &.categories-admin-dropdown, &.category-notifications-button, &.tag-notifications-button { + margin-top: 5px; + } + } } .list-container .full-width { @@ -313,7 +319,6 @@ tr.category-topic-link { // Misc. stuff // -------------------------------------------------- - .list-controls { .category-dropdown-button { padding: 4px 10px 4px 8px; @@ -407,7 +412,7 @@ ol.category-breadcrumb { .btn-default.pull-right { margin-right: 10px; } } -.category-notification-menu, .tags-admin-menu { +.tags-admin-menu { display: none; } diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 4068eb00ae..4e35da9a5c 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -76,21 +76,8 @@ nav.post-controls { } &.has-like {color: $love;} - .read-icon { - &:before { - font-family: "FontAwesome"; - content: "\f02e"; - } - &.unseen { - &:before { - content: "\f097"; - } - } - &.bookmarked { - &:before { - color: $tertiary; - } - } + &.bookmarked { + color: $tertiary; } } diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index d69b789a37..e6a84e5721 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -8,140 +8,6 @@ } } -.user-preferences { - .control-group { - padding: 8px 36px 8px 8px; - } - textarea { - width: 530px; - height: 100px; - } - .static { - color: $primary; - margin-top: 5px; - margin-left: 5px; - display: inline-block; - } - .instructions { - color: $primary; - margin-top: 5px; - } - .category-controls { - padding-top: 8px; - } - .avatar { - margin-left: 3px; - } - .instructions a[href] { - color: $primary; - } - .warning { - @include border-radius-all(6px); - background-color: $danger; - padding: 5px 8px; - color: $primary; - width: 520px; - } - - .controls-dropdown { - margin-top: 10px; - margin-bottom: 15px; - padding-left: 5px; - select { - width: 280px; - } - } - - .save-button { - width: 100%; - overflow: auto; - max-width: 200px; - button { - display: block; - } - } - - .delete-account { - overflow: hidden; - } - - .checkbox-label { - overflow: auto; - display: block; - width: 100%; - padding: 5px 8px; - } - - textarea {width: 100%;} - - .desktop-notifications button { - float: none; - } - .apps .controls button { - float: right; - } - .category-notifications .category-controls, .tag-notifications .tag-controls { - margin-top: 24px; - } -} - -.profile-image { - height: 25px; - width: 100%; -} - - -.groups { - .group-link { - color: $tertiary; - } -} - -.hasBackground .details {margin-top: 200px; -} - -.user-preferences { - input.category-selector { - } - - textarea { - height: 100px; - } - - input[type=text] { - @include small-width { - } - } - - .static { - color: $primary; - display: inline-block; - } - .instructions { - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-top: 5px; - margin-bottom: 10px; - font-size: 80%; - line-height: 1.4em; - } - .avatar { - margin-left: 3px; - } - .instructions a[href] { - color: $tertiary; - } - .warning { - background-color: scale-color($danger, $lightness: 30%); - padding: 5px 8px; - color: $secondary; - } - -} - -.form-horizontal .control-group.category { - margin-top: 18px; -} - .user-main { table.group-members { @@ -205,98 +71,31 @@ .about { background: dark-light-diff($primary, $secondary, 0%, -75%) center; - width: 100%; margin-bottom: 10px; - overflow: hidden; color: $secondary; .secondary { background: $primary-low; - font-size: 0.929em; - - .btn { padding: 3px 12px; } - - dl dd { - margin: 0 15px 0 5px; - padding: 0; - } - - dl dt { - display: inline-block; - } dl { padding: 10px 15px; - margin: 0; } dd { - overflow: hidden; - text-overflow: ellipsis; - color: $primary; - } - - dt { - color: $secondary-medium; - margin: 0; + margin: 0 15px 0 5px; } } .details { - padding: 15px 10px 4px 10px; - background: rgba($secondary, .85); - - blockquote { - background-color: $secondary-low; - border-left-color: $secondary-low; - } + padding: 15px 0 4px 0; h1 { - font-size: 2.143em; - font-weight: normal; margin: 10px 0 0 0; } - h2 { - font-size: 1.214em; - font-weight: normal; - margin-top: 10px; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - h3 { - font-weight: normal; - font-size: 1em; - margin: 5px 0; - i:not(:first-of-type) { - margin-left: 10px; - } - } - - .groups { - margin-left: 10px; - display: inline; - } - - img.avatar { - float: left; - } - - .suspended { - color: $danger; - } - .primary { - width: 100%; - position: relative; - float: left; color: $primary; - h1 {font-weight: bold;} - .primary-textual { float: left; padding-left: 15px; @@ -307,17 +106,7 @@ .bio { color: $primary; - max-height: 300px; - overflow: auto; max-width: 700px; - - a[href] { - text-decoration: underline; - } - - img { - max-width: 100%; - } } } } @@ -325,119 +114,26 @@ .controls { float: left; padding-left: 15px; + ul { - list-style-type: none; margin: 0; } + a { - padding: 5px 10px; width: 120px; - margin-bottom: 10px; } } - } - .about.collapsed-info { - .controls { - margin-top: 0; - - } - - .staff-counters { - display: none; - } - .secondary { display: none; } - - .profile-image { - height: 0; - } - - .details { - padding: 12px 15px 2px 15px; - margin-top: 0; - background: rgba($secondary, .85); - .bio { display: none; } - - .primary { - text-align: left; - margin-top: 0; - width: 100%; - - .avatar { - float: left; - margin-right: 10px; - width: 45px; - height: 45px; - } - - h1 { - font-size: 1.429em; - } - - h2 { - font-size: 1.071em; - margin-top: 4px; - } - - h3 { - display: none; - } + &.collapsed-info { + .details { + padding: 12px 15px 2px 15px; } } } .staff-counters { - text-align: left; - background: $primary; padding: 7px 0; display: inline; - > div { - margin: 0 10px 0 0; - display: inline-block; - padding: 5px 0; - &:first-of-type { - padding-left: 10px; - } - span { - padding: 1px 5px; - border-radius: 10px; - } - - } - a { - color: $secondary; - } - .active { - font-weight: bold; - } - } - .pill { - border-radius: 15px; - display: inline-block; - height: 30px; - width: 30px; - text-align: center; - vertical-align: middle; - line-height: 30px; - } - .helpful-flags { - background-color: green; - } - .flagged-posts { - background-color: #E49735; - } - .warnings-received { - background-color: #EC441B; - } - .deleted-posts { - background-color: #EC441B; - } - .suspensions { - background-color: #c22020; - } - - .user-field.text { - padding-top: 18px; } .user-field { @@ -446,61 +142,96 @@ text-align: left; font-weight: bold; } - .controls { - label { - width: auto; - text-align: left; - font-weight: normal; - float: auto; - } - .instructions { - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-top: 5px; - margin-bottom: 10px; - font-size: 80%; - line-height: 1.4em; - } - } - clear: both; - margin-bottom: 10px; } } +.profile-image { + height: 25px; + width: 100%; +} + +.has-background .details { + margin-top: 200px; +} + +.form-horizontal .control-group.category { + margin-top: 18px; +} + .paginated-topics-list { margin-top: 20px; - - .user-content { - width: 100%; - margin-top: 0; - } } // mobile fixups for badges - .badge-card.medium { width: 300px; } -.show-badge-details { - margin-bottom: 1em; -} - .user-badges { margin-bottom: 2em; } -.show-badge-details .badge-grant-info { - display: none; +.show-badge-details { + margin-bottom: 1em; + + .badge-grant-info { + display: none; + } } -.user-info.medium { - width: 300px; -} +.user-preferences { + .control-group { + padding: 8px 36px 8px 8px; + } -.user-info.medium .user-detail { - width: 240px; -} + .static { + margin-top: 5px; + margin-left: 5px; + } -.user-info.medium .user-image { - width: auto; + .instructions { + margin-top: 5px; + } + + .category-controls { + padding-top: 8px; + } + + .controls-dropdown { + margin-top: 10px; + margin-bottom: 15px; + padding-left: 5px; + + select { + width: 280px; + } + } + + .save-button { + width: 100%; + overflow: auto; + max-width: 200px; + + button { + display: block; + } + } + + .delete-account { + overflow: hidden; + } + + .checkbox-label { + overflow: auto; + display: block; + width: 100%; + padding: 5px 8px; + } + + .desktop-notifications button { + float: none; + } + .apps .controls button { + float: right; + } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 22c0631362..c1b66293e3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -36,6 +36,7 @@ class ApplicationController < ActionController::Base end end + before_action :check_readonly_mode before_filter :handle_theme before_filter :set_current_user_for_logs before_filter :clear_notifications @@ -61,7 +62,7 @@ class ApplicationController < ActionController::Base end def add_readonly_header - response.headers['Discourse-Readonly'] = 'true' if Discourse.readonly_mode? + response.headers['Discourse-Readonly'] = 'true' if @readonly_mode end def perform_refresh_session @@ -182,7 +183,7 @@ class ApplicationController < ActionController::Base end def clear_notifications - if current_user && !Discourse.readonly_mode? + if current_user && !@readonly_mode cookie_notifications = cookies['cn'.freeze] notifications = request.headers['Discourse-Clear-Notifications'.freeze] @@ -400,6 +401,10 @@ class ApplicationController < ActionController::Base private + def check_readonly_mode + @readonly_mode = Discourse.readonly_mode? + end + def locale_from_header begin # Rails I18n uses underscores between the locale and the region; the request @@ -574,7 +579,7 @@ class ApplicationController < ActionController::Base def block_if_readonly_mode return if request.fullpath.start_with?(path "/admin/backups") - raise Discourse::ReadOnly.new if !(request.get? || request.head?) && Discourse.readonly_mode? + raise Discourse::ReadOnly.new if !(request.get? || request.head?) && @readonly_mode end def build_not_found_page(status = 404, layout = false) diff --git a/app/controllers/post_actions_controller.rb b/app/controllers/post_actions_controller.rb index 3cef663c56..7bcad95126 100644 --- a/app/controllers/post_actions_controller.rb +++ b/app/controllers/post_actions_controller.rb @@ -13,8 +13,10 @@ class PostActionsController < ApplicationController guardian.ensure_post_can_act!( @post, PostActionType.types[@post_action_type_id], - is_warning: params[:is_warning], - taken_actions: taken + opts: { + is_warning: params[:is_warning], + taken_actions: taken + } ) args = {} diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 996dab4d26..15bcbb2336 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -588,9 +588,10 @@ class TopicsController < ApplicationController current_user, params[:topic_id].to_i, params[:topic_time].to_i, - (params[:timings] || []).map { |post_number, t| [post_number.to_i, t.to_i] }, + (params[:timings] || {}).map { |post_number, t| [post_number.to_i, t.to_i] }, mobile: view_context.mobile_view? ) + render nothing: true end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 4908b527bb..e9f23ca717 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -115,7 +115,7 @@ class Users::OmniauthCallbacksController < ApplicationController if @auth_result.email_valid && @auth_result.email == user.email user.update!(staged: false) # ensure there is an active email token - user.email_tokens.create(email: user.email) unless user.email_tokens.active.exists? + user.email_tokens.create(email: user.email) unless EmailToken.where(email: user.email, confirmed: true).present? || user.email_tokens.active.where(email: user.email).exists? user.activate end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index fcd6b9ef28..8c35efe7ab 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -372,14 +372,21 @@ class UsersController < ApplicationController user_id: user.id } else + errors = user.errors.to_hash + errors[:email] = errors.delete(:primary_email) if errors[:primary_email] + render json: { success: false, message: I18n.t( 'login.errors', errors: user.errors.full_messages.join("\n") ), - errors: user.errors.to_hash, - values: user.attributes.slice('name', 'username', 'email'), + errors: errors, + values: { + name: user.name, + username: user.username, + email: user.primary_email&.email + }, is_developer: UsernameCheckerService.is_developer?(user.email) } end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 64316cc474..6e23d88e78 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -250,7 +250,7 @@ module ApplicationHelper end def crawler_layout? - controller.try(:use_crawler_layout?) + controller&.use_crawler_layout? end def include_crawler_content? diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index f570dfcbf1..ad2ac01b25 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -10,7 +10,7 @@ module Jobs HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new( user_archive: ['topic_title', 'category', 'sub_category', 'is_pm', 'post', 'like_count', 'reply_count', 'url', 'created_at'], - user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'blocked', 'active', 'admin', 'moderator', 'ip_address'], + user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'blocked', 'active', 'admin', 'moderator', 'ip_address', 'staged'], user_stats: ['topics_entered', 'posts_read_count', 'time_read', 'topic_count', 'post_count', 'likes_given', 'likes_received'], user_profile: ['location', 'website', 'views'], user_sso: ['external_id', 'external_email', 'external_username', 'external_name', 'external_avatar_url'], @@ -181,7 +181,7 @@ module Jobs def get_base_user_array(user) user_array = [] - user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.blocked, user.active, user.admin, user.moderator, user.ip_address, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views) + user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.blocked, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views) end def add_single_sign_on(user, user_info_array) diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index c38d9a55e2..1b97ef06ff 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -25,6 +25,7 @@ module Jobs raw = post.raw.dup start_raw = raw.dup downloaded_urls = {} + broken_images, large_images = [], [] extract_images_from(post.cooked).each do |image| src = original_src = image['src'] @@ -56,9 +57,11 @@ module Jobs end else log(:info, "Failed to pull hotlinked image for post: #{post_id}: #{src} - Image is bigger than #{@max_size}") + large_images << original_src end else log(:info, "There was an error while downloading '#{src}' locally for post: #{post_id}") + broken_images << original_src end end # have we successfully downloaded that file? @@ -98,6 +101,40 @@ module Jobs post.revise(Discourse.system_user, changes, options) elsif downloaded_urls.present? post.trigger_post_process(true) + elsif broken_images.present? || large_images.present? + start_html = post.cooked + doc = Nokogiri::HTML::fragment(start_html) + images = doc.css("img[src]") - doc.css("img.avatar") + images.each do |tag| + src = tag['src'] + if broken_images.include?(src) + tag.name = 'span' + tag.set_attribute('class', 'broken-image fa fa-chain-broken') + tag.set_attribute('title', I18n.t('post.image_placeholder.broken')) + tag.remove_attribute('src') + tag.remove_attribute('width') + tag.remove_attribute('height') + elsif large_images.include?(src) + tag.name = 'a' + tag.set_attribute('href', src) + tag.set_attribute('target', '_blank') + tag.set_attribute('title', I18n.t('post.image_placeholder.large')) + tag.remove_attribute('src') + tag.remove_attribute('width') + tag.remove_attribute('height') + tag.inner_html = '' + parent = tag.parent + if parent.name == 'a' + parent.add_next_sibling(tag) + parent.add_next_sibling('
') + parent.content = parent["href"] + end + end + end + if start_html == post.cooked && doc.to_html != post.cooked + post.update_column(:cooked, doc.to_html) + post.publish_change_to_clients! :revised + end end end diff --git a/app/jobs/regular/send_system_message.rb b/app/jobs/regular/send_system_message.rb index d0dc41ec6f..3eb9f002d7 100644 --- a/app/jobs/regular/send_system_message.rb +++ b/app/jobs/regular/send_system_message.rb @@ -13,7 +13,7 @@ module Jobs return if user.blank? system_message = SystemMessage.new(user) - system_message.create(args[:message_type]) + system_message.create(args[:message_type], args[:message_options]&.symbolize_keys || {}) end end diff --git a/app/jobs/scheduled/pending_queued_posts_reminder.rb b/app/jobs/scheduled/pending_queued_posts_reminder.rb index 37b9afe6f4..867918dcef 100644 --- a/app/jobs/scheduled/pending_queued_posts_reminder.rb +++ b/app/jobs/scheduled/pending_queued_posts_reminder.rb @@ -4,13 +4,20 @@ module Jobs every 1.hour def execute(args) - return true unless SiteSetting.notify_about_queued_posts_after > 0 && SiteSetting.contact_email + return true unless SiteSetting.notify_about_queued_posts_after > 0 queued_post_ids = should_notify_ids if queued_post_ids.size > 0 && last_notified_id.to_i < queued_post_ids.max - message = PendingQueuedPostsMailer.notify(count: queued_post_ids.size) - Email::Sender.new(message, :pending_queued_posts_reminder).send + PostCreator.create( + Discourse.system_user, + target_group_names: Group[:moderators].name, + archetype: Archetype.private_message, + subtype: TopicSubtype.system_message, + title: I18n.t('system_messages.queued_posts_reminder.subject_template', count: queued_post_ids.size), + raw: I18n.t('system_messages.queued_posts_reminder.text_body_template', base_url: Discourse.base_url) + ) + self.last_notified_id = queued_post_ids.max end diff --git a/app/mailers/pending_flags_mailer.rb b/app/mailers/pending_flags_mailer.rb deleted file mode 100644 index 39fbbb0fd1..0000000000 --- a/app/mailers/pending_flags_mailer.rb +++ /dev/null @@ -1,39 +0,0 @@ -require_dependency 'email/message_builder' -require_dependency 'flag_query' - -class PendingFlagsMailer < ActionMailer::Base - include Email::BuildEmailHelper - - def notify - return unless SiteSetting.contact_email - - @posts, @topics, @users = FlagQuery.flagged_posts_report(Discourse.system_user, 'active', 0, 20) - - @posts.each do |post| # Note: post is a Hash, not a Post. - topic = @topics.select { |t| t[:id] == post[:topic_id] }.first - - post[:title] = topic[:title] - post[:url] = "#{Discourse.base_url}#{Post.url(topic[:slug], topic[:id], post[:post_number])}" - post[:user] = @users.select { |u| u[:id] == post[:user_id] }.first - - counts = flag_reason_counts(post) - post[:reason_counts] = counts.map { |reason, count| "#{I18n.t('post_action_types.' + reason.to_s + '.title')}: #{count}" }.join(', ') - post[:html_reason_counts] = counts.map { |reason, count| "#{I18n.t('post_action_types.' + reason.to_s + '.title')}: #{count}" }.join(', ') - end - - @hours = SiteSetting.notify_about_flags_after - - subject = "[#{SiteSetting.title}] " + I18n.t('flags_reminder.subject_template', count: PostAction.flagged_posts_count) - build_email(SiteSetting.contact_email, subject: subject) - end - - private - - def flag_reason_counts(post) - post[:post_actions].inject({}) do |h, v| - h[v[:name_key]] ||= 0 - h[v[:name_key]] += 1 - h - end - end -end diff --git a/app/mailers/pending_queued_posts_mailer.rb b/app/mailers/pending_queued_posts_mailer.rb deleted file mode 100644 index 525454b7d2..0000000000 --- a/app/mailers/pending_queued_posts_mailer.rb +++ /dev/null @@ -1,10 +0,0 @@ -require_dependency 'email/message_builder' - -class PendingQueuedPostsMailer < ActionMailer::Base - include Email::BuildEmailHelper - - def notify(opts = {}) - return unless SiteSetting.contact_email - build_email(SiteSetting.contact_email, template: 'queued_posts_reminder', count: opts[:count]) - end -end diff --git a/app/models/badge.rb b/app/models/badge.rb index 46502613f5..637cb24010 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -56,6 +56,10 @@ class Badge < ActiveRecord::Base GivesBack = 32 Empathetic = 39 + Enthusiast = 45 + Aficionado = 46 + Devotee = 47 + NewUserOfTheMonth = 44 # other consts diff --git a/app/models/post_action.rb b/app/models/post_action.rb index 00a196e37a..8c2006c1df 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -572,7 +572,8 @@ SQL edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts, flag_reason: I18n.t("flag_reasons.#{post_action_type}"), } - SystemMessage.create(post.user, :post_hidden, options) + + Jobs.enqueue_in(5.seconds, :send_system_message, user_id: post.user.id, message_type: :post_hidden, message_options: options) end end diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index e0105cce4e..84f018bfa8 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -60,7 +60,7 @@ class PostAnalyzer raw_mentions.compact! raw_mentions.uniq! - @raw_mention = raw_mentions + @raw_mentions = raw_mentions end # from rack ... compat with ruby 2.2 diff --git a/app/models/theme.rb b/app/models/theme.rb index f38668e1fa..b9bf76c53d 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -28,13 +28,11 @@ class Theme < ActiveRecord::Base changed_fields.each(&:save!) changed_fields.clear - Theme.expire_site_cache! if user_selectable_changed? + Theme.expire_site_cache! if user_selectable_changed? || name_changed? @dependant_themes = nil @included_themes = nil - end - after_save do remove_from_cache! notify_scheme_change if color_scheme_id_changed? end @@ -46,7 +44,6 @@ class Theme < ActiveRecord::Base end if self.id - ColorScheme .where(theme_id: self.id) .where("id NOT IN (SELECT color_scheme_id FROM themes where color_scheme_id IS NOT NULL)") @@ -56,6 +53,8 @@ class Theme < ActiveRecord::Base .where(theme_id: self.id) .update_all(theme_id: nil) end + + Theme.expire_site_cache! end after_commit ->(theme) do diff --git a/app/models/topic.rb b/app/models/topic.rb index 39dca92df6..52aa1255e5 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -247,7 +247,7 @@ class Topic < ActiveRecord::Base def self.visible_post_types(viewed_by = nil) types = Post.types result = [types[:regular], types[:moderator_action], types[:small_action]] - result << types[:whisper] if viewed_by.try(:staff?) + result << types[:whisper] if viewed_by&.staff? result end @@ -1224,12 +1224,21 @@ SQL end def pm_with_non_human_user? - Topic.private_messages - .joins("LEFT JOIN topic_allowed_groups ON topics.id = topic_allowed_groups.topic_id") - .where("topic_allowed_groups.topic_id IS NULL") - .where("topics.id = ?", self.id) - .where("(SELECT COUNT(*) FROM topic_allowed_users WHERE topic_allowed_users.topic_id = ? AND topic_allowed_users.user_id > 0) = 1", self.id) - .exists? + sql = <<~SQL + SELECT 1 FROM topics + LEFT JOIN topic_allowed_groups ON topics.id = topic_allowed_groups.topic_id + WHERE topic_allowed_groups.topic_id IS NULL + AND topics.archetype = :private_message + AND topics.id = :topic_id + AND ( + SELECT COUNT(*) FROM topic_allowed_users + WHERE topic_allowed_users.topic_id = :topic_id + AND topic_allowed_users.user_id > 0 + ) = 1 + SQL + + result = Topic.exec_sql(sql, private_message: Archetype.private_message, topic_id: self.id) + result.ntuples != 0 end private diff --git a/app/models/user.rb b/app/models/user.rb index e86f5966a3..a84a02b0c6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -10,6 +10,7 @@ require_dependency 'pretty_text' require_dependency 'url_helper' require_dependency 'letter_avatar' require_dependency 'promotion' +require_dependency 'password_validator' class User < ActiveRecord::Base include Searchable @@ -82,7 +83,7 @@ class User < ActiveRecord::Base validates :name, user_full_name: true, if: :name_changed?, length: { maximum: 255 } validates :ip_address, allowed_ip_address: { on: :create, message: :signup_not_allowed } validates :primary_email, presence: true - validates_associated :primary_email + validates_associated :primary_email, message: -> (_, user_email) { user_email[:value]&.errors[:email]&.first } after_initialize :add_trust_level @@ -139,8 +140,6 @@ class User < ActiveRecord::Base ucf.value::int > 0 )', 'master_id') } - scope :staff, -> { where("admin OR moderator") } - # TODO-PERF: There is no indexes on any of these # and NotifyMailingListSubscribers does a select-all-and-loop # may want to create an index on (active, blocked, suspended_till)? @@ -691,17 +690,15 @@ class User < ActiveRecord::Base end def activate - if email_token = self.email_tokens.active.first + if email_token = self.email_tokens.active.where(email: self.email).first EmailToken.confirm(email_token.token) else - self.active = true - save + self.update!(active: true) end end def deactivate - self.active = false - save + self.update!(active: false) end def change_trust_level!(level, opts = nil) diff --git a/app/serializers/basic_user_serializer.rb b/app/serializers/basic_user_serializer.rb index 8880c8dbd7..d14877e6da 100644 --- a/app/serializers/basic_user_serializer.rb +++ b/app/serializers/basic_user_serializer.rb @@ -9,7 +9,7 @@ class BasicUserSerializer < ApplicationSerializer if Hash === object User.avatar_template(user[:username], user[:uploaded_avatar_id]) else - user.try(:avatar_template) + user&.avatar_template end end diff --git a/app/serializers/group_post_serializer.rb b/app/serializers/group_post_serializer.rb index 99a06f5534..f21802cfbd 100644 --- a/app/serializers/group_post_serializer.rb +++ b/app/serializers/group_post_serializer.rb @@ -7,7 +7,7 @@ class GroupPostSerializer < ApplicationSerializer :created_at, :title, :url, - :category, + :category_id, :post_number, :topic_id @@ -22,7 +22,7 @@ class GroupPostSerializer < ApplicationSerializer SiteSetting.enable_names? end - def category - object.topic.category + def category_id + object.topic.category_id end end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index d2e7fc7e86..e06617b0f1 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -68,7 +68,8 @@ class PostSerializer < BasicPostSerializer :via_email, :is_auto_generated, :action_code, - :action_code_who + :action_code_who, + :last_wiki_edit def initialize(object, opts) super(object, opts) @@ -108,15 +109,15 @@ class PostSerializer < BasicPostSerializer end def moderator? - !!(object.try(:user).try(:moderator?)) + !!(object&.user&.moderator?) end def admin? - !!(object.try(:user).try(:admin?)) + !!(object&.user&.admin?) end def staff? - !!(object.try(:user).try(:staff?)) + !!(object&.user&.staff?) end def yours @@ -140,7 +141,7 @@ class PostSerializer < BasicPostSerializer end def display_username - object.user.try(:name) + object.user&.name end def primary_group_name @@ -154,15 +155,15 @@ class PostSerializer < BasicPostSerializer end def primary_group_flair_url - object.user.try(:primary_group).try(:flair_url) + object.user&.primary_group&.flair_url end def primary_group_flair_bg_color - object.user.try(:primary_group).try(:flair_bg_color) + object.user&.primary_group&.flair_bg_color end def primary_group_flair_color - object.user.try(:primary_group).try(:flair_color) + object.user&.primary_group&.flair_color end def link_counts @@ -189,11 +190,11 @@ class PostSerializer < BasicPostSerializer end def user_title - object.try(:user).try(:title) + object&.user&.title end def trust_level - object.try(:user).try(:trust_level) + object&.user&.trust_level end def reply_to_user @@ -225,14 +226,18 @@ class PostSerializer < BasicPostSerializer # Summary of the actions taken on this post def actions_summary result = [] - PostActionType.types.each do |sym, id| - next if [:bookmark].include?(sym) + can_see_post = scope.can_see_post?(object) + + PostActionType.types.except(:bookmark).each do |sym, id| count_col = "#{sym}_count".to_sym count = object.send(count_col) if object.respond_to?(count_col) summary = { id: id, count: count } summary[:hidden] = true if sym == :vote - summary[:can_act] = true if scope.post_can_act?(object, sym, taken_actions: actions) + + if scope.post_can_act?(object, sym, opts: { taken_actions: actions }, can_see_post: can_see_post) + summary[:can_act] = true + end if sym == :notify_user && scope.current_user.present? && scope.current_user == object.user summary.delete(:can_act) @@ -276,7 +281,7 @@ class PostSerializer < BasicPostSerializer end def include_raw? - @add_raw.present? && (!object.hidden || scope.user.try(:staff?) || yours) + @add_raw.present? && (!object.hidden || scope.user&.staff? || yours) end def include_link_counts? @@ -326,7 +331,7 @@ class PostSerializer < BasicPostSerializer end def is_auto_generated - object.incoming_email.try(:is_auto_generated) + object.incoming_email&.is_auto_generated end def include_is_auto_generated? @@ -349,6 +354,16 @@ class PostSerializer < BasicPostSerializer include_action_code? && action_code_who.present? end + def last_wiki_edit + object.revisions.last.updated_at + end + + def include_last_wiki_edit? + object.wiki && + object.post_number == 1 && + object.revisions.size > 0 + end + private def post_actions diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 228a19c835..ffe6f706b7 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -21,7 +21,6 @@ class TopicViewSerializer < ApplicationSerializer :created_at, :views, :reply_count, - :participant_count, :like_count, :last_posted_at, :visible, @@ -35,17 +34,18 @@ class TopicViewSerializer < ApplicationSerializer :deleted_at, :pending_posts_count, :user_id, - :pm_with_non_human_user? + :pm_with_non_human_user?, + :featured_link, + :pinned_globally, + :pinned_at, + :pinned_until attributes :draft, :draft_key, :draft_sequence, :posted, :unpinned, - :pinned_globally, - :pinned, # Is topic pinned and viewer hasn't cleared the pin? - :pinned_at, # Ignores clear pin - :pinned_until, + :pinned, :details, :highest_post_number, :last_read_post_number, @@ -59,10 +59,10 @@ class TopicViewSerializer < ApplicationSerializer :bookmarked, :message_archived, :tags, - :featured_link, :topic_timer, :unicode_title, - :message_bus_last_id + :message_bus_last_id, + :participant_count # TODO: Split off into proper object / serializer def details @@ -83,7 +83,7 @@ class TopicViewSerializer < ApplicationSerializer result[:allowed_users] = object.topic.allowed_users.select do |user| !allowed_user_ids.include?(user.id) - end.map do |user| + end.map! do |user| BasicUserSerializer.new(user, scope: scope, root: false) end end @@ -94,7 +94,7 @@ class TopicViewSerializer < ApplicationSerializer end end - if object.suggested_topics.try(:topics).present? + if object.suggested_topics&.topics.present? result[:suggested_topics] = object.suggested_topics.topics.map do |t| SuggestedTopicSerializer.new(t, scope: scope, root: false) end @@ -193,10 +193,6 @@ class TopicViewSerializer < ApplicationSerializer end alias_method :include_posted?, :has_topic_user? - def pinned_globally - object.topic.pinned_globally - end - def pinned PinnedCheck.pinned?(object.topic, object.topic_user) end @@ -205,17 +201,9 @@ class TopicViewSerializer < ApplicationSerializer PinnedCheck.unpinned?(object.topic, object.topic_user) end - def pinned_at - object.topic.pinned_at - end - - def pinned_until - object.topic.pinned_until - end - def actions_summary result = [] - return [] unless post = object.posts.try(:first) + return [] unless post = object.posts&.first PostActionType.topic_flag_types.each do |sym, id| result << { id: id, count: 0, @@ -243,7 +231,7 @@ class TopicViewSerializer < ApplicationSerializer end def bookmarked - object.topic_user.try(:bookmarked) + object.topic_user&.bookmarked end def include_pending_posts_count? @@ -266,10 +254,6 @@ class TopicViewSerializer < ApplicationSerializer SiteSetting.topic_featured_link_enabled end - def featured_link - object.topic.featured_link - end - def include_unicode_title? !!(object.topic.title =~ /:([\w\-+]*):/) end @@ -282,6 +266,10 @@ class TopicViewSerializer < ApplicationSerializer private_message?(object.topic) end + def participant_count + object.participants.size + end + private def private_message?(topic) diff --git a/app/services/random_topic_selector.rb b/app/services/random_topic_selector.rb index f19b1a1718..4aaa7d3343 100644 --- a/app/services/random_topic_selector.rb +++ b/app/services/random_topic_selector.rb @@ -4,7 +4,7 @@ class RandomTopicSelector BACKFILL_LOW_WATER_MARK = 500 def self.backfill(category = nil) - exclude = category.try(:topic_id) + exclude = category&.topic_id # don't leak private categories into the "everything" group user = category ? CategoryFeaturedTopic.fake_admin : nil @@ -28,10 +28,13 @@ class RandomTopicSelector .pluck(:id) key = cache_key(category) - results.each do |id| - $redis.rpush(key, id) + + if results.present? + $redis.multi do + $redis.rpush(key, results) + $redis.expire(key, 2.days) + end end - $redis.expire(key, 2.days) results end @@ -77,7 +80,7 @@ class RandomTopicSelector end def self.cache_key(category = nil) - "random_topic_cache_#{category.try(:id)}" + "random_topic_cache_#{category&.id}" end end diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index f9b0146793..72b3da6068 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -21,6 +21,7 @@