Version bump
@ -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
|
||||
|
||||
3
Gemfile
@ -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'
|
||||
|
||||
31
Gemfile.lock
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 919 B After Width: | Height: | Size: 882 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1021 B After Width: | Height: | Size: 984 B |
|
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 895 B |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
@ -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")) {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -87,8 +87,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
);
|
||||
},
|
||||
|
||||
showEmails: function() {
|
||||
this.set("showEmails", true);
|
||||
toggleEmailVisibility: function() {
|
||||
this.toggleProperty("showEmails");
|
||||
this._refreshUsers();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -19,6 +19,7 @@ export default Discourse.Route.extend({
|
||||
controller.setProperties({
|
||||
originalPrimaryGroupId: model.get("primary_group_id"),
|
||||
availableGroups: this._availableGroups,
|
||||
customGroupIdsBuffer: null,
|
||||
model
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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(" ")
|
||||
);
|
||||
},
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const searchPriorities = <%= Searchable::PRIORITIES.to_json %>;
|
||||
@ -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;
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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: [] });
|
||||
|
||||
@ -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"]);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
19
app/assets/javascripts/discourse/initializers/badging.js.es6
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const PHRASE_MATCH_REGEXP_PATTERN = '<%= Search::PHRASE_MATCH_REGEXP_PATTERN %>';
|
||||
173
app/assets/javascripts/discourse/lib/d-popover.js.es6
Normal 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));
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
@ -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"
|
||||
});
|
||||
|
||||
@ -1032,7 +1032,7 @@ const Composer = RestModel.extend({
|
||||
self.set("draftConflictUser", null);
|
||||
self._clearingStatus = null;
|
||||
},
|
||||
1000
|
||||
Ember.Test ? 0 : 1000
|
||||
);
|
||||
}
|
||||
}.observes("title", "reply")
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||