Version bump

This commit is contained in:
Neil Lalonde 2019-03-28 11:05:56 -04:00
commit be4b531072
8002 changed files with 22590 additions and 7832 deletions

View File

@ -83,8 +83,8 @@ script:
yarn eslint app/assets/javascripts test/javascripts
else
if [ '$QUNIT_RUN' == '1' ]; then
bundle exec rake qunit:test['500000'] && \
bundle exec rake qunit:test['500000','/wizard/qunit'] && \
bundle exec rake qunit:test['1200000'] && \
bundle exec rake qunit:test['1200000','/wizard/qunit'] && \
bundle exec rake plugin:qunit
else
bundle exec rspec && bundle exec rake plugin:spec

View File

@ -49,7 +49,7 @@ gem 'onebox', '1.8.82'
gem 'http_accept_language', '~>2.0.5', require: false
gem 'ember-rails', '0.18.5'
gem 'discourse-ember-source', '~> 3.5.1'
gem 'discourse-ember-source', '~> 3.7.0'
gem 'ember-handlebars-template', '0.8.0'
gem 'barber'
@ -191,6 +191,7 @@ gem 'logstash-logger', require: false
gem 'logster'
gem 'sassc', require: false
gem "sassc-rails"
gem 'rotp'
gem 'rqrcode'

View File

@ -108,7 +108,7 @@ GEM
terminal-table (~> 1)
debug_inspector (0.0.3)
diff-lcs (1.3)
discourse-ember-source (3.5.1.3)
discourse-ember-source (3.7.0.2)
discourse_image_optim (0.26.2)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
@ -145,7 +145,7 @@ GEM
rake-compiler
fast_xs (0.8.0)
fastimage (2.1.5)
ffi (1.9.25)
ffi (1.10.0)
flamegraph (0.9.5)
fspath (3.1.0)
gc_tracer (1.5.1)
@ -186,7 +186,7 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.1.2)
logster (2.3.0)
loofah (2.2.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@ -320,8 +320,8 @@ GEM
rake-compiler (1.0.4)
rake
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rb-inotify (0.10.0)
ffi (~> 1.0)
rbtrace (0.4.11)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
@ -381,15 +381,15 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
sass (3.5.6)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sassc (1.11.4)
bundler
ffi (~> 1.9.6)
sass (>= 3.3.0)
sassc (2.0.1)
ffi (~> 1.9)
rake
sassc-rails (2.1.0)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
@ -466,7 +466,7 @@ DEPENDENCIES
colored2
cppjieba_rb
danger
discourse-ember-source (~> 3.5.1)
discourse-ember-source (~> 3.7.0)
discourse_image_optim
email_reply_trimmer (~> 0.1)
ember-handlebars-template (= 0.8.0)
@ -545,6 +545,7 @@ DEPENDENCIES
ruby-readability
sanitize
sassc
sassc-rails
seed-fu
shoulda
sidekiq

View File

@ -48,10 +48,10 @@ Discourse is built for the *next* 10 years of the Internet, so our requirements
| Browsers | Tablets | Phones |
| --------------------- | ------------ | ------------ |
| Safari 6.1+ | iPad 3+ | iOS 8+ |
| Google Chrome 32+ | Android 4.3+ | Android 4.3+ |
| Safari 10+ | iPad 4+ | iOS 10+ |
| Google Chrome 57+ | Android 4.4+ | Android 4.4+ |
| Internet Explorer 11+ | | |
| Firefox 27+ | | |
| Firefox 52+ | | |
## Built With

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 919 B

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1021 B

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -100,7 +100,7 @@ export default Ember.Component.extend({
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.on("ace:resize", () => this.resize());
this.appEvents.on("ace:resize", this, "resize");
}
if (this.get("autofocus")) {

View File

@ -79,8 +79,8 @@ export default Ember.Component.extend({
if (sortLabel) {
const compare = (label, direction) => {
return (a, b) => {
let aValue = label.compute(a).value;
let bValue = label.compute(b).value;
const aValue = label.compute(a, { useSortProperty: true }).value;
const bValue = label.compute(b, { useSortProperty: true }).value;
const result = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
return result * direction;
};

View File

@ -4,10 +4,6 @@ import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import { SCHEMA_VERSION, default as Report } from "admin/models/report";
import computed from "ember-addons/ember-computed-decorators";
import {
registerHoverTooltip,
unregisterHoverTooltip
} from "discourse/lib/tooltip";
const TABLE_OPTIONS = {
perPage: 8,
@ -56,6 +52,7 @@ export default Ember.Component.extend({
endDate: null,
category: null,
groupId: null,
filter: null,
showTrend: false,
showHeader: true,
showTitle: true,
@ -85,6 +82,7 @@ export default Ember.Component.extend({
this.setProperties({
category: Category.findById(state.categoryId),
groupId: state.groupId,
filter: state.filter,
startDate: state.startDate,
endDate: state.endDate
});
@ -100,18 +98,6 @@ export default Ember.Component.extend({
}
},
didRender() {
this._super(...arguments);
registerHoverTooltip($(".info[data-tooltip]"));
},
willDestroyElement() {
this._super(...arguments);
unregisterHoverTooltip($(".info[data-tooltip]"));
},
showError: Ember.computed.or(
"showTimeoutError",
"showExceptionError",
@ -174,6 +160,18 @@ export default Ember.Component.extend({
return `admin-report-${currentMode}`;
},
@computed("model.filter_options")
filterOptions(options) {
if (options) {
return options.map(option => {
if (option.allowAny) {
option.choices.unshift(I18n.t("admin.dashboard.report_filter_any"));
}
return option;
});
}
},
@computed("startDate")
normalizedStartDate(startDate) {
return startDate && typeof startDate.isValid === "function"
@ -202,10 +200,11 @@ export default Ember.Component.extend({
"dataSourceName",
"categoryId",
"groupId",
"filter",
"normalizedStartDate",
"normalizedEndDate"
)
reportKey(dataSourceName, categoryId, groupId, startDate, endDate) {
reportKey(dataSourceName, categoryId, groupId, filter, startDate, endDate) {
if (!dataSourceName || !startDate || !endDate) return null;
let reportKey = "reports:";
@ -215,6 +214,7 @@ export default Ember.Component.extend({
startDate.replace(/-/g, ""),
endDate.replace(/-/g, ""),
groupId,
filter,
"[:prev_period]",
this.get("reportOptions.table.limit"),
SCHEMA_VERSION
@ -227,10 +227,35 @@ export default Ember.Component.extend({
},
actions: {
filter(filterOptionId, value) {
let params = [];
let paramPairs = {};
let newParams = [];
if (this.get("filter")) {
const filter = this.get("filter").slice(1, -1);
params = filter.split("&") || [];
params.map(p => {
const pair = p.split("=");
paramPairs[pair[0]] = pair[1];
});
}
paramPairs[filterOptionId] = value;
Object.keys(paramPairs).forEach(key => {
if (paramPairs[key] !== I18n.t("admin.dashboard.report_filter_any")) {
newParams.push(`${key}=${paramPairs[key]}`);
}
});
this.set("filter", `[${newParams.join("&")}]`);
},
refreshReport() {
this.attrs.onRefresh({
categoryId: this.get("categoryId"),
groupId: this.get("groupId"),
filter: this.get("filter"),
startDate: this.get("startDate"),
endDate: this.get("endDate")
});
@ -366,6 +391,10 @@ export default Ember.Component.extend({
payload.data.category_id = this.get("categoryId");
}
if (this.get("filter") && this.get("filter") !== "all") {
payload.data.filter = this.get("filter");
}
if (this.get("reportOptions.table.limit")) {
payload.data.limit = this.get("reportOptions.table.limit");
}

View File

@ -4,8 +4,8 @@ import { bufferedRender } from "discourse-common/lib/buffered-render";
export default Ember.Component.extend(
bufferedRender({
classes: ["text-muted", "text-danger", "text-successful"],
icons: ["circle-o", "times-circle", "circle"],
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["circle-o", "times-circle", "circle", "circle"],
@computed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {

View File

@ -1,7 +1,7 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend({
queryParams: ["start_date", "end_date", "category_id", "group_id"],
queryParams: ["start_date", "end_date", "category_id", "group_id", "filter"],
@computed("model.type")
reportOptions(type) {
@ -14,11 +14,12 @@ export default Ember.Controller.extend({
return options;
},
@computed("category_id", "group_id", "start_date", "end_date")
filters(categoryId, groupId, startDate, endDate) {
@computed("category_id", "group_id", "start_date", "end_date", "filter")
filters(categoryId, groupId, startDate, endDate, filter) {
return {
categoryId,
groupId,
filter,
startDate,
endDate
};
@ -28,6 +29,7 @@ export default Ember.Controller.extend({
onParamsChange(params) {
this.setProperties({
start_date: params.startDate,
filter: params.filter,
category_id: params.categoryId,
group_id: params.groupId,
end_date: params.endDate

View File

@ -87,8 +87,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
);
},
showEmails: function() {
this.set("showEmails", true);
toggleEmailVisibility: function() {
this.toggleProperty("showEmails");
this._refreshUsers();
}
}

View File

@ -0,0 +1,42 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
export default Ember.Controller.extend(ModalFunctionality, {
loading: true,
reseeding: false,
categories: null,
topics: null,
onShow() {
ajax("/admin/customize/reseed")
.then(result => {
this.setProperties({
categories: result.categories,
topics: result.topics
});
})
.finally(() => this.set("loading", false));
},
_extractSelectedIds(items) {
return items.filter(item => item.selected).map(item => item.id);
},
actions: {
reseed() {
this.set("reseeding", true);
ajax("/admin/customize/reseed", {
data: {
category_ids: this._extractSelectedIds(this.categories),
topic_ids: this._extractSelectedIds(this.topics)
},
method: "POST"
})
.then(
() => this.send("closeModal"),
() => bootbox.alert(I18n.t("generic_error"))
)
.finally(() => this.set("reseeding", false));
}
}
});

View File

@ -264,7 +264,13 @@ const Report = Discourse.Model.extend({
mainProperty,
type,
compute: (row, opts = {}) => {
const value = row[mainProperty];
let value = null;
if (opts.useSortProperty) {
value = row[label.sort_property || mainProperty];
} else {
value = row[mainProperty];
}
if (type === "user") return this._userLabel(label.properties, row);
if (type === "post") return this._postLabel(label.properties, row);

View File

@ -1,3 +1,5 @@
import showModal from "discourse/lib/show-modal";
export default Ember.Route.extend({
queryParams: {
q: { replace: true },
@ -13,5 +15,11 @@ export default Ember.Route.extend({
setupController(controller, model) {
controller.set("siteTexts", model);
},
actions: {
showReseedModal() {
showModal("admin-reseed", { admin: true });
}
}
});

View File

@ -19,6 +19,7 @@ export default Discourse.Route.extend({
controller.setProperties({
originalPrimaryGroupId: model.get("primary_group_id"),
availableGroups: this._availableGroups,
customGroupIdsBuffer: null,
model
});
}

View File

@ -10,14 +10,14 @@ export default Discourse.Route.extend({
const routeName = "adminUsersList.show";
if (transition.targetName === routeName) {
const params = transition.params[routeName];
const params = transition.routeInfos.find(a => a.name === routeName)
.params;
const controller = this.controllerFor(routeName);
if (controller) {
controller.setProperties({
order: transition.queryParams.order,
ascending: transition.queryParams.ascending,
order: transition.to.queryParams.order,
ascending: transition.to.queryParams.ascending,
query: params.filter,
showEmails: false,
refreshing: false
});

View File

@ -88,7 +88,7 @@ export default Ember.Service.extend({
_deleteSpammer(adminUser) {
// Try loading the email if the site supports it
let tryEmail = this.siteSettings.show_email_on_profile
let tryEmail = this.siteSettings.moderators_view_emails
? adminUser.checkEmail()
: Ember.RSVP.resolve();

View File

@ -173,6 +173,18 @@
</div>
{{/if}}
{{#each filterOptions as |filterOption|}}
<div class="control">
<div class="input">
{{combo-box content=filterOption.choices
filterable=true
allowAny=true
value=filterOption.selected
onSelect=(action "filter" filterOption.id)}}
</div>
</div>
{{/each}}
{{#if showExport}}
<div class="control">
<div class="input">

View File

@ -4,7 +4,7 @@
<div class="moderators-activity section">
<div class="section-title">
<h2>
<a href="{{get-url '/admin/dashboard/reports/moderators_activity'}}">
<a href="{{get-url '/admin/reports/moderators_activity'}}">
{{i18n "admin.dashboard.moderators_activity"}}
</a>
</h2>

View File

@ -28,7 +28,11 @@
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td><a {{action "showIncomingEmail" l.id}}>{{l.email_type}}</a></td>
{{#if l.has_bounce_key}}
<td><a {{action "showIncomingEmail" l.id}}>{{l.email_type}}</a></td>
{{else}}
<td>{{l.email_type}}</td>
{{/if}}
</tr>
{{else}}
{{#unless loading}}

View File

@ -0,0 +1,40 @@
{{#d-modal-body title="admin.reseed.modal.title" subtitle="admin.reseed.modal.subtitle" class="reseed-modal"}}
{{#conditional-loading-spinner condition=loading}}
{{#if categories}}
<fieldset>
<legend class="options-group-title">{{i18n "admin.reseed.modal.categories"}}</legend>
{{#each categories as |category|}}
<label>
{{input class="option" type="checkbox" checked=category.selected}}
<span>{{category.name}}</span>
</label>
{{/each}}
</fieldset>
{{/if}}
<br>
{{#if topics}}
<fieldset>
<legend class="options-group-title">{{i18n "admin.reseed.modal.topics"}}</legend>
{{#each topics as |topic|}}
<label>
{{input class="option" type="checkbox" checked=topic.selected}}
<span>{{topic.name}}</span>
</label>
{{/each}}
</fieldset>
{{/if}}
{{/conditional-loading-spinner}}
{{/d-modal-body}}
<div class="modal-footer">
{{conditional-loading-spinner condition=reseeding size="small"}}
{{d-button action=(action "reseed") class="btn-danger" label="admin.reseed.modal.replace" disabled=reseeding}}
{{#unless reseeding}}
{{d-modal-cancel close=(route-action "closeModal")}}
{{/unless}}
</div>

View File

@ -7,12 +7,20 @@
autofocus="true"
key-up=(action "search")}}
<div class='extra-options'>
<div class="reseed">
{{d-button action=(route-action "showReseedModal")
class="btn-default"
label="admin.reseed.action.label"
title="admin.reseed.action.title"
icon="sync"}}
</div>
<p class="filter-options">
<label>
{{input type="checkbox" checked=overridden click=(action "toggleOverridden")}}
{{i18n 'admin.site_text.show_overriden'}}
</label>
</div>
</p>
</div>
{{#conditional-loading-spinner condition=searching}}

View File

@ -8,12 +8,16 @@
<div class="admin-title">
<h2>{{title}}</h2>
{{#if canCheckEmails}}
<button {{action "showEmails"}} class="show-emails btn btn-default">{{i18n 'admin.users.show_emails'}}</button>
{{#if showEmails}}
<button {{action "toggleEmailVisibility"}} class="hide-emails btn btn-default">{{i18n 'admin.users.hide_emails'}}</button>
{{else}}
<button {{action "toggleEmailVisibility"}} class="show-emails btn btn-default">{{i18n 'admin.users.show_emails'}}</button>
{{/if}}
{{/if}}
</div>
<div class='username controls'>
{{text-field value=listFilter placeholder=searchHint}}
</div>
{{#conditional-loading-spinner condition=refreshing}}
@ -23,9 +27,9 @@
{{#if showApproval}}
<th>{{input type="checkbox" checked=selectAll}}</th>
{{/if}}
<th>{{i18n 'username'}}</th>
<th class='email-heading'>{{i18n 'email'}}</th>
<th>{{i18n 'admin.users.last_emailed'}}</th>
{{admin-directory-toggle field="username" i18nKey='username' order=order ascending=ascending}}
{{admin-directory-toggle field="email" i18nKey='email' order=order ascending=ascending}}
{{admin-directory-toggle field="last_emailed" i18nKey='admin.users.last_emailed' order=order ascending=ascending}}
{{admin-directory-toggle field="seen" i18nKey='last_seen' order=order ascending=ascending}}
{{admin-directory-toggle field="topics_viewed" i18nKey="admin.user.topics_entered" order=order ascending=ascending}}
{{admin-directory-toggle field="posts_read" i18nKey="admin.user.posts_read_count" order=order ascending=ascending}}

View File

@ -38,6 +38,7 @@ const REPLACEMENTS = {
};
// TODO: use lib/svg_sprite/fa4-renames.json here
// Note: these should not be edited manually. They define the fa4-fa5 migration
const fa4Replacements = {
"500px": "fab-500px",
"address-book-o": "far-address-book",
@ -167,7 +168,7 @@ const fa4Replacements = {
"eye-slash": "far-eye-slash",
eyedropper: "eye-dropper",
fa: "fab-font-awesome",
facebook: "fab-facebook",
facebook: "fab-facebook-f",
"facebook-f": "fab-facebook-f",
"facebook-official": "fab-facebook",
"facebook-square": "fab-facebook-square",

View File

@ -41,7 +41,7 @@ const Discourse = Ember.Application.extend({
Resolver: buildResolver("discourse"),
@observes("_docTitle", "hasFocus", "notifyCount")
@observes("_docTitle", "hasFocus", "contextCount", "notificationCount")
_titleChanged() {
let title = this.get("_docTitle") || Discourse.SiteSettings.title;
@ -51,22 +51,34 @@ const Discourse = Ember.Application.extend({
$("title").text(title);
}
const notifyCount = this.get("notifyCount");
if (notifyCount > 0 && !Discourse.User.currentProp("dynamic_favicon")) {
title = `(${notifyCount}) ${title}`;
var displayCount = Discourse.User.current()
? this.get("notificationCount")
: this.get("contextCount");
if (displayCount > 0 && !Discourse.User.currentProp("dynamic_favicon")) {
title = `(${displayCount}) ${title}`;
}
document.title = title;
},
@observes("notifyCount")
@observes("contextCount", "notificationCount")
faviconChanged() {
if (Discourse.User.currentProp("dynamic_favicon")) {
let url = Discourse.SiteSettings.site_favicon_url;
// Since the favicon is cached on the browser for a really long time, we
// append the favicon_url as query params to the path so that the cache
// is not used when the favicon changes.
if (/^http/.test(url)) {
url = Discourse.getURL("/favicon/proxied?" + encodeURIComponent(url));
}
new window.Favcount(url).set(this.get("notifyCount"));
var displayCount = Discourse.User.current()
? this.get("notificationCount")
: this.get("contextCount");
new window.Favcount(url).set(displayCount);
}
},
@ -78,23 +90,33 @@ const Discourse = Ember.Application.extend({
});
},
notifyTitle(count) {
this.set("notifyCount", count);
updateContextCount(count) {
this.set("contextCount", count);
},
notifyBackgroundCountIncrement() {
updateNotificationCount(count) {
if (!this.get("hasFocus")) {
this.set("notificationCount", count);
}
},
incrementBackgroundContextCount() {
if (!this.get("hasFocus")) {
this.set("backgroundNotify", true);
this.set("notifyCount", (this.get("notifyCount") || 0) + 1);
this.set("contextCount", (this.get("contextCount") || 0) + 1);
}
},
@observes("hasFocus")
resetBackgroundNotifyCount() {
resetCounts() {
if (this.get("hasFocus") && this.get("backgroundNotify")) {
this.set("notifyCount", 0);
this.set("contextCount", 0);
}
this.set("backgroundNotify", false);
if (this.get("hasFocus")) {
this.set("notificationCount", 0);
}
},
authenticationComplete(options) {

View File

@ -13,20 +13,24 @@ export default Ember.Component.extend({
return;
}
const slug = this.get("category.fullSlug");
const tags = this.get("tags");
this._removeClass();
if (slug) {
$("body").addClass(`category-${slug}`);
}
let classes = [];
if (slug) classes.push(`category-${slug}`);
if (tags) tags.forEach(t => classes.push(`tag-${t}`));
if (classes.length > 0) $("body").addClass(classes.join(" "));
},
@observes("category.fullSlug")
@observes("category.fullSlug", "tags")
refreshClass() {
Ember.run.scheduleOnce("afterRender", this, this._updateClass);
},
_removeClass() {
$("body").removeClass((_, css) =>
(css.match(/\bcategory-\S+/g) || []).join(" ")
(css.match(/\b(?:category|tag)-\S+/g) || []).join(" ")
);
},

View File

@ -916,7 +916,10 @@ export default Ember.Component.extend({
Ember.run.next(() => {
$("#main-outlet").css("padding-bottom", 0);
// need to wait a bit for the "slide down" transition of the composer
Ember.run.later(() => this.appEvents.trigger("composer:closed"), 400);
Ember.run.later(
() => this.appEvents.trigger("composer:closed"),
Ember.testing ? 0 : 400
);
});
if (this._enableAdvancedEditorPreviewSync())

View File

@ -0,0 +1 @@
export const searchPriorities = <%= Searchable::PRIORITIES.to_json %>;

View File

@ -290,25 +290,27 @@ export default Ember.Component.extend({
});
if (this.get("composerEvents")) {
this.appEvents.on("composer:insert-block", text =>
this._addBlock(this._getSelected(), text)
);
this.appEvents.on("composer:insert-text", (text, options) =>
this._addText(this._getSelected(), text, options)
);
this.appEvents.on("composer:replace-text", (oldVal, newVal, opts) =>
this._replaceText(oldVal, newVal, opts)
);
this.appEvents.on("composer:insert-block", this, "_insertBlock");
this.appEvents.on("composer:insert-text", this, "_insertText");
this.appEvents.on("composer:replace-text", this, "_replaceText");
}
this._mouseTrap = mouseTrap;
},
_insertBlock(text) {
this._addBlock(this._getSelected(), text);
},
_insertText(text, options) {
this._addText(this._getSelected(), text, options);
},
@on("willDestroyElement")
_shutDown() {
if (this.get("composerEvents")) {
this.appEvents.off("composer:insert-block");
this.appEvents.off("composer:insert-text");
this.appEvents.off("composer:replace-text");
this.appEvents.off("composer:insert-block", this, "_insertBlock");
this.appEvents.off("composer:insert-text", this, "_insertText");
this.appEvents.off("composer:replace-text", this, "_replaceText");
}
const mouseTrap = this._mouseTrap;

View File

@ -14,14 +14,14 @@ export default Ember.Component.extend({
}
Ember.run.scheduleOnce("afterRender", this, this._afterFirstRender);
this.appEvents.on("modal-body:flash", msg => this._flash(msg));
this.appEvents.on("modal-body:clearFlash", () => this._clearFlash());
this.appEvents.on("modal-body:flash", this, "_flash");
this.appEvents.on("modal-body:clearFlash", this, "_clearFlash");
},
willDestroyElement() {
this._super(...arguments);
this.appEvents.off("modal-body:flash");
this.appEvents.off("modal-body:clearFlash");
this.appEvents.off("modal-body:flash", this, "_flash");
this.appEvents.off("modal-body:clearFlash", this, "_clearFlash");
},
_afterFirstRender() {

View File

@ -5,6 +5,10 @@ import Scrolling from "discourse/mixins/scrolling";
import { selectedText } from "discourse/lib/utilities";
import { observes } from "ember-addons/ember-computed-decorators";
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 300;
// Small buffer so that very tiny scrolls don't trigger mobile header switch
const MOBILE_SCROLL_TOLERANCE = 5;
function highlight(postNumber) {
const $contents = $(`#post_${postNumber} .topic-body`);
@ -12,9 +16,6 @@ function highlight(postNumber) {
$contents.on("animationend", () => $contents.removeClass("highlighted"));
}
// used to determine scroll direction on mobile
let lastScroll, scrollDirection, delta;
export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
userFilters: Ember.computed.alias("topic.userFilters"),
classNameBindings: [
@ -23,7 +24,8 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
"topic.is_warning",
"topic.category.read_restricted:read_restricted",
"topic.deleted:deleted-topic",
"topic.categoryClass"
"topic.categoryClass",
"topic.tagClasses"
],
menuVisible: true,
SHORT_POST: 1200,
@ -34,6 +36,9 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
_lastShowTopic: null,
mobileScrollDirection: null,
_mobileLastScroll: null,
@observes("enteredAt")
_enteredTopic() {
// Ember is supposed to only call observers when values change but something
@ -47,6 +52,27 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
}
},
_highlightPost(postNumber) {
Ember.run.scheduleOnce("afterRender", null, highlight, postNumber);
},
_updateTopic(topic) {
if (topic === null) {
this._lastShowTopic = false;
this.appEvents.trigger("header:hide-topic");
return;
}
const offset = window.pageYOffset || $("html").scrollTop();
this._lastShowTopic = this.showTopicInHeader(topic, offset);
if (this._lastShowTopic) {
this.appEvents.trigger("header:show-topic", topic);
} else {
this.appEvents.trigger("header:hide-topic");
}
},
didInsertElement() {
this._super(...arguments);
this.bindScrolling({ name: "topic-view" });
@ -77,43 +103,9 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
}
);
this.appEvents.on("post:highlight", postNumber => {
Ember.run.scheduleOnce("afterRender", null, highlight, postNumber);
});
this.appEvents.on("post:highlight", this, "_highlightPost");
this.appEvents.on("header:update-topic", topic => {
if (topic === null) {
this._lastShowTopic = false;
this.appEvents.trigger("header:hide-topic");
return;
}
const offset = window.pageYOffset || $("html").scrollTop();
this._lastShowTopic = this.showTopicInHeader(topic, offset);
if (this._lastShowTopic) {
this.appEvents.trigger("header:show-topic", topic);
} else {
this.appEvents.trigger("header:hide-topic");
}
});
// setup mobile scroll logo
if (this.site.mobileView) {
this.appEvents.on("topic:scrolled", offset =>
this.mobileScrollGaurd(offset)
);
// used to animate header contents on scroll
this.appEvents.on("header:show-topic", () => {
$("header.d-header")
.removeClass("scroll-up")
.addClass("scroll-down");
});
this.appEvents.on("header:hide-topic", () => {
$("header.d-header")
.removeClass("scroll-down")
.addClass("scroll-up");
});
}
this.appEvents.on("header:update-topic", this, "_updateTopic");
},
willDestroyElement() {
@ -128,12 +120,8 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
// this happens after route exit, stuff could have trickled in
this.appEvents.trigger("header:hide-topic");
this.appEvents.off("post:highlight");
// mobile scroll logo clean up.
if (this.site.mobileView) {
this.appEvents.off("topic:scrolled");
$("header.d-header").removeClass("scroll-down scroll-up");
}
this.appEvents.off("post:highlight", this, "_highlightPost");
this.appEvents.off("header:update-topic", this, "_updateTopic");
},
@observes("Discourse.hasFocus")
@ -148,17 +136,13 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
},
showTopicInHeader(topic, offset) {
// conditions for showing topic title in the header for mobile
if (
this.site.mobileView &&
scrollDirection !== "up" &&
offset > this.dockAt
) {
return true;
// condition for desktops
} else {
return offset > this.dockAt;
}
// On mobile, we show the header topic if the user has scrolled past the topic
// title and the current scroll direction is down
// On desktop the user only needs to scroll past the topic title.
return (
offset > this.dockAt &&
(!this.site.mobileView || this.mobileScrollDirection === "down")
);
},
// The user has scrolled the window, or it is finished rendering and ready for processing.
scrolled() {
@ -193,25 +177,61 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
}
}
// Since the user has scrolled, we need to check the scroll direction on mobile.
// We use throttle instead of debounce because we want the switch to occur
// at the start of the scroll. This feels a lot more snappy compared to waiting
// for the scroll to end if we debounce.
if (this.site.mobileView && this.hasScrolled) {
Ember.run.throttle(
this,
this._mobileScrollDirectionCheck,
offset,
MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE
);
}
// Trigger a scrolled event
this.appEvents.trigger("topic:scrolled", offset);
},
// determines scroll direction, triggers header topic info on mobile
// and ensures that the switch happens only once per scroll direction change
mobileScrollGaurd(offset) {
// user hasn't scrolled past topic title.
if (offset < this.dockAt) return;
_mobileScrollDirectionCheck(offset) {
// Difference between this scroll and the one before it.
const delta = Math.floor(offset - this._mobileLastScroll);
delta = offset - lastScroll;
// 3px buffer so that the switch doesn't happen with tiny scrolls
if (delta > 3 && scrollDirection !== "down") {
scrollDirection = "down";
this.appEvents.trigger("header:show-topic", this.topic);
} else if (delta < -3 && scrollDirection !== "up") {
scrollDirection = "up";
this.appEvents.trigger("header:hide-topic");
// This is a tiny scroll, so we ignore it.
if (delta <= MOBILE_SCROLL_TOLERANCE && delta >= -MOBILE_SCROLL_TOLERANCE)
return;
const prevDirection = this.mobileScrollDirection;
const currDirection = delta > 0 ? "down" : "up";
if (currDirection !== prevDirection) {
this.set("mobileScrollDirection", currDirection);
}
lastScroll = offset;
// We store this to compare against it the next time the user scrolls
this._mobileLastScroll = Math.floor(offset);
// If the user reaches the very bottom of the topic, we want to reset the
// scroll direction in order for the header to switch back.
const distanceToTopicBottom = Math.floor(
$("body").height() - offset - $(window).height()
);
// Not at the bottom yet
if (distanceToTopicBottom > 0) return;
// We're at the bottom now, so we reset the direction.
this.set("mobileScrollDirection", null);
},
// We observe the scroll direction on mobile and if it's down, we show the topic
// in the header, otherwise, we hide it.
@observes("mobileScrollDirection")
toggleMobileHeaderTopic() {
return this.appEvents.trigger(
"header:update-topic",
this.mobileScrollDirection === "down" ? this.get("topic") : null
);
}
});

View File

@ -24,7 +24,7 @@ const DiscoveryTopicsListComponent = Ember.Component.extend(
@observes("incomingCount")
_updateTitle() {
Discourse.notifyTitle(this.get("incomingCount"));
Discourse.updateContextCount(this.get("incomingCount"));
},
saveScrollPosition() {
@ -38,7 +38,7 @@ const DiscoveryTopicsListComponent = Ember.Component.extend(
actions: {
loadMore() {
Discourse.notifyTitle(0);
Discourse.updateContextCount(0);
this.get("model")
.loadMore()
.then(hasMoreResults => {

View File

@ -1,4 +1,3 @@
import DiscourseURL from "discourse/lib/url";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
import Category from "discourse/models/category";
@ -94,7 +93,7 @@ export default buildCategoryPanel("general", {
actions: {
showCategoryTopic() {
DiscourseURL.routeTo(this.get("category.topic_url"));
window.open(this.get("category.topic_url"), "_blank").focus();
return false;
}
}

View File

@ -1,6 +1,7 @@
import { setting } from "discourse/lib/computed";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import computed from "ember-addons/ember-computed-decorators";
import { searchPriorities } from "discourse/components/concerns/category_search_priorities";
const categorySortCriteria = [];
export function addCategorySortCriteria(criteria) {
@ -57,6 +58,20 @@ export default buildCategoryPanel("settings", {
);
},
@computed
searchPrioritiesOptions() {
const options = [];
for (const [name, value] of Object.entries(searchPriorities)) {
options.push({
name: I18n.t(`category.search_priority.options.${name}`),
value: value
});
}
return options;
},
@computed
availableSorts() {
return [

View File

@ -77,7 +77,11 @@ export default Ember.Component.extend({
@on("willDestroyElement")
_unbindGlobalEvents() {
this.appEvents.off("emoji-picker:close");
this.appEvents.off("emoji-picker:close", this, "_closeEmojiPicker");
},
_closeEmojiPicker() {
this.set("active", false);
},
@on("didInsertElement")
@ -85,7 +89,7 @@ export default Ember.Component.extend({
this.$picker = this.$(".emoji-picker");
this.$modal = this.$(".emoji-picker-modal");
this.appEvents.on("emoji-picker:close", () => this.set("active", false));
this.appEvents.on("emoji-picker:close", this, "_closeEmojiPicker");
if (!keyValueStore.getObject(EMOJI_USAGE)) {
keyValueStore.setObject({ key: EMOJI_USAGE, value: [] });

View File

@ -5,7 +5,7 @@ import { bufferedRender } from "discourse-common/lib/buffered-render";
export default Ember.Component.extend(
bufferedRender({
rerenderTriggers: ["site.isReadOnly"],
rerenderTriggers: ["site.isReadOnly", "siteSettings.disable_emails"],
buildBuffer(buffer) {
let notices = [];
@ -25,8 +25,7 @@ export default Ember.Component.extend(
if (
this.siteSettings.disable_emails === "yes" ||
(this.siteSettings.disable_emails === "non-staff" &&
!(this.currentUser && this.currentUser.get("staff")))
this.siteSettings.disable_emails === "non-staff"
) {
notices.push([I18n.t("emails_are_disabled"), "alert-emails-disabled"]);
}

View File

@ -2,6 +2,7 @@ import { setting } from "discourse/lib/computed";
import { default as computed } from "ember-addons/ember-computed-decorators";
import CardContentsBase from "discourse/mixins/card-contents-base";
import CleansUp from "discourse/mixins/cleans-up";
import { groupPath } from "discourse/lib/url";
const maxMembersToDisplay = 10;
@ -23,6 +24,11 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
viewingTopic: Ember.computed.match("currentPath", /^topic\./),
showMoreMembers: Ember.computed.gt("moreMembersCount", 0),
hasMembersOrIsMember: Ember.computed.or(
"group.members",
"group.is_group_owner_display",
"group.is_group_user"
),
group: null,
@ -35,7 +41,7 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
@computed("group")
groupPath(group) {
return `${Discourse.BaseUri}/g/${group.name}`;
return groupPath(group.name);
},
_showCallback(username, $target) {
@ -83,6 +89,11 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
showGroup(group) {
this.showGroup(group);
this._close();
},
showUser(user) {
this.showUser(user);
this._close();
}
}
});

View File

@ -18,7 +18,7 @@ export default Ember.Component.extend({
"#login-account-password, #login-account-name, #login-second-factor"
).keydown(e => {
if (e.keyCode === 13) {
this.sendAction();
this.action();
}
});
});

View File

@ -1,33 +1,27 @@
/* You might be looking for navigation-item. */
import computed from "ember-addons/ember-computed-decorators";
import { getOwner } from "discourse-common/lib/get-owner";
export default Ember.Component.extend({
tagName: "li",
classNameBindings: ["active"],
@computed()
router() {
return getOwner(this).lookup("router:main");
},
router: Ember.inject.service(),
@computed("path")
fullPath(path) {
return Discourse.getURL(path);
},
@computed("route", "router.url")
active(route) {
@computed("route", "router.currentRoute")
active(route, currentRoute) {
if (!route) {
return;
}
const routeParam = this.get("routeParam"),
router = this.get("router");
const routeParam = this.get("routeParam");
if (routeParam && currentRoute) {
return currentRoute.params["filter"] === routeParam;
}
return routeParam
? router.isActive(route, routeParam)
: router.isActive(route);
return this.get("router").isActive(route);
}
});

View File

@ -262,6 +262,38 @@ export default MountWidget.extend({
Ember.run.scheduleOnce("afterRender", this, this.scrolled);
},
_posted(staged) {
const disableJumpReply = this.currentUser.get("disable_jump_reply");
this.queueRerender(() => {
if (staged && !disableJumpReply) {
const postNumber = staged.get("post_number");
DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true });
}
});
},
_refresh(args) {
if (args) {
if (args.id) {
this.dirtyKeys.keyDirty(`post-${args.id}`);
if (args.refreshLikes) {
this.dirtyKeys.keyDirty(`post-menu-${args.id}`, {
onRefresh: "refreshLikes"
});
}
} else if (args.force) {
this.dirtyKeys.forceAll();
}
}
this.queueRerender();
},
_debouncedScroll() {
Ember.run.debounce(this, this._scrollTriggered, 10);
},
didInsertElement() {
this._super(...arguments);
const debouncedScroll = () =>
@ -269,21 +301,12 @@ export default MountWidget.extend({
this._previouslyNearby = {};
this.appEvents.on("post-stream:refresh", debouncedScroll);
this.appEvents.on("post-stream:refresh", this, "_debouncedScroll");
$(document).bind("touchmove.post-stream", debouncedScroll);
$(window).bind("scroll.post-stream", debouncedScroll);
this._scrollTriggered();
this.appEvents.on("post-stream:posted", staged => {
const disableJumpReply = this.currentUser.get("disable_jump_reply");
this.queueRerender(() => {
if (staged && !disableJumpReply) {
const postNumber = staged.get("post_number");
DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true });
}
});
});
this.appEvents.on("post-stream:posted", this, "_posted");
this.$().on("mouseenter.post-stream", "button.widget-button", e => {
$("button.widget-button").removeClass("d-hover");
@ -294,33 +317,18 @@ export default MountWidget.extend({
$("button.widget-button").removeClass("d-hover");
});
this.appEvents.on("post-stream:refresh", args => {
if (args) {
if (args.id) {
this.dirtyKeys.keyDirty(`post-${args.id}`);
if (args.refreshLikes) {
this.dirtyKeys.keyDirty(`post-menu-${args.id}`, {
onRefresh: "refreshLikes"
});
}
} else if (args.force) {
this.dirtyKeys.forceAll();
}
}
this.queueRerender();
});
this.appEvents.on("post-stream:refresh", this, "_refresh");
},
willDestroyElement() {
this._super(...arguments);
$(document).unbind("touchmove.post-stream");
$(window).unbind("scroll.post-stream");
this.appEvents.off("post-stream:refresh");
this.appEvents.off("post-stream:refresh", this, "_debouncedScroll");
this.$().off("mouseenter.post-stream");
this.$().off("mouseleave.post-stream");
this.appEvents.off("post-stream:refresh");
this.appEvents.off("post-stream:posted");
this.appEvents.off("post-stream:refresh", this, "_refresh");
this.appEvents.off("post-stream:posted", this, "_posted");
},
showModerationHistory(post) {

View File

@ -154,6 +154,14 @@ export default Ember.Component.extend({
},
actions: {
replyAsNewTopic() {
const postStream = this.get("topic.postStream");
const postId = this.postId || postStream.findPostIdForPostNumber(1);
const post = postStream.findLoadedPost(postId);
this.replyAsNewTopic(post);
this.send("close");
},
close() {
this.setProperties({
link: null,

View File

@ -231,19 +231,14 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
const { isAndroid } = this.capabilities;
$(window).on("resize.discourse-menu-panel", () => this.afterRender());
this.appEvents.on("header:show-topic", topic => this.setTopic(topic));
this.appEvents.on("header:hide-topic", () => this.setTopic(null));
this.appEvents.on("header:show-topic", this, "setTopic");
this.appEvents.on("header:hide-topic", this, "setTopic");
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
if (this.$(".menu-panel").length) {
this.eventDispatched("dom:clean", "header");
}
});
this.appEvents.on("dom:clean", this, "_cleanDom");
// Only add listeners for opening menus by swiping them in on Android devices
// iOS will respond to these events, but also does swiping for back/forward
@ -252,15 +247,22 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
}
},
_cleanDom() {
// For performance, only trigger a re-render if any menu panels are visible
if (this.$(".menu-panel").length) {
this.eventDispatched("dom:clean", "header");
}
},
willDestroyElement() {
this._super(...arguments);
const { isAndroid } = this.capabilities;
$("body").off("keydown.header");
$(window).off("resize.discourse-menu-panel");
this.appEvents.off("header:show-topic");
this.appEvents.off("header:hide-topic");
this.appEvents.off("dom:clean");
this.appEvents.off("header:show-topic", this, "setTopic");
this.appEvents.off("header:hide-topic", this, "setTopic");
this.appEvents.off("dom:clean", this, "_cleanDom");
if (isAndroid) {
this.removeTouchListeners($("body"));

View File

@ -53,7 +53,7 @@ export default Ember.Component.extend(CleansUp, {
didInsertElement() {
this._super(...arguments);
this.appEvents.on("topic-entrance:show", data => this._show(data));
this.appEvents.on("topic-entrance:show", this, "_show");
},
_setCSS() {
@ -100,7 +100,7 @@ export default Ember.Component.extend(CleansUp, {
},
willDestroyElement() {
this.appEvents.off("topic-entrance:show");
this.appEvents.off("topic-entrance:show", this, "_show");
},
_jumpTo(destination) {

View File

@ -71,6 +71,10 @@ export default Ember.Component.extend(
classes.push("category-" + topic.get("category.fullSlug"));
}
if (topic.get("tags")) {
topic.get("tags").forEach(tagName => classes.push("tag-" + tagName));
}
if (topic.get("hasExcerpt")) {
classes.push("has-excerpt");
}

View File

@ -1,4 +1,3 @@
import { getOwner } from "discourse-common/lib/get-owner";
import {
default as computed,
observes
@ -155,16 +154,17 @@ export default Ember.Component.extend({
const $wrapper = this.$();
if (!$wrapper || $wrapper.length === 0) return;
const $html = $("html"),
offset = window.pageYOffset || $html.scrollTop(),
progressHeight = this.site.mobileView ? 0 : $("#topic-progress").height(),
maximumOffset = $("#topic-bottom").offset().top + progressHeight,
windowHeight = $(window).height(),
bodyHeight = $("body").height(),
composerHeight = $("#reply-control").height() || 0,
isDocked = offset >= maximumOffset - windowHeight + composerHeight,
bottom = bodyHeight - maximumOffset,
wrapperDir = $html.hasClass("rtl") ? "left" : "right";
const $html = $("html");
const offset = window.pageYOffset || $html.scrollTop();
const progressHeight = this.site.mobileView
? 0
: $("#topic-progress").height();
const maximumOffset = $("#topic-bottom").offset().top + progressHeight;
const windowHeight = $(window).height();
const composerHeight = $("#reply-control").height() || 0;
const isDocked = offset >= maximumOffset - windowHeight + composerHeight;
const bottom = $("body").height() - maximumOffset;
const wrapperDir = $html.hasClass("rtl") ? "left" : "right";
if (composerHeight > 0) {
$wrapper.css("bottom", isDocked ? bottom : composerHeight);
@ -180,25 +180,6 @@ export default Ember.Component.extend({
} else {
$wrapper.css(wrapperDir, "1em");
}
// switch mobile scroll logo at the very bottom of topics
if (this.site.mobileView) {
const isIOS = this.capabilities.isIOS,
switchHeight = bodyHeight - offset - windowHeight,
appEvents = getOwner(this).lookup("app-events:main");
if (isIOS && switchHeight < -10) {
// match elastic-scroll behaviour in iOS
setTimeout(function() {
appEvents.trigger("header:hide-topic");
}, 300);
} else if (!isIOS && switchHeight < 5) {
// normal switch for everyone else
setTimeout(function() {
appEvents.trigger("header:hide-topic");
}, 300);
}
}
},
click(e) {

View File

@ -46,6 +46,10 @@ export default Ember.Component.extend(
"user.location",
"user.website_name"
),
isSuspendedOrHasBio: Ember.computed.or(
"user.suspend_reason",
"user.bio_cooked"
),
showCheckEmail: Ember.computed.and("user.staged", "canCheckEmails"),
user: null,
@ -53,6 +57,12 @@ export default Ember.Component.extend(
// If inside a topic
topicPostCount: null,
@computed("user.staff")
staff: isStaff => (isStaff ? "staff" : ""),
@computed("user.trust_level")
newUser: trustLevel => (trustLevel === 0 ? "new-user" : ""),
@computed("user.name")
nameFirst(name) {
return (
@ -195,8 +205,8 @@ export default Ember.Component.extend(
this._close();
},
showUser() {
this.showUser(this.get("user"));
showUser(username) {
this.showUser(username);
this._close();
},

View File

@ -13,7 +13,7 @@ export default Ember.Component.extend({
$(window).on("load.faq resize.faq scroll.faq", () => {
const faqUnread = !currentUser.get("read_faq");
if (faqUnread && isElementInViewport($(".contents p").last())) {
this.sendAction();
this.action();
}
});
}

View File

@ -910,7 +910,7 @@ export default Ember.Controller.extend({
opts.topicTitle &&
opts.topicTitle.length <= this.siteSettings.max_topic_title_length
) {
this.set("model.title", escapeExpression(opts.topicTitle));
this.set("model.title", opts.topicTitle);
}
if (opts.topicCategoryId) {

View File

@ -24,7 +24,9 @@ const controllerOpts = {
expandAllPinned: false,
resetParams() {
this.setProperties({ order: "default", ascending: false });
Object.keys(this.get("model.params") || {}).forEach(key =>
this.set(key, null)
);
},
actions: {

View File

@ -204,6 +204,8 @@ export default Ember.Controller.extend({
return page === PAGE_LIMIT;
},
searchButtonDisabled: Ember.computed.or("searching", "loading"),
_search() {
if (this.get("searching")) {
return;
@ -218,11 +220,12 @@ export default Ember.Controller.extend({
let args = { q: searchTerm, page: this.get("page") };
this.set("searching", true);
this.set("loading", true);
if (args.page === 1) {
this.set("bulkSelectEnabled", false);
this.get("selected").clear();
this.set("searching", true);
} else {
this.set("loading", true);
}
const sortOrder = this.get("sortOrder");

View File

@ -0,0 +1,122 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Group from "discourse/models/group";
import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
import debounce from "discourse/lib/debounce";
export default Ember.Controller.extend({
queryParams: ["order", "desc", "filter"],
order: "",
desc: null,
loading: false,
limit: null,
offset: null,
filter: null,
filterInput: null,
application: Ember.inject.controller(),
@observes("filterInput")
_setFilter: debounce(function() {
this.set("filter", this.get("filterInput"));
}, 500),
@observes("order", "desc", "filter")
refreshRequesters(force) {
if (this.get("loading") || !this.get("model")) {
return;
}
if (
!force &&
this.get("count") &&
this.get("model.requesters.length") >= this.get("count")
) {
this.set("application.showFooter", true);
return;
}
this.set("loading", true);
this.set("application.showFooter", false);
Group.loadMembers(
this.get("model.name"),
force ? 0 : this.get("model.requesters.length"),
this.get("limit"),
{
order: this.get("order"),
desc: this.get("desc"),
filter: this.get("filter"),
requesters: true
}
).then(result => {
const requesters = (!force && this.get("model.requesters")) || [];
requesters.addObjects(result.members.map(m => Discourse.User.create(m)));
this.set("model.requesters", requesters);
this.setProperties({
loading: false,
count: result.meta.total,
limit: result.meta.limit,
offset: Math.min(
result.meta.offset + result.meta.limit,
result.meta.total
)
});
});
},
@computed("model.requesters")
hasRequesters(requesters) {
return requesters && requesters.length > 0;
},
@computed
filterPlaceholder() {
if (this.currentUser && this.currentUser.admin) {
return "groups.members.filter_placeholder_admin";
} else {
return "groups.members.filter_placeholder";
}
},
handleRequest(data) {
ajax(`/groups/${this.get("model.id")}/handle_membership_request.json`, {
data,
type: "PUT"
}).catch(popupAjaxError);
},
actions: {
loadMore() {
this.refreshRequesters();
},
acceptRequest(user) {
this.handleRequest({ user_id: user.get("id"), accept: true });
user.setProperties({
request_accepted: true,
request_denied: false
});
},
undoAcceptRequest(user) {
ajax("/groups/" + this.get("model.id") + "/members.json", {
type: "DELETE",
data: { user_id: user.get("id") }
}).then(() => {
user.set("request_undone", true);
});
},
denyRequest(user) {
this.handleRequest({ user_id: user.get("id") });
user.setProperties({
request_accepted: false,
request_denied: true
});
}
}
});

View File

@ -15,8 +15,13 @@ export default Ember.Controller.extend({
showing: "members",
destroying: null,
@computed("showMessages", "model.user_count", "canManageGroup")
tabs(showMessages, userCount, canManageGroup) {
@computed(
"showMessages",
"model.user_count",
"canManageGroup",
"model.allow_membership_requests"
)
tabs(showMessages, userCount, canManageGroup, allowMembershipRequests) {
const membersTab = Tab.create({
name: "members",
route: "group.index",
@ -28,6 +33,16 @@ export default Ember.Controller.extend({
const defaultTabs = [membersTab, Tab.create({ name: "activity" })];
if (canManageGroup && allowMembershipRequests) {
defaultTabs.push(
Tab.create({
name: "requests",
i18nKey: "requests.title",
icon: "user-plus"
})
);
}
if (showMessages) {
defaultTabs.push(
Tab.create({

View File

@ -2,15 +2,29 @@ import PreferencesTabController from "discourse/mixins/preferences-tab-controlle
import { default as computed } from "ember-addons/ember-computed-decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
const EMAIL_LEVELS = {
ALWAYS: 0,
ONLY_WHEN_AWAY: 1,
NEVER: 2
};
export default Ember.Controller.extend(PreferencesTabController, {
emailMessagesLevelAway: Ember.computed.equal(
"model.user_option.email_messages_level",
EMAIL_LEVELS.ONLY_WHEN_AWAY
),
emailLevelAway: Ember.computed.equal(
"model.user_option.email_level",
EMAIL_LEVELS.ONLY_WHEN_AWAY
),
saveAttrNames: [
"email_always",
"email_level",
"email_messages_level",
"mailing_list_mode",
"mailing_list_mode_frequency",
"email_digests",
"email_direct",
"email_in_reply_to",
"email_private_messages",
"email_previous_replies",
"digest_after_minutes",
"include_tl0_in_digests"
@ -42,15 +56,35 @@ export default Ember.Controller.extend(PreferencesTabController, {
{ name: I18n.t("user.email_previous_replies.never"), value: 2 }
],
emailLevelOptions: [
{ name: I18n.t("user.email_level.always"), value: EMAIL_LEVELS.ALWAYS },
{
name: I18n.t("user.email_level.only_when_away"),
value: EMAIL_LEVELS.ONLY_WHEN_AWAY
},
{ name: I18n.t("user.email_level.never"), value: EMAIL_LEVELS.NEVER }
],
digestFrequencies: [
{ name: I18n.t("user.email_digests.every_30_minutes"), value: 30 },
{ name: I18n.t("user.email_digests.every_hour"), value: 60 },
{ name: I18n.t("user.email_digests.daily"), value: 1440 },
{ name: I18n.t("user.email_digests.every_three_days"), value: 4320 },
{ name: I18n.t("user.email_digests.weekly"), value: 10080 },
{ name: I18n.t("user.email_digests.every_two_weeks"), value: 20160 }
{ name: I18n.t("user.email_digests.every_month"), value: 43200 },
{ name: I18n.t("user.email_digests.every_six_months"), value: 259200 }
],
@computed()
emailFrequencyInstructions() {
if (this.siteSettings.email_time_window_mins) {
return I18n.t("user.email.frequency", {
count: this.siteSettings.email_time_window_mins
});
} else {
return I18n.t("user.email.frequency_immediately");
}
},
actions: {
save() {
this.set("saved", false);

View File

@ -39,6 +39,11 @@ export default Ember.Controller.extend({
return findAll().length > 0;
},
@computed("currentUser")
showEnforcedNotice(user) {
return user && user.get("enforcedSecondFactor");
},
toggleSecondFactor(enable) {
if (!this.get("secondFactorToken")) return;
this.set("loading", true);

View File

@ -85,8 +85,11 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
actions: {
change(cat, e) {
let position = parseInt($(e.target).val());
cat.set("position", position);
this.fixIndices();
let amount = Math.min(
Math.max(position, 0),
this.get("categoriesOrdered").length - 1
);
this.moveDir(cat, amount - this.get("categoriesOrdered").indexOf(cat));
},
moveUp(cat) {

View File

@ -1226,7 +1226,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
case "created": {
postStream.triggerNewPostInStream(data.id).then(() => refresh());
if (this.get("currentUser.id") !== data.user_id) {
Discourse.notifyBackgroundCountIncrement();
Discourse.incrementBackgroundContextCount();
}
break;
}

View File

@ -1,3 +1,9 @@
import {
default as DiscourseURL,
userPath,
groupPath
} from "discourse/lib/url";
export default Ember.Controller.extend({
topic: Ember.inject.controller(),
application: Ember.inject.controller(),
@ -9,7 +15,11 @@ export default Ember.Controller.extend({
},
showUser(user) {
this.transitionToRoute("user", user);
DiscourseURL.routeTo(userPath(user.username_lower));
},
showGroup(group) {
DiscourseURL.routeTo(groupPath(group.name));
}
}
});

View File

@ -33,7 +33,10 @@ export default Ember.Controller.extend(CanCheckEmails, {
}
return (!indexStream || viewingSelf) && !forceExpand;
},
canMuteOrIgnoreUser: Ember.computed.or(
"model.can_ignore_user",
"model.can_mute_user"
),
hasGivenFlags: Ember.computed.gt("model.number_of_flags_given", 0),
hasFlaggedPosts: Ember.computed.gt("model.number_of_flagged_posts", 0),
hasDeletedPosts: Ember.computed.gt("model.number_of_deleted_posts", 0),
@ -147,14 +150,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
this.get("adminTools").deleteUser(this.get("model.id"));
},
ignoreUser() {
updateNotificationLevel(level) {
const user = this.get("model");
user.ignore().then(() => user.set("ignored", true));
},
unignoreUser() {
const user = this.get("model");
user.unignore().then(() => user.set("ignored", false));
return user.updateNotificationLevel(level);
}
}
});

View File

@ -9,15 +9,14 @@ const {
runInDebug
} = Ember;
function getCurrentHandlerInfos(router) {
function getCurrentRouteInfos(router) {
let routerLib = router._routerMicrolib || router.router;
return routerLib.currentHandlerInfos;
return routerLib.currentRouteInfos;
}
function getRoutes(router) {
return emberArray(getCurrentHandlerInfos(router))
.mapBy("handler")
return emberArray(getCurrentRouteInfos(router))
.mapBy("_route")
.reverse();
}

View File

@ -0,0 +1,19 @@
// Updates the PWA badging if avaliable
export default {
name: "badging",
after: "message-bus",
initialize(container) {
const appEvents = container.lookup("app-events:main");
const user = container.lookup("current-user:main");
if (!user) return; // must be logged in
if (!window.ExperimentalBadge) return; // must have the Badging API
appEvents.on("notifications:changed", () => {
let notifications =
user.get("unread_notifications") + user.get("unread_private_messages");
window.ExperimentalBadge.set(notifications);
});
}
};

View File

@ -0,0 +1,18 @@
import { showPopover, hidePopover } from "discourse/lib/d-popover";
const SELECTORS =
"[data-html-popover],[data-tooltip],[data-popover],[data-html-tooltip]";
export default {
name: "d-popover",
initialize() {
$("#main").on("click.d-popover mouseenter.d-popover", SELECTORS, event =>
showPopover(event)
);
$("#main").on("mouseleave.d-popover", SELECTORS, event =>
hidePopover(event)
);
}
};

View File

@ -12,10 +12,12 @@ export default {
initialize(container) {
// Tell our AJAX system to track a page transition
const router = container.lookup("router:main");
router.on("willTransition", viewTrackingRequired);
router.on("didTransition", cleanDOM);
router.on("routeWillChange", viewTrackingRequired);
router.on("routeDidChange", cleanDOM);
let appEvents = container.lookup("app-events:main");
startPageTracking(router, appEvents);
// Out of the box, Discourse tries to track google analytics

View File

@ -7,7 +7,7 @@ export default {
// only take care of hiding the footer here
// controllers MUST take care of displaying it
router.on("willTransition", () => {
router.on("routeWillChange", () => {
application.set("showFooter", false);
return true;
});

View File

@ -0,0 +1,18 @@
export default {
name: "title-notifications",
after: "message-bus",
initialize(container) {
const appEvents = container.lookup("app-events:main");
const user = container.lookup("current-user:main");
if (!user) return; // must be logged in
appEvents.on("notifications:changed", () => {
let notifications =
user.get("unread_notifications") + user.get("unread_private_messages");
Discourse.updateNotificationCount(notifications);
});
}
};

View File

@ -1 +1,48 @@
export default Ember.Object.extend(Ember.Evented);
import deprecated from "discourse-common/lib/deprecated";
export default Ember.Object.extend(Ember.Evented, {
_events: {},
on() {
if (arguments.length === 2) {
let [name, fn] = arguments;
let target = {};
this._events[name] = this._events[name] || [];
this._events[name].push({ target, fn });
this._super(name, target, fn);
} else if (arguments.length === 3) {
let [name, target, fn] = arguments;
this._events[name] = this._events[name] || [];
this._events[name].push({ target, fn });
this._super(...arguments);
}
return this;
},
off() {
let name = arguments[0];
let fn = arguments[2];
if (this._events[name]) {
if (arguments.length === 1) {
deprecated(
"Removing all event listeners at once is deprecated, please remove each listener individually."
);
this._events[name].forEach(ref => {
this._super(name, ref.target, ref.fn);
});
delete this._events[name];
} else if (arguments.length === 3) {
this._super(...arguments);
this._events[name] = this._events[name].filter(e => e.fn !== fn);
if (this._events[name].length === 0) delete this._events[name];
}
}
return this;
}
});

View File

@ -22,7 +22,7 @@ function _clean() {
.not(".no-blur")
.blur();
Discourse.set("notifyCount", 0);
Discourse.set("contextCount", 0);
Discourse.__container__.lookup("route:application").send("closeModal");
const hideDropDownFunction = $("html").data("hide-dropdown");
if (hideDropDownFunction) {

View File

@ -0,0 +1 @@
export const PHRASE_MATCH_REGEXP_PATTERN = '<%= Search::PHRASE_MATCH_REGEXP_PATTERN %>';

View File

@ -0,0 +1,173 @@
import { siteDir } from "discourse/lib/text-direction";
const D_POPOVER_ID = "d-popover";
const D_POPOVER_TEMPLATE = `
<div id="${D_POPOVER_ID}" class="is-under">
<div class="d-popover-arrow d-popover-top-arrow"></div>
<div class="d-popover-content">
<div class="spinner small"></div>
</div>
<div class="d-popover-arrow d-popover-bottom-arrow"></div>
</div>
`;
const D_ARROW_HEIGHT = 10;
const D_HORIZONTAL_MARGIN = 5;
export function hidePopover() {
getPopover()
.fadeOut()
.remove();
return getPopover();
}
export function showPopover(event, options = {}) {
const $enteredElement = $(event.currentTarget);
if (isRetina()) {
getPopover().addClass("retina");
}
if (!getPopover().length) {
$("body").append($(D_POPOVER_TEMPLATE));
}
setPopoverHtmlContent($enteredElement, options.htmlContent);
setPopoverTextContent($enteredElement, options.textContent);
getPopover().fadeIn();
positionPopover($enteredElement);
return {
html: content => replaceHtmlContent($enteredElement, content),
text: content => replaceTextContent($enteredElement, content),
hide: hidePopover
};
}
function setPopoverHtmlContent($enteredElement, content) {
content =
content ||
$enteredElement.attr("data-html-popover") ||
$enteredElement.attr("data-html-tooltip");
replaceHtmlContent($enteredElement, content);
}
function setPopoverTextContent($enteredElement, content) {
content =
content ||
$enteredElement.attr("data-popover") ||
$enteredElement.attr("data-tooltip");
replaceTextContent($enteredElement, content);
}
function replaceTextContent($enteredElement, content) {
if (content) {
getPopover()
.find(".d-popover-content")
.text(content);
window.requestAnimationFrame(() => positionPopover($enteredElement));
}
}
function replaceHtmlContent($enteredElement, content) {
if (content) {
getPopover()
.find(".d-popover-content")
.html(content);
window.requestAnimationFrame(() => positionPopover($enteredElement));
}
}
function positionPopover($element) {
const $popover = getPopover();
$popover.removeClass("is-above is-under is-left-aligned is-right-aligned");
const $dHeader = $(".d-header");
const windowRect = {
left: 0,
top: $dHeader.length ? $dHeader[0].getBoundingClientRect().bottom : 0,
width: $(window).width(),
height: $(window).height()
};
const popoverRect = {
width: $popover.width(),
height: $popover.height(),
left: null,
right: null
};
if (popoverRect.width > windowRect.width - D_HORIZONTAL_MARGIN * 2) {
popoverRect.width = windowRect.width - D_HORIZONTAL_MARGIN * 2;
$popover.width(popoverRect.width);
}
const targetRect = $element[0].getBoundingClientRect();
const underSpace = windowRect.height - targetRect.bottom - D_ARROW_HEIGHT;
const topSpace = targetRect.top - windowRect.top - D_ARROW_HEIGHT;
if (
underSpace > popoverRect.height + D_HORIZONTAL_MARGIN ||
underSpace > topSpace
) {
$popover
.css("top", targetRect.bottom + window.pageYOffset + D_ARROW_HEIGHT)
.addClass("is-under");
} else {
$popover
.css(
"top",
targetRect.top +
window.pageYOffset -
popoverRect.height -
D_ARROW_HEIGHT
)
.addClass("is-above");
}
const leftSpace = targetRect.left + targetRect.width / 2;
if (siteDir() === "ltr") {
if (leftSpace > popoverRect.width / 2 + D_HORIZONTAL_MARGIN) {
popoverRect.left = leftSpace - popoverRect.width / 2;
$popover.css("left", popoverRect.left);
} else {
popoverRect.left = D_HORIZONTAL_MARGIN;
$popover.css("left", popoverRect.left).addClass("is-left-aligned");
}
} else {
const rightSpace = windowRect.width - targetRect.right;
if (rightSpace > popoverRect.width / 2 + D_HORIZONTAL_MARGIN) {
popoverRect.left = leftSpace - popoverRect.width / 2;
$popover.css("left", popoverRect.left);
} else {
popoverRect.left =
windowRect.width - popoverRect.width - D_HORIZONTAL_MARGIN * 2;
$popover.css("left", popoverRect.left).addClass("is-right-aligned");
}
}
let arrowPosition;
if (siteDir() === "ltr") {
arrowPosition = Math.abs(targetRect.left - popoverRect.left);
} else {
arrowPosition = targetRect.left - popoverRect.left + targetRect.width / 2;
}
$popover.find(".d-popover-arrow").css("left", arrowPosition);
}
function isRetina() {
return window.devicePixelRatio && window.devicePixelRatio > 1;
}
function getPopover() {
return $(document.getElementById(D_POPOVER_ID));
}

View File

@ -1,3 +1,5 @@
import { defaultHomepage } from "discourse/lib/utilities";
/**
@module Discourse
*/
@ -87,7 +89,10 @@ const DiscourseLocation = Ember.Object.extend({
path = this.formatURL(path);
if (state && state.path !== path) {
this.pushState(path);
const paths = [path, state.path];
if (!(paths.includes("/") && paths.includes(`/${defaultHomepage()}`))) {
this.pushState(path);
}
}
},

View File

@ -1,8 +1,16 @@
import { PHRASE_MATCH_REGEXP_PATTERN } from "discourse/lib/concerns/search-constants";
export const CLASS_NAME = "search-highlight";
export default function($elem, term) {
if (!_.isEmpty(term)) {
// special case ignore "l" which is used for magic sorting
let words = _.reject(term.match(/"[^"]+"|[^\s]+/g), t => t === "l");
let words = _.reject(
term.match(new RegExp(`${PHRASE_MATCH_REGEXP_PATTERN}|[^\\s]+`, "g")),
t => t === "l"
);
words = words.map(w => w.replace(/^"(.*)"$/, "$1"));
$elem.highlight(words, { className: "search-highlight", wordsOnly: true });
$elem.highlight(words, { className: CLASS_NAME, wordsOnly: true });
}
}

View File

@ -140,6 +140,8 @@ export default {
this.sendToSelectedPost("replyToPost");
// lazy but should work for now
Ember.run.later(() => $(".d-editor .quote").click(), 500);
return false;
},
goToFirstSuggestedTopic() {

View File

@ -6,6 +6,7 @@ export default function($elem) {
if (!$elem) {
return;
}
const originalMeta = $("meta[name=viewport]").attr("content");
loadScript("/javascripts/jquery.magnific-popup.min.js").then(function() {
const spoilers = $elem.find(".spoiler a.lightbox, .spoiled a.lightbox");
$elem
@ -23,6 +24,10 @@ export default function($elem) {
callbacks: {
open() {
$("meta[name=viewport]").attr(
"content",
"width=device-width, initial-scale=1.0"
);
const wrap = this.wrap,
img = this.currItem.img,
maxHeight = img.css("max-height");
@ -36,6 +41,7 @@ export default function($elem) {
});
},
beforeClose() {
$("meta[name=viewport]").attr("content", originalMeta);
this.wrap.off("click.pinhandler");
this.wrap.removeClass("mfp-force-scrollbars");
}

View File

@ -15,10 +15,9 @@ export function startPageTracking(router, appEvents) {
if (_started) {
return;
}
router.on("didTransition", function() {
this.send("refreshTitle");
const url = Discourse.getURL(this.get("url"));
router.on("routeDidChange", () => {
router.send("refreshTitle");
const url = Discourse.getURL(router.get("url"));
// Refreshing the title is debounced, so we need to trigger this in the
// next runloop to have the correct title.
@ -39,6 +38,7 @@ export function startPageTracking(router, appEvents) {
}
});
});
_started = true;
}

View File

@ -140,7 +140,7 @@ class PluginApi {
* you can register a renderer that will return an icon in the
* format required.
*
* For example, the follwing resolver will render a smile in the place
* For example, the following resolver will render a smile in the place
* of every icon on Discourse.
*
* api.registerIconRenderer({
@ -170,7 +170,7 @@ class PluginApi {
}
/**
* Replace all ocurrences of one icon with another without having to
* Replace all occurrences of one icon with another without having to
* resort to a custom IconRenderer. If you want to do something more
* complicated than a simple replacement then create a new icon renderer.
*
@ -217,7 +217,7 @@ class PluginApi {
*
* This function can be used to add an icon with a link that will be displayed
* beside a poster's name. The `callback` is called with the post's user custom
* fields and post attrions. An icon will be rendered if the callback returns
* fields and post attributes. An icon will be rendered if the callback returns
* an object with the appropriate attributes.
*
* The returned object can have the following attributes:
@ -473,8 +473,9 @@ class PluginApi {
/**
* Registers a callback that will be invoked when the server calls
* Post#publish_change_to_clients! please ensure your type does not
* match acted,revised,rebaked,recovered, created,move_to_inbox or archived
* Post#publish_change_to_clients! Please ensure your type does not
* match acted, revised, rebaked, recovered, created, move_to_inbox
* or archived
*
* callback will be called with topicController and Message
*
@ -528,7 +529,7 @@ class PluginApi {
/**
* Exposes the widget update ability to plugins. Updates the widget
* registry for the given widget name to include the properties on args
* See `reopenWidget` in `discourse/widgets/widget` from more ifo.
* See `reopenWidget` in `discourse/widgets/widget` from more info.
**/
reopenWidget(name, args) {

View File

@ -1,3 +1,4 @@
import deprecated from "discourse-common/lib/deprecated";
import { escapeExpression } from "discourse/lib/utilities";
const fadeSpeed = 300;
@ -77,12 +78,16 @@ export function hideTooltip() {
}
export function registerTooltip(jqueryContext) {
deprecated("tooltip is getting deprecated. Use d-popover instead");
if (jqueryContext.length) {
jqueryContext.off("click").on("click", event => showTooltip(event));
}
}
export function registerHoverTooltip(jqueryContext) {
deprecated("tooltip is getting deprecated. Use d-popover instead");
if (jqueryContext.length) {
jqueryContext
.off("mouseenter mouseleave click")

View File

@ -22,7 +22,8 @@ const SERVER_SIDE_ONLY = [
/^\/wizard/,
/\.rss$/,
/\.json$/,
/^\/admin\/upgrade$/
/^\/admin\/upgrade$/,
/^\/logs($|\/)/
];
export function rewritePath(path) {
@ -51,6 +52,10 @@ export function userPath(subPath) {
return Discourse.getURL(subPath ? `/u/${subPath}` : "/u");
}
export function groupPath(subPath) {
return Discourse.getURL(subPath ? `/g/${subPath}` : "/g");
}
let _jumpScheduled = false;
export function jumpToElement(elementId) {
if (_jumpScheduled || Ember.isEmpty(elementId)) {

View File

@ -2,7 +2,7 @@ import { propertyEqual, setting } from "discourse/lib/computed";
export default Ember.Mixin.create({
isCurrentUser: propertyEqual("model.id", "currentUser.id"),
showEmailOnProfile: setting("show_email_on_profile"),
showEmailOnProfile: setting("moderators_view_emails"),
canStaffCheckEmails: Ember.computed.and(
"showEmailOnProfile",
"currentUser.staff"

View File

@ -26,8 +26,8 @@ export default Ember.Mixin.create({
username = Ember.Handlebars.Utils.escapeExpression(username.toString());
// Don't show on mobile or nested
if (this.site.mobileView || $target.parents(".card-content").length) {
// Don't show if nested
if ($target.parents(".card-content").length) {
this._close();
DiscourseURL.routeTo($target.attr("href"));
return false;
@ -97,10 +97,6 @@ export default Ember.Mixin.create({
}
this._close();
if (this.site.mobileView) {
return false;
}
}
return true;
@ -122,10 +118,7 @@ export default Ember.Mixin.create({
return this._show($target.text().replace(/^@/, ""), $target);
});
this.appEvents.on(previewClickEvent, $target => {
this.set("isFixed", true);
return this._show($target.text().replace(/^@/, ""), $target);
});
this.appEvents.on(previewClickEvent, this, "_previewClick");
this.appEvents.on(`topic-header:trigger-${id}`, (username, $target) => {
this.setProperties({ isFixed: true, isDocked: true });
@ -133,6 +126,11 @@ export default Ember.Mixin.create({
});
},
_previewClick($target) {
this.set("isFixed", true);
return this._show($target.text().replace(/^@/, ""), $target);
},
_positionCard(target) {
const rtl = $("html").css("direction") === "rtl";
if (!target) {
@ -147,58 +145,67 @@ export default Ember.Mixin.create({
Ember.run.schedule("afterRender", () => {
if (target) {
let position = target.offset();
if (position) {
position.bottom = "unset";
if (!this.site.mobileView) {
let position = target.offset();
if (position) {
position.bottom = "unset";
if (rtl) {
// The site direction is rtl
position.right = $(window).width() - position.left + 10;
position.left = "auto";
let overage = $(window).width() - 50 - (position.right + width);
if (overage < 0) {
position.right += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
} else {
// The site direction is ltr
position.left += target.width() + 10;
let overage = $(window).width() - 50 - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
}
position.top -= $("#main-outlet").offset().top;
if (isFixed) {
position.top -= $("html").scrollTop();
//if content is fixed and will be cut off on the bottom, display it above...
if (
position.top + height + verticalAdjustments >
$(window).height() - 50
) {
position.bottom =
$(window).height() -
(target.offset().top - $("html").scrollTop());
if (verticalAdjustments > 0) {
position.bottom += 48;
if (rtl) {
// The site direction is rtl
position.right = $(window).width() - position.left + 10;
position.left = "auto";
let overage = $(window).width() - 50 - (position.right + width);
if (overage < 0) {
position.right += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
} else {
// The site direction is ltr
position.left += target.width() + 10;
let overage = $(window).width() - 50 - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
position.top = "unset";
}
}
const avatarOverflowSize = 44;
if (isDocked && position.top < avatarOverflowSize) {
position.top = avatarOverflowSize;
}
position.top -= $("#main-outlet").offset().top;
if (isFixed) {
position.top -= $("html").scrollTop();
//if content is fixed and will be cut off on the bottom, display it above...
if (
position.top + height + verticalAdjustments >
$(window).height() - 50
) {
position.bottom =
$(window).height() -
(target.offset().top - $("html").scrollTop());
if (verticalAdjustments > 0) {
position.bottom += 48;
}
position.top = "unset";
}
}
this.$().css(position);
const avatarOverflowSize = 44;
if (isDocked && position.top < avatarOverflowSize) {
position.top = avatarOverflowSize;
}
this.$().css(position);
}
}
if (this.site.mobileView) {
$(".card-cloak").removeClass("hidden");
let position = target.offset();
position.top = "10%"; // match modal behaviour
position.left = 0;
this.$().css(position);
}
this.$().toggleClass("docked-card", isDocked);
// After the card is shown, focus on the first link
@ -214,6 +221,9 @@ export default Ember.Mixin.create({
_hide() {
if (!this.get("visible")) {
this.$().css({ left: -9999, top: -9999 });
if (this.site.mobileView) {
$(".card-cloak").addClass("hidden");
}
}
},
@ -239,7 +249,7 @@ export default Ember.Mixin.create({
$("#main")
.off(clickDataExpand)
.off(clickMention);
this.appEvents.off(previewClickEvent);
this.appEvents.off(previewClickEvent, this, "_previewClick");
},
keyUp(e) {

View File

@ -7,12 +7,12 @@ export default {
didInsertElement() {
this._super(...arguments);
this.appEvents.on("url:refresh", this.refresh);
this.appEvents.on("url:refresh", this, "refresh");
},
willDestroyElement() {
this._super(...arguments);
this.appEvents.off("url:refresh");
this.appEvents.off("url:refresh", this, "refresh");
}
};

View File

@ -129,7 +129,8 @@ const Category = RestModel.extend({
minimum_required_tags: this.get("minimum_required_tags"),
navigate_to_first_post_after_read: this.get(
"navigate_to_first_post_after_read"
)
),
search_priority: this.get("search_priority")
},
type: id ? "PUT" : "POST"
});

View File

@ -1032,7 +1032,7 @@ const Composer = RestModel.extend({
self.set("draftConflictUser", null);
self._clearingStatus = null;
},
1000
Ember.Test ? 0 : 1000
);
}
}.observes("title", "reply")

View File

@ -99,9 +99,7 @@ export function findAll(siteSettings, capabilities, isMobileDevice) {
}
// exclude FA icon for Google, uses custom SVG
methods.forEach(m =>
m.set("hasRegularIcon", m.get("name") === "google_oauth2" ? false : true)
);
methods.forEach(m => m.set("isGoogle", m.get("name") === "google_oauth2"));
return methods;
}

View File

@ -225,6 +225,11 @@ const Topic = RestModel.extend({
categoryClass: fmt("category.fullSlug", "category-%@"),
@computed("tags")
tagClasses(tags) {
return tags && tags.map(t => `tag-${t}`).join(" ");
},
@computed("url")
shareUrl(url) {
const user = Discourse.User.current();

Some files were not shown because too many files have changed in this diff Show More