Version bump

This commit is contained in:
Neil Lalonde 2019-07-15 10:16:48 -04:00
commit a3c836541c
926 changed files with 19580 additions and 6726 deletions

View File

@ -1,45 +0,0 @@
# Use this file to configure the Overcommit hooks you wish to use. This will
# extend the default configuration defined in:
# https://github.com/brigade/overcommit/blob/master/config/default.yml
#
# At the topmost level of this YAML file is a key representing type of hook
# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
# customize each hook, such as whether to only run it on certain files (via
# `include`), whether to only display output if it fails (via `quiet`), etc.
#
# For a complete list of hooks, see:
# https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook
#
# For a complete list of options that you can use to customize hooks, see:
# https://github.com/brigade/overcommit#configuration
PreCommit:
RuboCop:
enabled: true
command: ['bundle', 'exec', 'rubocop']
EsLint:
enabled: true
required_executable: './node_modules/.bin/eslint'
install_command: 'yarn install'
command: ['yarn', 'eslint', '--ext', '.es6', '-f', 'compact']
include: '**/*.es6'
YamlSyntax:
enabled: true
PostCheckout:
BundleInstall:
enabled: true
YarnInstall:
enabled: true
PostMerge:
BundleInstall:
enabled: true
YarnInstall:
enabled: true
PostRewrite:
BundleInstall:
enabled: true
YarnInstall:
enabled: true

View File

@ -74,13 +74,7 @@ script:
- |
bash -c "
if [ '$RUN_LINT' == '1' ]; then
bundle exec rubocop --parallel && \
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"
yarn eslint --ext .es6 app/assets/javascripts && \
yarn eslint --ext .es6 test/javascripts && \
yarn eslint --ext .es6 plugins/**/assets/javascripts && \
yarn eslint --ext .es6 plugins/**/test/javascripts && \
yarn eslint app/assets/javascripts test/javascripts
npx lefthook run lints
else
if [ '$QUNIT_RUN' == '1' ]; then
bundle exec rake qunit:test['1200000'] && \

View File

@ -46,7 +46,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox', '1.8.92'
gem 'onebox', '1.9.2'
gem 'http_accept_language', '~>2.0.5', require: false
@ -152,6 +152,7 @@ group :development do
gem 'bullet', require: !!ENV['BULLET']
gem 'better_errors'
gem 'binding_of_caller'
gem 'yaml-lint'
# waiting on 2.7.5 per: https://github.com/ctran/annotate_models/pull/595
if rails_master?

View File

@ -184,7 +184,7 @@ GEM
mini_portile2 (2.4.0)
mini_racer (0.2.6)
libv8 (>= 6.9.411)
mini_scheduler (0.9.2)
mini_scheduler (0.11.0)
sidekiq
mini_sql (0.2.2)
mini_suffix (0.3.0)
@ -238,7 +238,7 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.92)
onebox (1.9.2)
htmlentities (~> 4.3)
moneta (~> 1.0)
multi_json (~> 1.11)
@ -412,6 +412,7 @@ GEM
webpush (0.3.8)
hkdf (~> 0.2)
jwt (~> 2.0)
yaml-lint (0.0.10)
PLATFORMS
ruby
@ -486,7 +487,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox (= 1.8.92)
onebox (= 1.9.2)
openid-redis-store
parallel_tests
pg
@ -533,6 +534,7 @@ DEPENDENCIES
unicorn
webmock
webpush
yaml-lint
BUNDLED WITH
1.17.3

View File

@ -17,8 +17,9 @@ export default Ember.Component.extend({
@observes("content")
contentChanged() {
if (this._editor && !this._skipContentChangeEvent && this.content) {
this._editor.getSession().setValue(this.content);
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
},

View File

@ -55,7 +55,6 @@ export default Ember.Component.extend({
showTitle: true,
showFilteringUI: false,
showDatesOptions: Ember.computed.alias("model.dates_filtering"),
showExport: Ember.computed.not("model.onlyTable"),
showRefresh: Ember.computed.or(
"showDatesOptions",
"model.available_filters.length"
@ -165,8 +164,8 @@ export default Ember.Component.extend({
let reportKey = "reports:";
reportKey += [
dataSourceName,
startDate.replace(/-/g, ""),
endDate.replace(/-/g, ""),
Ember.testing ? "start" : startDate.replace(/-/g, ""),
Ember.testing ? "end" : endDate.replace(/-/g, ""),
"[:prev_period]",
this.get("reportOptions.table.limit"),
customFilters

View File

@ -1,12 +1,17 @@
import BufferedContent from "discourse/mixins/buffered-content";
import SettingComponent from "admin/mixins/setting-component";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Ember.Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
_save() {
return this.model.saveSettings(
this.get("setting.setting"),
this.get("buffered.value")
);
return ajax(`/admin/themes/${this.model.id}/setting`, {
type: "PUT",
data: {
name: this.setting.setting,
value: this.get("buffered.value")
}
}).catch(popupAjaxError);
}
});

View File

@ -2,6 +2,8 @@ import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
import { iconHTML } from "discourse-common/lib/icon-library";
import { escape } from "pretty-text/sanitizer";
const MAX_COMPONENTS = 4;
@ -64,7 +66,10 @@ export default Ember.Component.extend({
children = this.childrenExpanded
? children
: children.slice(0, MAX_COMPONENTS);
return children.map(t => t.get("name"));
return children.map(t => {
const name = escape(t.name);
return t.enabled ? name : `${iconHTML("ban")} ${name}`;
});
},
@computed("children")

View File

@ -5,6 +5,13 @@ import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(bufferedProperty("emailTemplate"), {
saved: false,
@computed("buffered.body", "buffered.subject")
saveDisabled(body, subject) {
return (
this.emailTemplate.body === body && this.emailTemplate.subject === subject
);
},
@computed("buffered")
hasMultipleSubjects(buffered) {
if (buffered.getProperties("subject")["subject"]) {

View File

@ -301,6 +301,20 @@ export default Ember.Controller.extend({
} else {
this.commitSwitchType();
}
},
enableComponent() {
this.model.set("enabled", true);
this.model
.saveChanges("enabled")
.catch(() => this.model.set("enabled", false));
},
disableComponent() {
this.model.set("enabled", false);
this.model
.saveChanges("enabled")
.catch(() => this.model.set("enabled", true));
}
}
});

View File

@ -1,9 +1,15 @@
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(bufferedProperty("siteText"), {
saved: false,
@computed("buffered.value")
saveDisabled(value) {
return this.siteText.value === value;
},
actions: {
saveChanges() {
const buffered = this.buffered;

View File

@ -15,32 +15,64 @@ export default Ember.Controller.extend(CanCheckEmails, {
selectAll: false,
searchHint: i18n("search_hint"),
init() {
this._super(...arguments);
this._page = 1;
this._results = [];
this._canLoadMore = true;
},
@computed("query")
title(query) {
return I18n.t("admin.users.titles." + query);
},
_filterUsers: debounce(function() {
this._refreshUsers();
this.resetFilters();
}, 250).observes("listFilter"),
resetFilters() {
this._page = 1;
this._results = [];
this._canLoadMore = true;
this._refreshUsers();
},
_refreshUsers() {
if (!this._canLoadMore) {
return;
}
this.set("refreshing", true);
AdminUser.findAll(this.query, {
filter: this.listFilter,
show_emails: this.showEmails,
order: this.order,
ascending: this.ascending
ascending: this.ascending,
page: this._page
})
.then(result => this.set("model", result))
.then(result => {
if (!result || result.length === 0) {
this._canLoadMore = false;
}
this._results = this._results.concat(result);
this.set("model", this._results);
})
.finally(() => this.set("refreshing", false));
},
actions: {
loadMore() {
this._page += 1;
this._refreshUsers();
},
toggleEmailVisibility() {
this.toggleProperty("showEmails");
this._refreshUsers();
this.resetFilters();
}
}
});

View File

@ -17,6 +17,8 @@ const CUSTOM_TYPES = [
"group_list"
];
const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"];
export default Ember.Mixin.create({
classNameBindings: [":row", ":setting", "overridden", "typeClass"],
content: Ember.computed.alias("setting"),
@ -113,7 +115,9 @@ export default Ember.Mixin.create({
.then(() => {
this.set("validationMessage", null);
this.commitBuffer();
this.afterSave();
if (AUTO_REFRESH_ON_SAVE.includes(this.get("setting.setting"))) {
this.afterSave();
}
})
.catch(e => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {

View File

@ -16,8 +16,8 @@ const Report = Discourse.Model.extend({
higher_is_better: true,
@computed("modes")
onlyTable(modes) {
return modes.length === 1 && modes[0] === "table";
isTable(modes) {
return modes.some(mode => mode === "table");
},
@computed("type", "start_date", "end_date")
@ -332,9 +332,7 @@ const Report = Discourse.Model.extend({
ignoreTitle: true
});
return `<a href='${href}'>${avatarImg}<span class='username'>${
user.name
}</span></a>`;
return `<a href='${href}'>${avatarImg}<span class='username'>${user.name}</span></a>`;
};
return {

View File

@ -21,7 +21,7 @@ export default Discourse.Route.extend({
refreshing: false
});
controller._refreshUsers();
controller.resetFilters();
}
}
}

View File

@ -164,7 +164,7 @@
</div>
{{/each}}
{{#if showExport}}
{{#if model.isTable}}
<div class="control">
<div class="input">
{{d-button

View File

@ -17,6 +17,9 @@
{{#if theme.isBroken}}
{{d-icon "exclamation-circle" class="broken-indicator" title="admin.customize.theme.broken_theme_tooltip"}}
{{/if}}
{{#unless theme.enabled}}
{{d-icon "ban" class="light-grey-icon" title="admin.customize.theme.disabled_component_tooltip"}}
{{/unless}}
{{else}}
{{d-icon "caret-right"}}
{{/unless}}
@ -25,7 +28,7 @@
{{#if displayComponents}}
<div class="components-list">
<span class="components">{{childrenString}}</span>
<span class="components">{{{childrenString}}}</span>
{{#if displayHasMore}}
<span {{action "toggleChildrenExpanded"}} class="others-count">

View File

@ -10,7 +10,7 @@
<label>{{i18n "admin.customize.email_templates.body"}}</label>
{{d-editor value=buffered.body}}
{{#save-controls model=emailTemplate action=(action "saveChanges") saved=saved}}
{{#save-controls model=emailTemplate action=(action "saveChanges") saved=saved saveDisabled=saveDisabled}}
{{#if emailTemplate.can_revert}}
{{d-button action=(action "revertChanges") label="admin.customize.email_templates.revert"}}
{{/if}}

View File

@ -16,7 +16,7 @@
</div>
{{/each}}
{{#unless model.enabled}}
{{#unless model.supported}}
<div class="alert alert-error">
{{i18n "admin.customize.theme.required_version.error"}}
{{#if model.remote_theme.minimum_discourse_version}}
@ -28,6 +28,26 @@
</div>
{{/unless}}
{{#unless model.enabled}}
<div class="alert alert-error">
{{#if model.disabled_by}}
{{i18n "admin.customize.theme.disabled_by"}}
{{#user-link user=model.disabled_by}}
{{avatar model.disabled_by imageSize="tiny"}}
{{model.disabled_by.username}}
{{/user-link}}
{{format-date model.disabled_at leaveAgo="true"}}
{{else}}
{{i18n "admin.customize.theme.disabled"}}
{{/if}}
{{d-button
class='btn-default'
action=(action "enableComponent")
icon="check"
label="admin.customize.theme.enable"}}
</div>
{{/unless}}
{{#unless model.component}}
<div class="control-unit">
{{inline-edit-checkbox action=(action "applyDefault") labelKey="admin.customize.theme.is_default" checked=model.default}}
@ -204,7 +224,13 @@
{{#if model.childThemes.length}}
<ul class='removable-list'>
{{#each model.childThemes as |child|}}
<li>{{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action=(action "removeChildTheme") actionParam=child class="btn-default cancel-edit col" icon="times"}}</li>
<li class={{unless child.enabled "disabled-child"}}>
{{#link-to 'adminCustomizeThemes.show' child replace=true class='col child-link'}}
{{child.name}}
{{/link-to}}
{{d-button action=(action "removeChildTheme") actionParam=child class="btn-default cancel-edit col" icon="times"}}
</li>
{{/each}}
</ul>
{{/if}}
@ -221,5 +247,22 @@
<a class="btn btn-default export" target="_blank" href={{downloadUrl}}>{{d-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
{{d-button action=(action "switchType") label="admin.customize.theme.convert" icon=convertIcon class="btn-default btn-normal" title=convertTooltip}}
{{#if model.component}}
{{#if model.enabled}}
{{d-button
class='btn-default'
action=(action "disableComponent")
icon="ban"
label="admin.customize.theme.disable"}}
{{else}}
{{d-button
class='btn-default'
action=(action "enableComponent")
icon="check"
label="admin.customize.theme.enable"}}
{{/if}}
{{/if}}
{{d-button action=(action "destroy") label="admin.customize.delete" icon="trash-alt" class="btn-danger"}}
</div>

View File

@ -6,7 +6,7 @@
{{expanding-text-area value=buffered.value rows="1" class="site-text-value"}}
{{#save-controls model=siteText action=(action "saveChanges") saved=saved}}
{{#save-controls model=siteText action=(action "saveChanges") saved=saved saveDisabled=saveDisabled}}
{{#if siteText.can_revert}}
{{d-button action=(action "revertChanges") label="admin.site_text.revert" class="revert-site-text"}}
{{/if}}

View File

@ -367,7 +367,11 @@
<div class="display-row">
<div class="field">{{i18n "trust_level"}}</div>
<div class="value">
{{combo-box content=site.trustLevels value=model.trust_level nameProperty="detailedName"}}
{{combo-box
content=site.trustLevels
value=model.trustLevel.id
nameProperty="detailedName"}}
{{#if model.dirty}}
<div>
{{d-button class="ok no-text" action=(action "saveTrustLevel") icon="check"}}

View File

@ -13,7 +13,7 @@
</div>
{{#conditional-loading-spinner condition=refreshing}}
{{#load-more selector=".users-list tr" action=(action "loadMore")}}
{{#if model}}
<table class='table users-list grid'>
<thead>
@ -90,8 +90,9 @@
{{/each}}
</tbody>
</table>
{{conditional-loading-spinner condition=refreshing}}
{{else}}
<p>{{i18n 'search.no_results'}}</p>
{{/if}}
{{/conditional-loading-spinner}}
{{/load-more}}

View File

@ -10,9 +10,9 @@
{{nav-item route='adminUsersList.show' routeParam='staged' label='admin.users.nav.staged'}}
{{nav-item route='groups' label='groups.index.title'}}
<div class="admin-actions">
{{#unless siteSettings.enable_sso}}
{{#if currentUser.can_invite_to_forum}}
{{d-button class="btn-default" action=(route-action "sendInvites") title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}}
{{/unless}}
{{/if}}
{{#if currentUser.admin}}
{{d-button class="btn-default" action=(route-action "exportUsers") title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}}
{{/if}}

View File

@ -72,9 +72,7 @@ export default Ember.Component.extend({
},
_formatReplyToUserPost(avatar, link) {
const htmlLink = `<a class="user-link" href="${link.href}">${
link.anchor
}</a>`;
const htmlLink = `<a class="user-link" href="${link.href}">${link.anchor}</a>`;
return `${avatar}${htmlLink}`.htmlSafe();
}
});

View File

@ -541,7 +541,7 @@ export default Ember.Component.extend({
}
textarea.selectionStart = from;
textarea.selectionEnd = from + length;
Ember.run.next(() => $textarea.trigger("change"));
$textarea.scrollTop(oldScrollPos);
});
},
@ -601,7 +601,13 @@ export default Ember.Component.extend({
this.set("value", `${pre}${hval}${example}${tail}${post}`);
this._selectText(pre.length + hlen, example.length);
} else if (opts && !opts.multiline) {
const [hval, hlen] = getHead(head);
let [hval, hlen] = getHead(head);
if (opts.useBlockMode && sel.value.split("\n").length > 1) {
hval += "\n";
hlen += 1;
tail = `\n${tail}`;
}
if (pre.slice(-hlen) === hval && post.slice(0, tail.length) === tail) {
this.set(

View File

@ -5,6 +5,8 @@ import {
on
} from "ember-addons/ember-computed-decorators";
const DATE_FORMAT = "YYYY-MM-DD";
export default Ember.Component.extend({
classNames: ["date-picker-wrapper"],
_picker: null,
@ -17,11 +19,10 @@ export default Ember.Component.extend({
@on("didInsertElement")
_loadDatePicker() {
const container = this.element.querySelector(`#${this.containerId}`);
if (this.site.mobileView) {
this._loadNativePicker(container);
this._loadNativePicker();
} else {
const container = document.getElementById(this.containerId);
this._loadPikadayPicker(container);
}
},
@ -29,11 +30,11 @@ export default Ember.Component.extend({
_loadPikadayPicker(container) {
loadScript("/javascripts/pikaday.js").then(() => {
Ember.run.next(() => {
const default_opts = {
const options = {
field: this.element.querySelector(".date-picker"),
container: container || this.element,
container: container || null,
bound: container === null,
format: "YYYY-MM-DD",
format: DATE_FORMAT,
firstDay: 1,
i18n: {
previousMonth: I18n.t("dates.previous_month"),
@ -45,14 +46,13 @@ export default Ember.Component.extend({
onSelect: date => this._handleSelection(date)
};
this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
this._picker = new Pikaday(Object.assign(options, this._opts()));
});
});
},
_loadNativePicker(container) {
const wrapper = container || this.element;
const picker = wrapper.querySelector("input.date-picker");
_loadNativePicker() {
const picker = this.element.querySelector("input.date-picker");
picker.onchange = () => this._handleSelection(picker.value);
picker.hide = () => {
/* do nothing for native */
@ -64,12 +64,10 @@ export default Ember.Component.extend({
},
_handleSelection(value) {
const formattedDate = moment(value).format("YYYY-MM-DD");
const formattedDate = moment(value).format(DATE_FORMAT);
if (!this.element || this.isDestroying || this.isDestroyed) return;
this._picker && this._picker.hide();
if (this.onSelect) {
this.onSelect(formattedDate);
}
@ -79,8 +77,8 @@ export default Ember.Component.extend({
_destroy() {
if (this._picker) {
this._picker.destroy();
this._picker = null;
}
this._picker = null;
},
@computed()

View File

@ -565,7 +565,7 @@ export default Ember.Component.extend({
} else {
const previewInputOffset = $(".d-editor-input").offset();
const pickerHeight = $(".emoji-picker").height();
const pickerHeight = $(".d-editor .emoji-picker").height();
const editorHeight = $(".d-editor-input").height();
const windowBottom = $(window).scrollTop() + $(window).height();

View File

@ -13,19 +13,25 @@ export default Ember.Component.extend({
},
{
name: I18n.t(
"admin.groups.manage.interaction.visibility_levels.members"
"admin.groups.manage.interaction.visibility_levels.logged_on_users"
),
value: 1
},
{
name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"),
name: I18n.t(
"admin.groups.manage.interaction.visibility_levels.members"
),
value: 2
},
{
name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"),
value: 3
},
{
name: I18n.t(
"admin.groups.manage.interaction.visibility_levels.owners"
),
value: 3
value: 4
}
];
@ -34,6 +40,7 @@ export default Ember.Component.extend({
{ name: I18n.t("groups.alias_levels.only_admins"), value: 1 },
{ name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 },
{ name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 },
{ name: I18n.t("groups.alias_levels.owners_mods_and_admins"), value: 4 },
{ name: I18n.t("groups.alias_levels.everyone"), value: 99 }
];
},

View File

@ -9,7 +9,9 @@ export default Ember.Component.extend({
relatedTitle(topic) {
const href = this.currentUser && this.currentUser.pmPath(topic);
return href
? `<a href="${href}">${iconHTML("envelope", {
? `<a href="${href}" aria-label="${I18n.t(
"user.messages.inbox"
)}">${iconHTML("envelope", {
class: "private-message-glyph"
})}</a><span>${I18n.t("related_messages.title")}</span>`
: I18n.t("related_messages.title");

View File

@ -10,7 +10,9 @@ export default Ember.Component.extend({
suggestedTitle(topic) {
const href = this.currentUser && this.currentUser.pmPath(topic);
return topic.get("isPrivateMessage") && href
? `<a href="${href}">${iconHTML("envelope", {
? `<a href="${href}" aria-label="${I18n.t(
"user.messages.inbox"
)}>${iconHTML("envelope", {
class: "private-message-glyph"
})}</a><span>${I18n.t("suggested_topics.pm_title")}</span>`
: I18n.t("suggested_topics.title");

View File

@ -0,0 +1,17 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
classNames: ["topic-notice"],
@computed("model.group.{full_name,name,allow_membership_requests}")
accessViaGroupText(group) {
const name = group.full_name || group.name;
const suffix = group.allow_membership_requests ? "request" : "join";
return I18n.t(`topic.group_${suffix}`, { name });
},
@computed("model.group.allow_membership_requests")
accessViaGroupButtonText(allowRequest) {
return `groups.${allowRequest ? "request" : "join"}`;
}
});

View File

@ -1,6 +1,8 @@
import { iconHTML } from "discourse-common/lib/icon-library";
import { bufferedRender } from "discourse-common/lib/buffered-render";
import Category from "discourse/models/category";
import computed from "ember-addons/ember-computed-decorators";
import { REMINDER_TYPE } from "discourse/controllers/edit-topic-timer";
export default Ember.Component.extend(
bufferedRender({
@ -16,6 +18,12 @@ export default Ember.Component.extend(
"categoryId"
],
@computed("statusType")
canRemoveTimer(type) {
if (type === REMINDER_TYPE) return true;
return this.currentUser && this.currentUser.get("canManageTopic");
},
buildBuffer(buffer) {
if (!this.executeAt) return;
@ -40,7 +48,7 @@ export default Ember.Component.extend(
}
let autoCloseHours = this.duration || 0;
buffer.push(`<h3>${iconHTML("far-clock")} `);
buffer.push(`<h3 class="topic-timer-heading">`);
let options = {
timeLeft: duration.humanize(true),
@ -61,11 +69,17 @@ export default Ember.Component.extend(
}
buffer.push(
`<span title="${moment(this.executeAt).format("LLLL")}">${I18n.t(
this._noticeKey(),
options
)}</span>`
`<span title="${moment(this.executeAt).format("LLLL")}">${iconHTML(
"far-clock"
)} ${I18n.t(this._noticeKey(), options)}</span>`
);
if (this.removeTopicTimer && this.canRemoveTimer) {
buffer.push(
`<button class="btn topic-timer-remove no-text" title="${I18n.t(
"post.controls.remove_timer"
)}">${iconHTML("trash-alt")}</button>`
);
}
buffer.push("</h3>");
// TODO Sam: concerned this can cause a heavy rerender loop
@ -79,7 +93,21 @@ export default Ember.Component.extend(
}
},
didInsertElement() {
this._super(...arguments);
if (this.removeTopicTimer) {
$(this.element).on(
"click.topic-timer-remove",
"button",
this.removeTopicTimer
);
}
},
willDestroyElement() {
$(this.element).off("click.topic-timer-remove", this.removeTopicTimer);
if (this._delayedRerender) {
Ember.run.cancel(this._delayedRerender);
}

View File

@ -1,5 +1,6 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import computed from "ember-addons/ember-computed-decorators";
import { cookAsync } from "discourse/lib/text";
export default Ember.Controller.extend(ModalFunctionality, {
post: null,
@ -42,10 +43,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
post
.updatePostField("notice", notice)
.then(() => {
.then(() => cookAsync(notice, { features: { onebox: false } }))
.then(cookedNotice => {
post.setProperties({
notice_type: "custom",
notice_args: notice
notice_args: cookedNotice.string
});
resolve();
this.send("closeModal");

View File

@ -507,7 +507,9 @@ export default Ember.Controller.extend({
},
cancel() {
this.cancelComposer();
const differentDraftContext =
this.get("topic.id") !== this.get("model.topic.id");
this.cancelComposer(differentDraftContext);
},
save() {
@ -830,7 +832,12 @@ export default Ember.Controller.extend({
}
// If it's a different draft, cancel it and try opening again.
return this.cancelComposer()
const differentDraftContext =
opts.post && composerModel.topic
? composerModel.topic.id !== opts.post.topic_id
: true;
return this.cancelComposer(differentDraftContext)
.then(() => this.open(opts))
.then(resolve, reject);
}
@ -984,11 +991,23 @@ export default Ember.Controller.extend({
}
},
cancelComposer() {
cancelComposer(differentDraft = false) {
return new Ember.RSVP.Promise(resolve => {
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
bootbox.dialog(I18n.t("post.abandon.confirm"), [
{ label: I18n.t("post.abandon.no_value") },
{
label: differentDraft
? I18n.t("post.abandon.no_save_draft")
: I18n.t("post.abandon.no_value"),
callback: () => {
// cancel composer without destroying draft on new draft context
if (differentDraft) {
this.model.clearState();
this.close();
resolve();
}
}
},
{
label: I18n.t("post.abandon.yes_value"),
class: "btn-danger",

View File

@ -77,6 +77,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
keys2: [SHIFT, "k"],
keysDelimiter: PLUS,
shortcutsDelimiter: "slash"
}),
go_to_unread_post: buildShortcut("navigation.go_to_unread_post", {
keys1: [SHIFT, "l"],
keysDelimiter: PLUS
})
},
application: {
@ -158,6 +162,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
print: buildShortcut("actions.print", {
keys1: [CTRL, "p"],
keysDelimiter: PLUS
}),
defer: buildShortcut("actions.defer", {
keys1: [SHIFT, "u"],
keysDelimiter: PLUS
})
}
}

View File

@ -1,37 +1,28 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import CanCheckEmails from "discourse/mixins/can-check-emails";
import { default as DiscourseURL, userPath } from "discourse/lib/url";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { findAll } from "discourse/models/login-method";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
import showModal from "discourse/lib/show-modal";
export default Ember.Controller.extend({
export default Ember.Controller.extend(CanCheckEmails, {
loading: false,
dirty: false,
resetPasswordLoading: false,
resetPasswordProgress: "",
password: null,
secondFactorImage: null,
secondFactorKey: null,
showSecondFactorKey: false,
errorMessage: null,
newUsername: null,
backupEnabled: Ember.computed.alias("model.second_factor_backup_enabled"),
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
totps: null,
loaded: Ember.computed.and("secondFactorImage", "secondFactorKey"),
@computed("loading")
submitButtonText(loading) {
return loading ? "loading" : "continue";
},
@computed("loading")
enableButtonText(loading) {
return loading ? "loading" : "enable";
},
@computed("loading")
disableButtonText(loading) {
return loading ? "loading" : "disable";
init() {
this._super(...arguments);
this.set("totps", []);
},
@computed
@ -41,58 +32,64 @@ export default Ember.Controller.extend({
@computed("currentUser")
showEnforcedNotice(user) {
return user && user.get("enforcedSecondFactor");
return user && user.enforcedSecondFactor;
},
toggleSecondFactor(enable) {
if (!this.secondFactorToken) return;
handleError(error) {
if (error.jqXHR) {
error = error.jqXHR;
}
let parsedJSON = error.responseJSON;
if (parsedJSON.error_type === "invalid_access") {
const usernameLower = this.model.username.toLowerCase();
DiscourseURL.redirectTo(
userPath(`${usernameLower}/preferences/second-factor`)
);
} else {
popupAjaxError(error);
}
},
loadSecondFactors() {
if (this.dirty === false) {
return;
}
this.set("loading", true);
this.model
.toggleSecondFactor(
this.secondFactorToken,
this.secondFactorMethod,
SECOND_FACTOR_METHODS.TOTP,
enable
)
.loadSecondFactorCodes(this.password)
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.set("errorMessage", null);
DiscourseURL.redirectTo(
userPath(`${this.model.username.toLowerCase()}/preferences`)
this.setProperties({
errorMessage: null,
loaded: true,
totps: response.totps,
password: null,
dirty: false
});
this.set(
"model.second_factor_enabled",
response.totps && response.totps.length > 0
);
})
.catch(error => {
popupAjaxError(error);
})
.catch(e => this.handleError(e))
.finally(() => this.set("loading", false));
},
markDirty() {
this.set("dirty", true);
},
actions: {
confirmPassword() {
if (!this.password) return;
this.set("loading", true);
this.model
.loadSecondFactorCodes(this.password)
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.setProperties({
errorMessage: null,
secondFactorKey: response.key,
secondFactorImage: response.qr
});
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
this.markDirty();
this.loadSecondFactors();
this.set("password", null);
},
resetPassword() {
@ -113,16 +110,66 @@ export default Ember.Controller.extend({
.finally(() => this.set("resetPasswordLoading", false));
},
showSecondFactorKey() {
this.set("showSecondFactorKey", true);
disableAllSecondFactors() {
if (this.loading) {
return;
}
bootbox.confirm(
I18n.t("user.second_factor.disable_confirm"),
I18n.t("cancel"),
I18n.t("user.second_factor.disable"),
result => {
if (result) {
this.model
.disableAllSecondFactors()
.then(() => {
const usernameLower = this.model.username.toLowerCase();
DiscourseURL.redirectTo(
userPath(`${usernameLower}/preferences`)
);
})
.catch(e => this.handleError(e))
.finally(() => this.set("loading", false));
}
}
);
},
enableSecondFactor() {
this.toggleSecondFactor(true);
createTotp() {
const controller = showModal("second-factor-add-totp", {
model: this.model,
title: "user.second_factor.totp.add"
});
controller.setProperties({
onClose: () => this.loadSecondFactors(),
markDirty: () => this.markDirty(),
onError: e => this.handleError(e)
});
},
disableSecondFactor() {
this.toggleSecondFactor(false);
editSecondFactor(second_factor) {
const controller = showModal("second-factor-edit", {
model: second_factor,
title: "user.second_factor.edit_title"
});
controller.setProperties({
user: this.model,
onClose: () => this.loadSecondFactors(),
markDirty: () => this.markDirty(),
onError: e => this.handleError(e)
});
},
editSecondFactorBackup() {
const controller = showModal("second-factor-backup-edit", {
model: this.model,
title: "user.second_factor_backup.title"
});
controller.setProperties({
onClose: () => this.loadSecondFactors(),
markDirty: () => this.markDirty(),
onError: e => this.handleError(e)
});
}
}
});

View File

@ -0,0 +1,67 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Ember.Controller.extend(ModalFunctionality, {
loading: false,
secondFactorImage: null,
secondFactorKey: null,
showSecondFactorKey: false,
errorMessage: null,
onShow() {
this.setProperties({
errorMessage: null,
secondFactorKey: null,
secondFactorToken: null,
showSecondFactorKey: false,
secondFactorImage: null,
loading: true
});
this.model
.createSecondFactorTotp()
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.setProperties({
errorMessage: null,
secondFactorKey: response.key,
secondFactorImage: response.qr
});
})
.catch(error => {
this.send("closeModal");
this.onError(error);
})
.finally(() => this.set("loading", false));
},
actions: {
showSecondFactorKey() {
this.set("showSecondFactorKey", true);
},
enableSecondFactor() {
if (!this.secondFactorToken) return;
this.set("loading", true);
this.model
.enableSecondFactorTotp(
this.secondFactorToken,
I18n.t("user.second_factor.totp.default_name")
)
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.markDirty();
this.set("errorMessage", null);
this.send("closeModal");
})
.catch(error => this.onError(error))
.finally(() => this.set("loading", false));
}
}
});

View File

@ -1,9 +1,8 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import { default as DiscourseURL, userPath } from "discourse/lib/url";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Ember.Controller.extend({
export default Ember.Controller.extend(ModalFunctionality, {
loading: false,
errorMessage: null,
successMessage: null,
@ -14,25 +13,6 @@ export default Ember.Controller.extend({
backupCodes: null,
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
@computed("secondFactorToken", "secondFactorMethod")
isValidSecondFactorToken(secondFactorToken, secondFactorMethod) {
if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) {
return secondFactorToken && secondFactorToken.length === 6;
} else if (secondFactorMethod === SECOND_FACTOR_METHODS.BACKUP_CODE) {
return secondFactorToken && secondFactorToken.length === 16;
}
},
@computed("isValidSecondFactorToken", "backupEnabled", "loading")
isDisabledGenerateBackupCodeBtn(isValid, backupEnabled, loading) {
return !isValid || loading;
},
@computed("isValidSecondFactorToken", "backupEnabled", "loading")
isDisabledDisableBackupCodeBtn(isValid, backupEnabled, loading) {
return !isValid || !backupEnabled || loading;
},
@computed("backupEnabled")
generateBackupCodeBtnLabel(backupEnabled) {
return backupEnabled
@ -40,6 +20,15 @@ export default Ember.Controller.extend({
: "user.second_factor_backup.enable";
},
onShow() {
this.setProperties({
loading: false,
errorMessage: null,
successMessage: null,
backupCodes: null
});
},
actions: {
copyBackupCode(successful) {
if (successful) {
@ -59,18 +48,10 @@ export default Ember.Controller.extend({
disableSecondFactorBackup() {
this.set("backupCodes", []);
if (!this.secondFactorToken) return;
this.set("loading", true);
this.model
.toggleSecondFactor(
this.secondFactorToken,
this.secondFactorMethod,
SECOND_FACTOR_METHODS.BACKUP_CODE,
false
)
.updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);
@ -78,28 +59,28 @@ export default Ember.Controller.extend({
}
this.set("errorMessage", null);
const usernameLower = this.model.username.toLowerCase();
DiscourseURL.redirectTo(userPath(`${usernameLower}/preferences`));
this.model.set("second_factor_backup_enabled", false);
this.markDirty();
this.send("closeModal");
})
.catch(error => {
this.send("closeModal");
this.onError(error);
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
generateSecondFactorCodes() {
if (!this.secondFactorToken) return;
this.set("loading", true);
this.model
.generateSecondFactorCodes(
this.secondFactorToken,
this.secondFactorMethod
)
.generateSecondFactorCodes()
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.markDirty();
this.setProperties({
errorMessage: null,
backupCodes: response.backup_codes,
@ -107,11 +88,13 @@ export default Ember.Controller.extend({
remainingCodes: response.backup_codes.length
});
})
.catch(popupAjaxError)
.catch(error => {
this.send("closeModal");
this.onError(error);
})
.finally(() => {
this.setProperties({
loading: false,
secondFactorToken: null
loading: false
});
});
}

View File

@ -0,0 +1,53 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Ember.Controller.extend(ModalFunctionality, {
actions: {
disableSecondFactor() {
this.user
.updateSecondFactor(
this.model.id,
this.model.name,
true,
this.model.method
)
.then(response => {
if (response.error) {
return;
}
this.markDirty();
})
.catch(error => {
this.send("closeModal");
this.onError(error);
})
.finally(() => {
this.set("loading", false);
this.send("closeModal");
});
},
editSecondFactor() {
this.user
.updateSecondFactor(
this.model.id,
this.model.name,
false,
this.model.method
)
.then(response => {
if (response.error) {
return;
}
this.markDirty();
})
.catch(error => {
this.send("closeModal");
this.onError(error);
})
.finally(() => {
this.set("loading", false);
this.send("closeModal");
});
}
}
});

View File

@ -61,7 +61,10 @@ if (Discourse.SiteSettings.tagging_enabled) {
class: "btn-default"
});
}
addBulkButton("deleteTopics", "delete", { icon: "trash", class: "btn-danger" });
addBulkButton("deleteTopics", "delete", {
icon: "trash-alt",
class: "btn-danger"
});
// Modal for performing bulk actions on topics
export default Ember.Controller.extend(ModalFunctionality, {

View File

@ -17,6 +17,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { spinnerHTML } from "discourse/helpers/loading-spinner";
import { userPath } from "discourse/lib/url";
import showModal from "discourse/lib/show-modal";
import TopicTimer from "discourse/models/topic-timer";
let customPostMessageCallbacks = {};
@ -522,6 +523,16 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
if (user.get("staff") && hasReplies) {
ajax(`/posts/${post.id}/reply-ids.json`).then(replies => {
if (replies.length === 0) {
return post
.destroy(user)
.then(refresh)
.catch(error => {
popupAjaxError(error);
post.undoDeleteState();
});
}
const buttons = [];
buttons.push({
@ -940,6 +951,46 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
}
},
joinGroup() {
const groupId = this.get("model.group.id");
if (groupId) {
if (this.get("model.group.allow_membership_requests")) {
const groupName = this.get("model.group.name");
return ajax(`/groups/${groupName}/request_membership`, {
type: "POST",
data: {
topic_id: this.get("model.id")
}
})
.then(() => {
bootbox.alert(
I18n.t("topic.group_request_sent", {
group_name: this.get("model.group.full_name")
}),
() =>
this.previousURL
? DiscourseURL.routeTo(this.previousURL)
: DiscourseURL.routeTo("/")
);
})
.catch(popupAjaxError);
} else {
const topic = this.model;
return ajax(`/groups/${groupId}/members`, {
type: "PUT",
data: { user_id: this.get("currentUser.id") }
})
.then(() =>
topic.reload().then(() => {
topic.set("view_hidden", false);
topic.postStream.refresh();
})
)
.catch(popupAjaxError);
}
}
},
replyAsNewTopic(post, quotedText) {
const composerController = this.composer;
@ -1035,6 +1086,18 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
resetBumpDate() {
this.model.resetBumpDate();
},
removeTopicTimer(statusType, topicTimer) {
TopicTimer.updateStatus(
this.get("model.id"),
null,
null,
statusType,
null
)
.then(() => this.set(`model.${topicTimer}`, Ember.Object.create({})))
.catch(error => popupAjaxError(error));
}
},

View File

@ -5,9 +5,9 @@ export default {
name: "avatar-select",
initialize(container) {
this.selectAvatarsEnabled = container.lookup(
this.selectableAvatarsEnabled = container.lookup(
"site-settings:main"
).select_avatars_enabled;
).selectable_avatars_enabled;
container
.lookup("app-events:main")
@ -27,7 +27,7 @@ export default {
const modal = showModal("avatar-selector");
modal.setProperties({ user, selected });
if (this.selectAvatarsEnabled) {
if (this.selectableAvatarsEnabled) {
ajax("/site/selectable-avatars.json").then(avatars =>
modal.set("selectableAvatars", avatars)
);

View File

@ -1,6 +1,7 @@
import { cleanDOM } from "discourse/lib/clean-dom";
import {
startPageTracking,
resetPageTracking,
googleTagManagerPageChanged
} from "discourse/lib/page-tracker";
import { viewTrackingRequired } from "discourse/lib/ajax";
@ -49,5 +50,9 @@ export default {
}
});
}
},
teardown() {
resetPageTracking();
}
};

View File

@ -101,7 +101,7 @@ const DiscourseLocation = Ember.Object.extend({
const state = this.getState();
path = this.formatURL(path);
if (state && state.path !== path) {
if (!state || state.path !== path) {
this.replaceState(path);
}
},

View File

@ -12,8 +12,7 @@ Eyeline.prototype.update = function() {
windowHeight = $(window).height(),
docViewBottom = docViewTop + windowHeight,
$elements = $(this.selector),
bottomOffset = $elements.last().offset(),
self = this;
bottomOffset = $elements.last().offset();
let atBottom = false;
if (bottomOffset) {
@ -21,7 +20,7 @@ Eyeline.prototype.update = function() {
bottomOffset.top <= docViewBottom && bottomOffset.top >= docViewTop;
}
return $elements.each(function(i, elem) {
return $elements.each((i, elem) => {
const $elem = $(elem),
elemTop = $elem.offset().top,
elemBottom = elemTop + $elem.height();
@ -45,17 +44,17 @@ Eyeline.prototype.update = function() {
// If you hit the bottom we mark all the elements as seen. Otherwise, just the first one
if (!atBottom) {
self.trigger("saw", { detail: $elem });
this.trigger("saw", { detail: $elem });
if (i === 0) {
self.trigger("sawTop", { detail: $elem });
this.trigger("sawTop", { detail: $elem });
}
return false;
}
if (i === 0) {
self.trigger("sawTop", { detail: $elem });
this.trigger("sawTop", { detail: $elem });
}
if (i === $elements.length - 1) {
return self.trigger("sawBottom", { detail: $elem });
return this.trigger("sawBottom", { detail: $elem });
}
});
};
@ -65,10 +64,8 @@ Eyeline.prototype.flushRest = function() {
if (Ember.testing) {
return;
}
const self = this;
$(this.selector).each(function(i, elem) {
return self.trigger("saw", { detail: $(elem) });
});
$(this.selector).each((i, elem) => this.trigger("saw", { detail: $(elem) }));
};
RSVP.EventTarget.mixin(Eyeline.prototype);

View File

@ -65,9 +65,10 @@ const bindings = {
"shift+p": { handler: "pinUnpinTopic" },
"shift+r": { handler: "replyToTopic" },
"shift+s": { click: "#topic-footer-buttons button.share", anonymous: true }, // share topic
"shift+u": { handler: "goToUnreadPost" },
"shift+l": { handler: "goToUnreadPost" },
"shift+z shift+z": { handler: "logout" },
"shift+f11": { handler: "fullscreenComposer", global: true },
"shift+u": { handler: "deferTopic" },
t: { postAction: "replyAsNewTopic" },
u: { handler: "goBack", anonymous: true },
"x r": {
@ -438,9 +439,22 @@ export default {
$selected = $articles.filter("[data-islastviewedtopic=true]");
}
// Discard selection if it is not in viewport, so users can combine
// keyboard shortcuts with mouse scrolling.
if ($selected.length !== 0) {
const offset = minimumOffset();
const beginScreen = $(window).scrollTop() - offset;
const endScreen = beginScreen + window.innerHeight + offset;
const beginArticle = $selected.offset().top;
const endArticle = $selected.offset().top + $selected.height();
if (beginScreen > endArticle || beginArticle > endScreen) {
$selected = null;
}
}
// If still nothing is selected, select the first post that is
// visible and cancel move operation.
if ($selected.length === 0) {
if (!$selected || $selected.length === 0) {
const offset = minimumOffset();
$selected = $articles
.toArray()
@ -618,5 +632,9 @@ export default {
_replyToPost() {
this.container.lookup("controller:topic").send("replyToPost");
},
deferTopic() {
this.container.lookup("controller:topic").send("deferTopic");
}
};

View File

@ -10,9 +10,7 @@ function replaceSpan($e, username, opts) {
if (opts && opts.group) {
if (opts.mentionable) {
extra = `data-name='${username}' data-mentionable-user-count='${
opts.mentionable.user_count
}' data-max-mentions='${maxGroupMention}'`;
extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}' data-max-mentions='${maxGroupMention}'`;
extraClass = "notify";
}
$e.replaceWith(

View File

@ -1,6 +1,5 @@
let _started = false;
const cache = {};
let cache = {};
let transitionCount = 0;
export function setTransient(key, data, count) {
@ -11,6 +10,12 @@ export function getTransient(key) {
return cache[key];
}
export function resetPageTracking() {
_started = false;
transitionCount = 0;
cache = {};
}
export function startPageTracking(router, appEvents) {
if (_started) {
return;

View File

@ -42,6 +42,7 @@ import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1
import Sharing from "discourse/lib/sharing";
import { addComposerUploadHandler } from "discourse/components/composer-editor";
import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
import { queryRegistry } from "discourse/widgets/widget";
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = "0.8.31";
@ -325,7 +326,9 @@ class PluginApi {
* ```
**/
attachWidgetAction(widget, actionName, fn) {
const widgetClass = this.container.factoryFor(`widget:${widget}`).class;
const widgetClass =
queryRegistry(widget) ||
this.container.factoryFor(`widget:${widget}`).class;
widgetClass.prototype[actionName] = fn;
}

View File

@ -53,9 +53,7 @@ export function registerTopicFooterButton(button) {
!normalizedButton.translatedTitle
) {
Ember.error(
`Attempted to register a topic button: ${
button.id
} with no icon or title.`
`Attempted to register a topic button: ${button.id} with no icon or title.`
);
return;
}

View File

@ -21,6 +21,7 @@ export default function(topic, params) {
let tags = topic.tags;
let buffer = "";
let tagsForUser = null;
let tagName;
const isPrivateMessage = topic.get("isPrivateMessage");
if (params) {
@ -30,6 +31,9 @@ export default function(topic, params) {
if (params.tagsForUser) {
tagsForUser = params.tagsForUser;
}
if (params.tagName) {
tagName = params.tagName;
}
}
let customHtml = null;
@ -50,7 +54,8 @@ export default function(topic, params) {
buffer = "<div class='discourse-tags'>";
if (tags) {
for (let i = 0; i < tags.length; i++) {
buffer += renderTag(tags[i], { isPrivateMessage, tagsForUser }) + " ";
buffer +=
renderTag(tags[i], { isPrivateMessage, tagsForUser, tagName }) + " ";
}
}

View File

@ -116,16 +116,15 @@ function positioningWorkaround($fixedElement) {
var blurred = debounce(blurredNow, 250);
var positioningHack = function(evt) {
const self = this;
done = false;
// we need this, otherwise changing focus means we never clear
self.addEventListener("blur", blurred);
this.addEventListener("blur", blurred);
if (fixedElement.style.top === "0px") {
if (this !== document.activeElement) {
evt.preventDefault();
self.focus();
this.focus();
}
return;
}
@ -157,7 +156,7 @@ function positioningWorkaround($fixedElement) {
$(fixedElement).addClass("no-transition");
evt.preventDefault();
self.focus();
this.focus();
workaroundActive = true;
};

View File

@ -1,36 +1,7 @@
import { isAppleDevice } from "discourse/lib/utilities";
export default function(name, opts) {
opts = opts || {};
const container = Discourse.__container__;
// iOS 11 -> 11.1 have broken INPUTs on position fixed
// if for any reason there is a body higher than 100% behind them.
// What happens is that when INPUTs gets focus they shift the body
// which ends up moving the cursor to an invisible spot
// this makes the login experience on iOS painful, user thinks it is broken.
//
// Also, very little value in showing main outlet and header on iOS
// anyway, so just hide it.
if (isAppleDevice()) {
let pos = $(window).scrollTop();
$(window)
.off("show.bs.modal.ios-hacks")
.on("show.bs.modal.ios-hacks", () => {
$("#main-outlet, header").hide();
});
$(window)
.off("hide.bs.modal.ios-hacks")
.on("hide.bs.modal.ios-hacks", () => {
$("#main-outlet, header").show();
$(window).scrollTop(pos);
$(window).off("hide.bs.modal.ios-hacks");
$(window).off("show.bs.modal.ios-hacks");
});
}
// We use the container here because modals are like singletons
// in Discourse. Only one can be shown with a particular state.
const route = container.lookup("route:application");

View File

@ -206,8 +206,16 @@ export class Tag {
["lightbox", "d-lazyload"].includes(attr.class) &&
hasChild(e, "img")
) {
let href = attr.href;
const img = (e.children || []).find(c => c.name === "img");
const base62SHA1 = img.attributes["data-base62-sha1"];
text = attr.title || "";
return "![" + text + "](" + attr.href + ")";
if (base62SHA1) {
href = `upload://${base62SHA1}`;
}
return "![" + text + "](" + href + ")";
}
if (attr.href && text !== attr.href) {

View File

@ -428,13 +428,6 @@ const DiscourseURL = Ember.Object.extend({
if (opts.replaceURL) {
this.replaceState(path);
} else {
const discoveryTopics = this.controllerFor("discovery/topics");
if (discoveryTopics) {
discoveryTopics.resetParams();
}
router._routerMicrolib.updateURL(path);
}
const split = path.split("#");
@ -445,7 +438,16 @@ const DiscourseURL = Ember.Object.extend({
elementId = split[1];
}
const transition = router.handleURL(path);
// The default path has a hack to allow `/` to default to defaultHomepage
// via BareRouter.handleUrl
let transition;
if (path === "/" || path.substring(0, 2) === "/?") {
router._routerMicrolib.updateURL(path);
transition = router.handleURL(path);
} else {
transition = router.transitionTo(path);
}
transition._discourse_intercepted = true;
const promise = transition.promise || transition;
promise.then(() => jumpToElement(elementId));

View File

@ -18,7 +18,7 @@ export default Ember.Mixin.create({
this.selected.clear();
},
dismissRead(operationType) {
dismissRead(operationType, categoryOptions) {
let operation;
if (operationType === "posts") {
operation = { type: "dismiss_posts" };
@ -36,7 +36,8 @@ export default Ember.Mixin.create({
promise = Discourse.Topic.bulkOperationByFilter(
"unread",
operation,
this.get("category.id")
this.get("category.id"),
categoryOptions
);
}

View File

@ -29,7 +29,7 @@ const LoginMethod = Ember.Object.extend({
authUrl += "?reconnect=true";
}
if (fullScreenLogin) {
if (fullScreenLogin || this.full_screen_login) {
document.cookie = "fsl=true";
window.location = authUrl;
} else {

View File

@ -69,7 +69,8 @@ const Post = RestModel.extend({
return postNumber === 1 ? `${baseUrl}/1` : baseUrl;
},
@computed("username") usernameUrl: userPath,
@computed("username")
usernameUrl: userPath,
topicOwner: propertyEqual("topic.details.created_by.id", "user_id"),

View File

@ -12,11 +12,11 @@ StaticPage.reopenClass({
text = text.match(
/<!-- preload-content: -->((?:.|[\n\r])*)<!-- :preload-content -->/
)[1];
resolve(StaticPage.create({ path: path, html: text }));
resolve(StaticPage.create({ path, html: text }));
} else {
ajax(path + ".html", { dataType: "html" }).then(function(result) {
resolve(StaticPage.create({ path: path, html: result }));
});
ajax(`/${path}.html`, { dataType: "html" }).then(result =>
resolve(StaticPage.create({ path, html: result }))
);
}
});
}

View File

@ -481,12 +481,12 @@ const Topic = RestModel.extend({
// Update our attributes from a JSON result
updateFromJson(json) {
this.details.updateFromJson(json.details);
const keys = Object.keys(json);
keys.removeObject("details");
keys.removeObject("post_stream");
if (!json.view_hidden) {
this.details.updateFromJson(json.details);
keys.removeObjects(["details", "post_stream"]);
}
keys.forEach(key => this.set(key, json[key]));
},
@ -733,8 +733,13 @@ Topic.reopenClass({
});
},
bulkOperationByFilter(filter, operation, categoryId) {
const data = { filter, operation };
bulkOperationByFilter(filter, operation, categoryId, options) {
let data = { filter, operation };
if (options && options.includeSubcategories) {
data.include_subcategories = true;
}
if (categoryId) data.category_id = categoryId;
return ajax("/topics/bulk", {
type: "PUT",

View File

@ -366,6 +366,40 @@ const User = RestModel.extend({
});
},
createSecondFactorTotp() {
return ajax("/u/create_second_factor_totp.json", {
type: "POST"
});
},
enableSecondFactorTotp(authToken, name) {
return ajax("/u/enable_second_factor_totp.json", {
data: {
second_factor_token: authToken,
name
},
type: "POST"
});
},
disableAllSecondFactors() {
return ajax("/u/disable_second_factor.json", {
type: "PUT"
});
},
updateSecondFactor(id, name, disable, targetMethod) {
return ajax("/u/second_factor.json", {
data: {
second_factor_target: targetMethod,
name,
disable,
id
},
type: "PUT"
});
},
toggleSecondFactor(authToken, authMethod, targetMethod, enable) {
return ajax("/u/second_factor.json", {
data: {
@ -378,12 +412,8 @@ const User = RestModel.extend({
});
},
generateSecondFactorCodes(authToken, authMethod) {
generateSecondFactorCodes() {
return ajax("/u/second_factors_backup.json", {
data: {
second_factor_token: authToken,
second_factor_method: authMethod
},
type: "PUT"
});
},

View File

@ -68,10 +68,12 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
composePrivateMessage(user, post) {
const recipient = user ? user.get("username") : "",
reply = post
? window.location.protocol +
"//" +
window.location.host +
post.get("url")
? `${window.location.protocol}//${window.location.host}${post.url}`
: null,
title = post
? I18n.t("composer.reference_topic_title", {
title: post.topic.title
})
: null;
// used only once, one less dependency
@ -80,7 +82,8 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
usernames: recipient,
archetypeId: "private_message",
draftKey: "new_private_message",
reply: reply
reply,
title
});
},

View File

@ -60,12 +60,15 @@ export default Discourse.Route.extend(OpenComposer, {
},
dismissReadTopics(dismissTopics) {
var operationType = dismissTopics ? "topics" : "posts";
this.controllerFor("discovery/topics").send("dismissRead", operationType);
const operationType = dismissTopics ? "topics" : "posts";
this.send("dismissRead", operationType);
},
dismissRead(operationType) {
this.controllerFor("discovery/topics").send("dismissRead", operationType);
const controller = this.controllerFor("discovery/topics");
controller.send("dismissRead", operationType, {
includeSubcategories: !controller.noSubcategories
});
}
}
});

View File

@ -1,21 +0,0 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
model() {
return this.modelFor("user");
},
renderTemplate() {
return this.render({ into: "user" });
},
setupController(controller, model) {
controller.setProperties({ model, newUsername: model.get("username") });
},
deactivate() {
this.controller.setProperties({ backupCodes: null });
}
});

View File

@ -13,6 +13,23 @@ export default RestrictedUserRoute.extend({
setupController(controller, model) {
controller.setProperties({ model, newUsername: model.get("username") });
controller.set("loading", true);
model
.loadSecondFactorCodes("")
.then(response => {
if (response.error) {
controller.set("errorMessage", response.error);
} else {
controller.setProperties({
errorMessage: null,
loaded: !response.password_required,
dirty: !!response.password_required,
totps: response.totps
});
}
})
.catch(controller.popupAjaxError)
.finally(() => controller.set("loading", false));
},
actions: {
@ -26,6 +43,7 @@ export default RestrictedUserRoute.extend({
if (
transition.targetName === "preferences.second-factor" ||
!user ||
(settings.allow_anonymous_posting && user.is_anonymous) ||
user.second_factor_enabled ||
(settings.enforce_second_factor === "staff" && !user.staff) ||
settings.enforce_second_factor === "no"

View File

@ -1,15 +1,16 @@
import Composer from "discourse/models/composer";
import showModal from "discourse/lib/show-modal";
import { findTopicList } from "discourse/routes/build-topic-route";
import {
filterQueryParams,
findTopicList
} from "discourse/routes/build-topic-route";
import { queryParams } from "discourse/controllers/discovery-sortable";
import PermissionType from "discourse/models/permission-type";
export default Discourse.Route.extend({
navMode: "latest",
queryParams: {
ascending: { refreshModel: true },
order: { refreshModel: true }
},
queryParams,
renderTemplate() {
const controller = this.controllerFor("tags.show");
@ -69,10 +70,7 @@ export default Discourse.Route.extend({
const controller = this.controllerFor("tags.show");
controller.set("loading", true);
const params = controller.getProperties("order", "ascending");
params.order = transition.to.queryParams.order || params.order;
params.ascending = transition.to.queryParams.ascending || params.ascending;
const params = filterQueryParams(transition.to.queryParams, {});
const categorySlug = this.categorySlug;
const parentCategorySlug = this.parentCategorySlug;
const filter = this.navMode;

View File

@ -35,6 +35,11 @@ export default Discourse.Route.extend({
// TODO we are seeing errors where closest post is null and this is exploding
// we need better handling and logging for this condition.
// there are no closestPost for hidden topics
if (topic.view_hidden) {
return;
}
// The post we requested might not exist. Let's find the closest post
const closestPost = postStream.closestPostForPostNumber(
params.nearPost || 1
@ -76,5 +81,18 @@ export default Discourse.Route.extend({
console.log("Could not view topic", e);
}
});
},
actions: {
willTransition() {
this.controllerFor("topic").set(
"previousURL",
document.location.pathname
);
// NOTE: omitting this return can break the back button when transitioning quickly between
// topics and the latest page.
return true;
}
}
});

View File

@ -225,9 +225,13 @@ const TopicRoute = Discourse.Route.extend({
model(params, transition) {
if (params.slug.match(ID_CONSTRAINT)) {
return DiscourseURL.routeTo(`/t/topic/${params.slug}/${params.id}`, {
transition.abort();
DiscourseURL.routeTo(`/t/topic/${params.slug}/${params.id}`, {
replaceURL: true
});
return;
}
const queryParams = transition.to.queryParams;

View File

@ -47,7 +47,7 @@
class="d-editor-input"
placeholder=placeholderTranslated
disabled=disabled
change=change}}
input=change}}
{{popup-input-tip validation=validation}}
{{plugin-outlet name="after-d-editor" tagName="" args=outletArgs}}
</div>

View File

@ -9,6 +9,10 @@
content=visibilityLevelOptions
castInteger=true
class="groups-form-visibility-level"}}
<div class="control-instructions">
{{i18n 'admin.groups.manage.interaction.visibility_levels.description'}}
</div>
</div>
{{/if}}

View File

@ -0,0 +1,5 @@
{{accessViaGroupText}}
{{d-button action=action
class="btn-primary topic-join-group"
icon="user-plus"
label=accessViaGroupButtonText}}

View File

@ -125,6 +125,12 @@
{{#if searchActive}}
<h3>{{i18n "search.no_results"}}</h3>
{{#if model.grouped_search_result.error}}
<div class="warning">
{{model.grouped_search_result.error}}
</div>
{{/if}}
{{#if showSuggestion}}
<div class="no-results-suggestion">
{{i18n "search.cant_find"}}

View File

@ -1,5 +1,5 @@
<div class='bulk-buttons'>
{{#each buttons as |button|}}
{{d-button action=button.action label=button.label icon=button.icon class=button.class}}
{{d-button action=(action button.action) label=button.label icon=button.icon class=button.class}}
{{/each}}
</div>

View File

@ -8,13 +8,21 @@
</p>
<form>
{{date-picker-past value=date containerId="date-container"}}
{{date-picker-past
value=(unbound date)
containerId="date-container"
onSelect=(action (mut date))}}
{{input type="time" value=time}}
</form>
<div id="date-container" />
<div id="date-container"></div>
{{/d-modal-body}}
<div class="modal-footer change-timestamp-footer">
<button class="btn btn-primary" disabled={{buttonDisabled}} {{action "changeTimestamp"}}>{{buttonTitle}}</button>
{{d-button
class="btn-primary"
disabled=buttonDisabled
action=(action "changeTimestamp")
translatedLabel=buttonTitle}}
</div>

View File

@ -23,6 +23,7 @@
<li>{{{shortcuts.navigation.up_down}}}</li>
<li>{{{shortcuts.navigation.open}}}</li>
<li>{{{shortcuts.navigation.next_prev}}}</li>
<li>{{{shortcuts.navigation.go_to_unread_post}}}</li>
</ul>
</div>
<div>
@ -64,6 +65,7 @@
<li>{{{shortcuts.actions.mark_regular}}}</li>
<li>{{{shortcuts.actions.mark_tracking}}}</li>
<li>{{{shortcuts.actions.mark_watching}}}</li>
<li>{{{shortcuts.actions.defer}}}</li>
<li>{{{shortcuts.actions.print}}}</li>
</ul>
</div>

View File

@ -0,0 +1,51 @@
{{#d-modal-body}}
{{#conditional-loading-spinner condition=loading}}
{{#if errorMessage}}
<div class="control-group">
<div class="controls">
<div class='alert alert-error'>{{errorMessage}}</div>
</div>
</div>
{{/if}}
<div class="control-group">
<div class="controls">
{{{i18n 'user.second_factor.enable_description'}}}
</div>
</div>
<div class="control-group">
<div class="controls">
<div class="qr-code-container">
<div class="qr-code">
{{{secondFactorImage}}}
</div>
</div>
<p>
{{#if showSecondFactorKey}}
{{secondFactorKey}}
{{else}}
<a {{action "showSecondFactorKey"}}>{{i18n 'user.second_factor.show_key_description'}}</a>
{{/if}}
</p>
</div>
</div>
<div class="control-group">
<label class="control-label input-prepend">{{i18n 'user.second_factor.label'}}</label>
<div class="controls">
{{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token'}}
</div>
</div>
<div class="control-group">
<div class="controls">
{{d-button action=(action "enableSecondFactor")
class="btn btn-primary add-totp"
label="enable"}}
</div>
</div>
{{/conditional-loading-spinner}}
{{/d-modal-body}}

View File

@ -0,0 +1,50 @@
{{#d-modal-body}}
<section class="user-preferences solo-preference second-factor-backup-preferences">
<form class="form-horizontal">
{{#if successMessage}}
<div class="alert alert-success">
{{successMessage}}
</div>
{{/if}}
{{#if errorMessage}}
<div class="alert alert-error">
{{errorMessage}}
</div>
{{/if}}
{{#if backupEnabled}}
{{{i18n "user.second_factor_backup.remaining_codes" count=remainingCodes}}}
{{/if}}
<div class="actions">
{{d-button
action=(action "generateSecondFactorCodes")
class="btn btn-primary"
disabled=loading
label=generateBackupCodeBtnLabel}}
{{#if backupEnabled}}
{{d-button
action=(action "disableSecondFactorBackup")
class="btn btn-danger"
disabled=loading
label="user.second_factor_backup.disable"}}
{{/if}}
</div>
{{#conditional-loading-section isLoading=loading}}
{{#if backupCodes}}
<h3>{{i18n "user.second_factor_backup.codes.title"}}</h3>
<p>
{{i18n "user.second_factor_backup.codes.description"}}
</p>
{{backup-codes
copyBackupCode=(action "copyBackupCode")
backupCodes=backupCodes}}
{{/if}}
{{/conditional-loading-section}}
</form>
</section>
{{/d-modal-body}}

View File

@ -0,0 +1,15 @@
{{#d-modal-body}}
<div class="form-horizontal">
{{input type="text" value=model.name}}
</div>
<div class='second-factor instructions'>
{{i18n 'user.second_factor.edit_description'}}
</div>
{{d-button action=(action "editSecondFactor")
class="btn-primary"
label="user.second_factor.edit"}}
{{d-button action=(action "disableSecondFactor")
class="btn-danger"
label="user.second_factor.disable"}}
{{/d-modal-body}}

View File

@ -1,71 +0,0 @@
<section class="user-preferences solo-preference second-factor-backup-preferences">
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
{{#if successMessage}}
<div class="alert alert-success">
{{successMessage}}
</div>
{{/if}}
{{#if errorMessage}}
<div class="alert alert-error">
{{errorMessage}}
</div>
{{/if}}
</div>
</div>
<div class="control-group">
<div class="controls">
{{#second-factor-form
secondFactorMethod=secondFactorMethod
backupEnabled=backupEnabled
secondFactorToken=secondFactorToken
secondFactorTitle=(i18n 'user.second_factor_backup.title')
optionalText=(if backupEnabled (i18n "user.second_factor_backup.remaining_codes" count=remainingCodes))
isLogin=false}}
{{second-factor-input value=secondFactorToken inputId='second-factor-token' secondFactorMethod=secondFactorMethod}}
{{/second-factor-form}}
</div>
</div>
<div class="control-group">
<div class="controls">
<div class="actions">
{{d-button
action=(action "generateSecondFactorCodes")
class="btn btn-primary"
disabled=isDisabledGenerateBackupCodeBtn
label=generateBackupCodeBtnLabel}}
{{#if backupEnabled}}
{{d-button
action=(action "disableSecondFactorBackup")
class="btn btn-danger"
disabled=isDisabledDisableBackupCodeBtn
label="user.second_factor_backup.disable"}}
{{/if}}
</div>
{{#conditional-loading-section isLoading=loading}}
{{#if backupCodes}}
<h3>{{i18n "user.second_factor_backup.codes.title"}}</h3>
<p>
{{i18n "user.second_factor_backup.codes.description"}}
</p>
{{backup-codes
copyBackupCode=(action "copyBackupCode")
backupCodes=backupCodes}}
{{#link-to "preferences.account" model.username}}
{{i18n "go_back"}}
{{/link-to}}
{{/if}}
{{/conditional-loading-section}}
</div>
</div>
</form>
</section>

View File

@ -1,4 +1,5 @@
<section class='user-preferences solo-preference'>
<section class='user-preferences solo-preference second-factor'>
{{#conditional-loading-spinner condition=loading}}
<form class="form-horizontal">
{{#if showEnforcedNotice}}
@ -9,6 +10,14 @@
</div>
{{/if}}
{{#if displayOAuthWarning}}
<div class="control-group">
<div class="controls">
{{i18n 'user.second_factor.oauth_enabled_warning'}}
</div>
</div>
{{/if}}
{{#if errorMessage}}
<div class="control-group">
<div class="controls">
@ -17,121 +26,105 @@
</div>
{{/if}}
{{#if model.second_factor_enabled}}
{{#if loaded}}
<div class="control-group">
<div class="controls">
{{#second-factor-form
secondFactorMethod=secondFactorMethod
backupEnabled=backupEnabled
secondFactorToken=secondFactorToken
secondFactorTitle=(i18n 'user.second_factor.title')
isLogin=false}}
{{second-factor-input value=secondFactorToken inputId='second-factor-token' secondFactorMethod=secondFactorMethod}}
{{/second-factor-form}}
<h2>{{i18n "user.second_factor.totp.title"}}</h2>
{{d-button action=(action "createTotp")
class="btn-primary new-totp"
disabled=loading
label="user.second_factor.totp.add"}}
{{#each totps as |totp|}}
<div class="second-factor-item">
{{totp.name}}
{{#if isCurrentUser}}
{{d-button action=(action "editSecondFactor" totp)
class="btn-default btn-small btn-icon pad-left no-text edit"
disabled=loading
icon="pencil-alt"
}}
{{/if}}
</div>
{{/each}}
</div>
</div>
<div class="control-group">
<div class="controls pref-second-factor-backup">
<h2>{{i18n "user.second_factor_backup.title"}}</h2>
{{#if model.second_factor_enabled}}
{{#if model.second_factor_backup_enabled}}
{{{i18n 'user.second_factor_backup.manage' count=model.second_factor_remaining_backup_codes}}}
{{else}}
{{i18n 'user.second_factor_backup.enable_long'}}
{{/if}}
{{#if isCurrentUser}}
{{d-button action=(action "editSecondFactorBackup")
class="btn-default btn-small btn-icon pad-left no-text edit edit-2fa-backup"
disabled=loading
icon="pencil-alt"
}}
{{/if}}
{{else}}
{{i18n "user.second_factor_backup.enable_prerequisites"}}
{{/if}}
</div>
</div>
{{#if model.second_factor_enabled}}
{{#unless showEnforcedNotice}}
<div class="control-group">
<div class="controls">
<h2>{{i18n "user.second_factor.disable_title"}}</h2>
{{d-button action=(action "disableAllSecondFactors")
class="btn btn-danger"
disabled=loading
label="disable"}}
</div>
</div>
{{/unless}}
{{/if}}
{{else}}
<div class="control-group">
<label class='control-label'>{{i18n 'user.password.title'}}</label>
<div class="controls">
<div>
{{text-field value=password
id="password"
type="password"
classNames="input-xxlarge"
autofocus="autofocus"}}
</div>
<div class='instructions'>
{{i18n 'user.second_factor.confirm_password_description'}}
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
{{d-button action=(action "disableSecondFactor")
class="btn btn-primary"
disabled=loading
label=disableButtonText}}
{{d-button action=(action "confirmPassword")
class="btn-primary"
disabled=loading
label="continue"}}
{{d-button action=(action "resetPassword")
class="btn"
disabled=resetPasswordLoading
icon="envelope"
label='user.change_password.action'}}
{{resetPasswordProgress}}
{{#unless showEnforcedNotice}}
{{cancel-link route="preferences.account" args= model.username}}
{{/unless}}
</div>
</div>
{{else}}
{{#if loaded}}
<div class="control-group">
<div class="controls">
{{{i18n 'user.second_factor.enable_description'}}}
{{#if displayOAuthWarning}}
{{i18n 'user.second_factor.oauth_enabled_warning'}}
{{/if}}
</div>
</div>
<div class="control-group">
<div class="controls">
<div class="qr-code-container">
<div class="qr-code">
{{{secondFactorImage}}}
</div>
</div>
<p>
{{#if showSecondFactorKey}}
{{secondFactorKey}}
{{else}}
<a {{action "showSecondFactorKey"}}>{{i18n 'user.second_factor.show_key_description'}}</a>
{{/if}}
</p>
</div>
</div>
<div class="control-group">
<label class="control-label input-prepend">{{i18n 'user.second_factor.label'}}</label>
<div class="controls">
{{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token'}}
</div>
</div>
<div class="control-group">
<div class="controls">
{{d-button action=(action "enableSecondFactor")
class="btn btn-primary"
disabled=loading
label=enableButtonText}}
{{#unless showEnforcedNotice}}
{{cancel-link route="preferences.account" args= model.username}}
{{/unless}}
</div>
</div>
{{else}}
<div class="control-group">
<label class='control-label'>{{i18n 'user.password.title'}}</label>
<div class="controls">
<div>
{{text-field value=password
id="password"
type="password"
classNames="input-xxlarge"
autofocus="autofocus"}}
</div>
<div class='instructions'>
{{i18n 'user.second_factor.confirm_password_description'}}
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
{{d-button action=(action "confirmPassword")
class="btn btn-primary"
disabled=loading
label=submitButtonText}}
{{d-button action=(action "resetPassword")
class="btn"
disabled=resetPasswordLoading
icon="envelope"
label='user.change_password.action'}}
{{resetPasswordProgress}}
{{#unless showEnforcedNotice}}
{{cancel-link route="preferences.account" args= model.username}}
{{/unless}}
</div>
</div>
{{/if}}
{{/if}}
</form>
{{/conditional-loading-spinner}}
</section>

View File

@ -90,32 +90,11 @@
</label>
{{/unless}}
<div class="controls pref-second-factor">
{{i18n 'user.second_factor.enable'}}
{{#if isCurrentUser}}
{{#if model.second_factor_enabled}}
{{#link-to "preferences.second-factor" class="btn btn-default"}}
{{d-icon "unlock"}} <span>{{i18n 'user.second_factor.disable'}}</span>
{{/link-to}}
{{else}}
{{#link-to "preferences.second-factor" class="btn btn-default"}}
{{d-icon "lock"}} <span>{{i18n 'user.second_factor.enable'}}</span>
{{/link-to}}
{{/if}}
{{/if}}
</div>
<div class="controls pref-second-factor-backup">
{{#if model.second_factor_enabled}}
{{#if isCurrentUser}}
{{#link-to "preferences.second-factor-backup"}}
<span>
{{#if model.second_factor_backup_enabled}}
{{i18n 'user.second_factor_backup.manage'}}
{{else}}
{{i18n 'user.second_factor_backup.enable_long'}}
{{/if}}
</span>
{{/link-to}}
{{/if}}
{{#link-to "preferences.second-factor" class="btn btn-default"}}
{{d-icon "lock"}} <span>{{i18n 'user.second_factor.enable'}}</span>
{{/link-to}}
{{/if}}
</div>
</div>
@ -126,31 +105,32 @@
<label class="control-label">{{i18n 'user.associated_accounts.title'}}</label>
{{#if associatedAccountsLoaded}}
<table>
{{#each authProviders as |authProvider|}}
<tr>
<td>{{authProvider.method.prettyName}}</td>
{{#each authProviders as |authProvider|}}
{{#if authProvider.account}}
<td>{{authProvider.account.description}}</td>
<td>
{{#if authProvider.method.can_revoke}}
{{#conditional-loading-spinner condition=revoking size='small'}}
{{d-button action=(action "revokeAccount") actionParam=authProvider.account title="user.associated_accounts.revoke" class="btn-danger no-text" icon="trash-alt" }}
{{/conditional-loading-spinner}}
{{/if}}
</td>
<tr class="{{dasherize authProvider.method.name}} account-connected">
<td>{{authProvider.method.prettyName}}</td>
<td>{{authProvider.account.description}}</td>
<td>
{{#if authProvider.method.can_revoke}}
{{#conditional-loading-spinner condition=revoking size='small'}}
{{d-button action=(action "revokeAccount") actionParam=authProvider.account title="user.associated_accounts.revoke" class="btn-danger no-text" icon="trash-alt" }}
{{/conditional-loading-spinner}}
{{/if}}
</td>
</tr>
{{else}}
<td colspan=2>
{{#if authProvider.method.can_connect}}
{{d-button action=(action "connectAccount") actionParam=authProvider.method label="user.associated_accounts.connect" class="btn-default" icon="plug" disabled=disableConnectButtons}}
{{else}}
{{i18n 'user.associated_accounts.not_connected'}}
{{/if}}
</td>
<tr class="{{dasherize authProvider.method.name}}">
<td>{{authProvider.method.prettyName}}</td>
<td colspan=2>
{{#if authProvider.method.can_connect}}
{{d-button action=(action "connectAccount") actionParam=authProvider.method label="user.associated_accounts.connect" class="btn-default" icon="plug" disabled=disableConnectButtons}}
{{else}}
{{i18n 'user.associated_accounts.not_connected'}}
{{/if}}
</td>
</tr>
{{/if}}
</tr>
{{/each}}
{{/each}}
</table>
{{else}}
<div class="controls">

View File

@ -3,25 +3,23 @@
<div class="controls tracking-controls">
<label>{{d-icon "d-watching"}} {{i18n 'user.watched_categories'}}</label>
{{#if canSee}}
<a class="show-tracking" href="{{unbound model.watchingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
{{/if}}
{{category-selector categories=model.watchedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.watched_categories_instructions'}}</div>
{{#if canSee}}
<div class="controls">
<a href="{{unbound model.watchingTopicsPath}}">{{i18n 'user.watched_topics_link'}}</a>
</div>
{{/if}}
<div class="controls tracking-controls">
<label>{{d-icon "d-tracking"}} {{i18n 'user.tracked_categories'}}</label>
{{#if canSee}}
<a class="show-tracking" href="{{unbound model.trackingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
{{/if}}
{{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.tracked_categories_instructions'}}</div>
{{#if canSee}}
<div class="controls">
<a href="{{unbound model.trackingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
</div>
{{/if}}
<div class="controls tracking-controls">
<label>{{d-icon "d-watching-first"}} {{i18n 'user.watched_first_post_categories'}}</label>
@ -31,14 +29,12 @@
<div class="controls tracking-controls">
<label>{{d-icon "d-muted"}} {{i18n 'user.muted_categories'}}</label>
{{#if canSee}}
<a class="show-tracking" href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
{{/if}}
{{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n (if hideMutedTags 'user.muted_categories_instructions' 'user.muted_categories_instructions_dont_hide')}}</div>
{{#if canSee}}
<div class="controls">
<a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
</div>
{{/if}}
</div>
{{plugin-outlet name="user-preferences-categories" args=(hash model=model save=(action "save"))}}

View File

@ -10,7 +10,6 @@
<div class="instructions">{{i18n 'user.muted_users_instructions'}}</div>
</div>
{{plugin-outlet name="user-preferences-notifications" args=(hash model=model save=(action "save"))}}
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
<div class="control-group save-button">

View File

@ -7,6 +7,7 @@
tags=model.tag_names
everyTag=true
allowCreate=true
filterPlaceholder="tagging.groups.tags_placeholder"
unlimitedTagCount=true}}
</section>

View File

@ -52,32 +52,32 @@
<div class="row">
<div class="full-width">
<div id='list-area'>
{{conditional-loading-spinner condition=loading}}
{{#unless loading}}
{{#if list.topics}}
{{#discovery-topics-list model=list refresh=(action "refresh")}}
{{bulk-select-button selected=selected action=(action "refresh")}}
{{#unless loading}}
{{#if list.topics}}
{{#discovery-topics-list model=list refresh=(action "refresh")}}
{{bulk-select-button selected=selected action=(action "refresh")}}
{{topic-list topics=list.topics
canBulkSelect=canBulkSelect
toggleBulkSelect=(action "toggleBulkSelect")
bulkSelectEnabled=bulkSelectEnabled
selected=selected
showPosters=true
order=order
ascending=ascending
changeSort=(action "changeSort")}}
{{topic-list topics=list.topics
canBulkSelect=canBulkSelect
toggleBulkSelect=(action "toggleBulkSelect")
bulkSelectEnabled=bulkSelectEnabled
selected=selected
showPosters=true
order=order
ascending=ascending
changeSort=(action "changeSort")}}
{{/discovery-topics-list}}
{{else}}
<footer class='topic-list-bottom'>
<h3>
{{footerMessage}}{{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}}.
</h3>
</footer>
{{/if}}
{{/unless}}
{{/discovery-topics-list}}
{{else}}
<footer class='topic-list-bottom'>
<h3>
{{footerMessage}}{{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}}.
</h3>
</footer>
{{/if}}
{{/unless}}
{{conditional-loading-spinner condition=list.loadingMore}}
</div>
</div>
</div>

View File

@ -1,144 +1,99 @@
{{#discourse-topic multiSelect=multiSelect enteredAt=enteredAt topic=model hasScrolled=hasScrolled}}
{{#if model}}
{{add-category-tag-classes category=model.category tags=model.tags}}
<div class="container">
{{discourse-banner user=currentUser banner=site.banner overlay=hasScrolled hide=model.errorLoading}}
</div>
{{/if}}
{{#if showSharedDraftControls}}
{{shared-draft-controls topic=model}}
{{/if}}
{{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}}
{{#if model.postStream.loaded}}
{{#if model.postStream.firstPostPresent}}
{{#topic-title cancelled=(action "cancelEditingTopic") save=(action "finishedEditingTopic") model=model}}
{{#if editingTopic}}
<div class="edit-topic-title">
{{#if model.isPrivateMessage}}
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
{{/if}}
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
{{#if showCategoryChooser}}
{{category-chooser
class="small"
value=(unbound buffered.category_id)
onSelectAny=(action "topicCategoryChanged")}}
{{/if}}
{{#if canEditTags}}
{{mini-tag-chooser filterable=true tags=buffered.tags categoryId=buffered.category_id}}
{{/if}}
{{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
<div class="edit-controls">
{{d-button action=(action "finishedEditingTopic") class="btn-primary submit-edit" icon="check"}}
{{d-button action=(action "cancelEditingTopic") class="btn-default cancel-edit" icon="times"}}
{{#if canRemoveTopicFeaturedLink}}
<a href {{action "removeFeaturedLink"}} class="remove-featured-link" title="{{i18n "composer.remove_featured_link"}}">
{{d-icon "times-circle"}}
{{featuredLinkDomain}}
</a>
{{/if}}
</div>
</div>
{{else}}
<h1 data-topic-id="{{unbound model.id}}">
{{#unless model.is_warning}}
{{#if siteSettings.enable_personal_messages}}
<a href={{pmPath}}>
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
</a>
{{else}}
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
{{/if}}
{{/unless}}
{{#if model.details.loaded}}
{{topic-status topic=model}}
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
{{{model.fancyTitle}}}
</a>
{{/if}}
{{#if model.details.can_edit}}
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{d-icon "pencil-alt"}}</a>
{{/if}}
</h1>
{{topic-category topic=model class="topic-category"}}
{{/if}}
{{/topic-title}}
{{#if model.view_hidden}}
{{topic-join-group-notice model=model action=(action "joinGroup")}}
{{else}}
{{#if model}}
{{add-category-tag-classes category=model.category tags=model.tags}}
<div class="container">
{{discourse-banner user=currentUser banner=site.banner overlay=hasScrolled hide=model.errorLoading}}
</div>
{{/if}}
{{#if showSharedDraftControls}}
{{shared-draft-controls topic=model}}
{{/if}}
<div class="container posts">
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
{{partial "selected-posts"}}
</div>
{{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}}
{{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}}
{{#if info.renderTimeline}}
{{#if info.renderAdminMenuButton}}
{{topic-admin-menu-button
topic=model
fixed="true"
toggleMultiSelect=(action "toggleMultiSelect")
hideMultiSelect=(action "hideMultiSelect")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
showFeatureTopic=(route-action "showFeatureTopic")
showChangeTimestamp=(route-action "showChangeTimestamp")
resetBumpDate=(action "resetBumpDate")
convertToPublicTopic=(action "convertToPublicTopic")
convertToPrivateMessage=(action "convertToPrivateMessage")}}
{{#if model.postStream.loaded}}
{{#if model.postStream.firstPostPresent}}
{{#topic-title cancelled=(action "cancelEditingTopic") save=(action "finishedEditingTopic") model=model}}
{{#if editingTopic}}
<div class="edit-topic-title">
{{#if model.isPrivateMessage}}
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
{{/if}}
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
{{#if showCategoryChooser}}
{{category-chooser
class="small"
value=(unbound buffered.category_id)
onSelectAny=(action "topicCategoryChanged")}}
{{/if}}
{{#if canEditTags}}
{{mini-tag-chooser filterable=true tags=buffered.tags categoryId=buffered.category_id}}
{{/if}}
{{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
<div class="edit-controls">
{{d-button action=(action "finishedEditingTopic") class="btn-primary submit-edit" icon="check"}}
{{d-button action=(action "cancelEditingTopic") class="btn-default cancel-edit" icon="times"}}
{{#if canRemoveTopicFeaturedLink}}
<a href {{action "removeFeaturedLink"}} class="remove-featured-link" title="{{i18n "composer.remove_featured_link"}}">
{{d-icon "times-circle"}}
{{featuredLinkDomain}}
</a>
{{/if}}
</div>
</div>
{{else}}
<h1 data-topic-id="{{unbound model.id}}">
{{#unless model.is_warning}}
{{#if siteSettings.enable_personal_messages}}
{{#if model.isPrivateMessage}}
<a href={{pmPath}} title="{{i18n 'topic_statuses.personal_message.title'}}" aria-label={{i18n 'user.messages.inbox'}}>
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
</a>
{{/if}}
{{else}}
{{#if model.isPrivateMessage}}
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
{{/if}}
{{/if}}
{{/unless}}
{{#if model.details.loaded}}
{{topic-status topic=model}}
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
{{{model.fancyTitle}}}
</a>
{{/if}}
{{#if model.details.can_edit}}
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{d-icon "pencil-alt"}}</a>
{{/if}}
</h1>
{{topic-category topic=model class="topic-category"}}
{{/if}}
{{/topic-title}}
{{/if}}
{{topic-timeline
topic=model
notificationLevel=model.details.notification_level
prevEvent=info.prevEvent
fullscreen=info.topicProgressExpanded
enteredIndex=enteredIndex
loading=model.postStream.loading
jumpToPost=(action "jumpToPost")
jumpTop=(action "jumpTop")
jumpBottom=(action "jumpBottom")
jumpToPostPrompt=(action "jumpToPostPrompt")
jumpToIndex=(action "jumpToIndex")
replyToPost=(action "replyToPost")
toggleMultiSelect=(action "toggleMultiSelect")
hideMultiSelect=(action "hideMultiSelect")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
showFeatureTopic=(route-action "showFeatureTopic")
showChangeTimestamp=(route-action "showChangeTimestamp")
resetBumpDate=(action "resetBumpDate")
convertToPublicTopic=(action "convertToPublicTopic")
convertToPrivateMessage=(action "convertToPrivateMessage")}}
{{else}}
{{#topic-progress
prevEvent=info.prevEvent
topic=model
expanded=info.topicProgressExpanded
jumpToPost=(action "jumpToPost")}}
<div class="container posts">
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
{{partial "selected-posts"}}
</div>
{{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}}
{{#if info.renderTimeline}}
{{#if info.renderAdminMenuButton}}
{{topic-admin-menu-button
topic=model
openUpwards="true"
rightSide="true"
fixed="true"
toggleMultiSelect=(action "toggleMultiSelect")
hideMultiSelect=(action "hideMultiSelect")
deleteTopic=(action "deleteTopic")
@ -153,210 +108,265 @@
convertToPublicTopic=(action "convertToPublicTopic")
convertToPrivateMessage=(action "convertToPrivateMessage")}}
{{/if}}
{{/topic-progress}}
{{/if}}
{{/topic-navigation}}
<div class="row">
<section class="topic-area" id="topic" data-topic-id="{{unbound model.id}}">
{{topic-timeline
topic=model
notificationLevel=model.details.notification_level
prevEvent=info.prevEvent
fullscreen=info.topicProgressExpanded
enteredIndex=enteredIndex
loading=model.postStream.loading
jumpToPost=(action "jumpToPost")
jumpTop=(action "jumpTop")
jumpBottom=(action "jumpBottom")
jumpToPostPrompt=(action "jumpToPostPrompt")
jumpToIndex=(action "jumpToIndex")
replyToPost=(action "replyToPost")
toggleMultiSelect=(action "toggleMultiSelect")
hideMultiSelect=(action "hideMultiSelect")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
showFeatureTopic=(route-action "showFeatureTopic")
showChangeTimestamp=(route-action "showChangeTimestamp")
resetBumpDate=(action "resetBumpDate")
convertToPublicTopic=(action "convertToPublicTopic")
convertToPrivateMessage=(action "convertToPrivateMessage")}}
{{else}}
{{#topic-progress
prevEvent=info.prevEvent
topic=model
expanded=info.topicProgressExpanded
jumpToPost=(action "jumpToPost")}}
{{#if info.renderAdminMenuButton}}
{{topic-admin-menu-button
topic=model
openUpwards="true"
rightSide="true"
toggleMultiSelect=(action "toggleMultiSelect")
hideMultiSelect=(action "hideMultiSelect")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
showFeatureTopic=(route-action "showFeatureTopic")
showChangeTimestamp=(route-action "showChangeTimestamp")
resetBumpDate=(action "resetBumpDate")
convertToPublicTopic=(action "convertToPublicTopic")
convertToPrivateMessage=(action "convertToPrivateMessage")}}
{{/if}}
{{/topic-progress}}
{{/if}}
{{/topic-navigation}}
<div class="posts-wrapper">
{{conditional-loading-spinner condition=model.postStream.loadingAbove}}
<div class="row">
<section class="topic-area" id="topic" data-topic-id="{{unbound model.id}}">
{{plugin-outlet name="topic-above-posts" args=(hash model=model)}}
<div class="posts-wrapper">
{{conditional-loading-spinner condition=model.postStream.loadingAbove}}
{{#unless model.postStream.loadingFilter}}
{{scrolling-post-stream
posts=postsToRender
canCreatePost=model.details.can_create_post
multiSelect=multiSelect
selectedPostsCount=selectedPostsCount
selectedQuery=selectedQuery
gaps=model.postStream.gaps
showFlags=(action "showPostFlags")
editPost=(action "editPost")
showHistory=(route-action "showHistory")
showLogin=(route-action "showLogin")
showRawEmail=(route-action "showRawEmail")
deletePost=(action "deletePost")
recoverPost=(action "recoverPost")
expandHidden=(action "expandHidden")
newTopicAction=(action "replyAsNewTopic")
toggleBookmark=(action "toggleBookmark")
togglePostType=(action "togglePostType")
rebakePost=(action "rebakePost")
changePostOwner=(action "changePostOwner")
grantBadge=(action "grantBadge")
addNotice=(action "addNotice")
removeNotice=(action "removeNotice")
lockPost=(action "lockPost")
unlockPost=(action "unlockPost")
unhidePost=(action "unhidePost")
replyToPost=(action "replyToPost")
toggleWiki=(action "toggleWiki")
toggleSummary=(action "toggleSummary")
removeAllowedUser=(action "removeAllowedUser")
removeAllowedGroup=(action "removeAllowedGroup")
topVisibleChanged=(action "topVisibleChanged")
currentPostChanged=(action "currentPostChanged")
currentPostScrolled=(action "currentPostScrolled")
bottomVisibleChanged=(action "bottomVisibleChanged")
togglePostSelection=(action "togglePostSelection")
selectReplies=(action "selectReplies")
selectBelow=(action "selectBelow")
fillGapBefore=(action "fillGapBefore")
fillGapAfter=(action "fillGapAfter")
showInvite=(route-action "showInvite")}}
{{/unless}}
{{plugin-outlet name="topic-above-posts" args=(hash model=model)}}
{{conditional-loading-spinner condition=model.postStream.loadingBelow}}
</div>
<div id="topic-bottom"></div>
{{#unless model.postStream.loadingFilter}}
{{scrolling-post-stream
posts=postsToRender
canCreatePost=model.details.can_create_post
multiSelect=multiSelect
selectedPostsCount=selectedPostsCount
selectedQuery=selectedQuery
gaps=model.postStream.gaps
showFlags=(action "showPostFlags")
editPost=(action "editPost")
showHistory=(route-action "showHistory")
showLogin=(route-action "showLogin")
showRawEmail=(route-action "showRawEmail")
deletePost=(action "deletePost")
recoverPost=(action "recoverPost")
expandHidden=(action "expandHidden")
newTopicAction=(action "replyAsNewTopic")
toggleBookmark=(action "toggleBookmark")
togglePostType=(action "togglePostType")
rebakePost=(action "rebakePost")
changePostOwner=(action "changePostOwner")
grantBadge=(action "grantBadge")
addNotice=(action "addNotice")
removeNotice=(action "removeNotice")
lockPost=(action "lockPost")
unlockPost=(action "unlockPost")
unhidePost=(action "unhidePost")
replyToPost=(action "replyToPost")
toggleWiki=(action "toggleWiki")
toggleSummary=(action "toggleSummary")
removeAllowedUser=(action "removeAllowedUser")
removeAllowedGroup=(action "removeAllowedGroup")
topVisibleChanged=(action "topVisibleChanged")
currentPostChanged=(action "currentPostChanged")
currentPostScrolled=(action "currentPostScrolled")
bottomVisibleChanged=(action "bottomVisibleChanged")
togglePostSelection=(action "togglePostSelection")
selectReplies=(action "selectReplies")
selectBelow=(action "selectBelow")
fillGapBefore=(action "fillGapBefore")
fillGapAfter=(action "fillGapAfter")
showInvite=(route-action "showInvite")}}
{{/unless}}
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
{{#if loadedAllPosts}}
{{conditional-loading-spinner condition=model.postStream.loadingBelow}}
</div>
<div id="topic-bottom"></div>
{{#if model.pending_posts}}
<div class='pending-posts'>
{{#each model.pending_posts as |pending|}}
<div class='reviewable-item'>
<div class='reviewable-meta-data'>
<span class='reviewable-type'>
{{i18n "review.awaiting_approval"}}
</span>
<span class='created-at'>
{{age-with-tooltip pending.created_at}}
</span>
</div>
<div class='post-contents-wrapper'>
{{reviewable-created-by user=currentUser tagName=''}}
<div class='post-contents'>
{{reviewable-created-by-name user=currentUser tagName=''}}
<div class='post-body'>{{cook-text pending.raw}}</div>
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
{{#if loadedAllPosts}}
{{#if model.pending_posts}}
<div class='pending-posts'>
{{#each model.pending_posts as |pending|}}
<div class='reviewable-item'>
<div class='reviewable-meta-data'>
<span class='reviewable-type'>
{{i18n "review.awaiting_approval"}}
</span>
<span class='created-at'>
{{age-with-tooltip pending.created_at}}
</span>
</div>
<div class='post-contents-wrapper'>
{{reviewable-created-by user=currentUser tagName=''}}
<div class='post-contents'>
{{reviewable-created-by-name user=currentUser tagName=''}}
<div class='post-body'>{{cook-text pending.raw}}</div>
</div>
</div>
<div class='reviewable-actions'>
{{d-button
class="btn-danger"
label="review.delete"
icon="trash-alt"
action=(action "deletePending" pending) }}
</div>
</div>
<div class='reviewable-actions'>
{{d-button
class="btn-danger"
label="review.delete"
icon="trash-alt"
action=(action "deletePending" pending) }}
</div>
</div>
{{/each}}
</div>
{{/if}}
{{#if model.queued_posts_count}}
<div class="has-pending-posts">
<div>
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
{{/each}}
</div>
{{/if}}
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
{{i18n "review.view_pending"}}
{{/link-to}}
</div>
{{/if}}
{{#if model.queued_posts_count}}
<div class="has-pending-posts">
<div>
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
</div>
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
{{i18n "review.view_pending"}}
{{/link-to}}
</div>
{{/if}}
{{#if model.private_topic_timer.execute_at}}
{{topic-timer-info
topicClosed=model.closed
statusType=model.private_topic_timer.status_type
executeAt=model.private_topic_timer.execute_at
duration=model.private_topic_timer.duration
removeTopicTimer=(action "removeTopicTimer" model.private_topic_timer.status_type "private_topic_timer")}}
{{/if}}
{{#if model.private_topic_timer.execute_at}}
{{topic-timer-info
topicClosed=model.closed
statusType=model.private_topic_timer.status_type
executeAt=model.private_topic_timer.execute_at
duration=model.private_topic_timer.duration}}
{{/if}}
statusType=model.topic_timer.status_type
executeAt=model.topic_timer.execute_at
basedOnLastPost=model.topic_timer.based_on_last_post
duration=model.topic_timer.duration
categoryId=model.topic_timer.category_id
removeTopicTimer=(action "removeTopicTimer" model.topic_timer.status_type "topic_timer")}}
{{topic-timer-info
topicClosed=model.closed
statusType=model.topic_timer.status_type
executeAt=model.topic_timer.execute_at
basedOnLastPost=model.topic_timer.based_on_last_post
duration=model.topic_timer.duration
categoryId=model.topic_timer.category_id}}
{{#if session.showSignupCta}}
{{! replace "Log In to Reply" with the infobox }}
{{signup-cta}}
{{else}}
{{#if currentUser}}
{{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}}
{{topic-footer-buttons
topic=model
toggleMultiSelect=(action "toggleMultiSelect")
hideMultiSelect=(action "hideMultiSelect")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
showFeatureTopic=(route-action "showFeatureTopic")
showChangeTimestamp=(route-action "showChangeTimestamp")
resetBumpDate=(action "resetBumpDate")
convertToPublicTopic=(action "convertToPublicTopic")
convertToPrivateMessage=(action "convertToPrivateMessage")
toggleBookmark=(action "toggleBookmark")
showFlagTopic=(route-action "showFlagTopic")
toggleArchiveMessage=(action "toggleArchiveMessage")
editFirstPost=(action "editFirstPost")
deferTopic=(action "deferTopic")
replyToPost=(action "replyToPost")}}
{{#if session.showSignupCta}}
{{! replace "Log In to Reply" with the infobox }}
{{signup-cta}}
{{else}}
<div id="topic-footer-buttons">
{{d-button icon="reply" class="btn-primary pull-right" action=(route-action "showLogin") label="topic.reply.title"}}
{{#if currentUser}}
{{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}}
{{topic-footer-buttons
topic=model
toggleMultiSelect=(action "toggleMultiSelect")
hideMultiSelect=(action "hideMultiSelect")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
toggleArchived=(action "toggleArchived")
toggleVisibility=(action "toggleVisibility")
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
showFeatureTopic=(route-action "showFeatureTopic")
showChangeTimestamp=(route-action "showChangeTimestamp")
resetBumpDate=(action "resetBumpDate")
convertToPublicTopic=(action "convertToPublicTopic")
convertToPrivateMessage=(action "convertToPrivateMessage")
toggleBookmark=(action "toggleBookmark")
showFlagTopic=(route-action "showFlagTopic")
toggleArchiveMessage=(action "toggleArchiveMessage")
editFirstPost=(action "editFirstPost")
deferTopic=(action "deferTopic")
replyToPost=(action "replyToPost")}}
{{else}}
<div id="topic-footer-buttons">
{{d-button icon="reply" class="btn-primary pull-right" action=(route-action "showLogin") label="topic.reply.title"}}
</div>
{{/if}}
{{/if}}
{{#if showSelectedPostsAtBottom}}
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
{{partial "selected-posts"}}
</div>
{{/if}}
{{/if}}
{{#if showSelectedPostsAtBottom}}
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
{{partial "selected-posts"}}
{{plugin-outlet name="topic-above-suggested" args=(hash model=model)}}
<div class="{{if model.relatedMessages.length 'related-messages-wrapper'}} {{if model.suggestedTopics.length 'suggested-topics-wrapper'}}">
{{#if model.relatedMessages.length}}
{{related-messages topic=model}}
{{/if}}
{{#if model.suggestedTopics.length}}
{{suggested-topics topic=model}}
{{/if}}
</div>
{{/if}}
{{/conditional-loading-spinner}}
{{plugin-outlet name="topic-above-suggested" args=(hash model=model)}}
<div class="{{if model.relatedMessages.length 'related-messages-wrapper'}} {{if model.suggestedTopics.length 'suggested-topics-wrapper'}}">
{{#if model.relatedMessages.length}}
{{related-messages topic=model}}
{{/if}}
{{#if model.suggestedTopics.length}}
{{suggested-topics topic=model}}
{{/if}}
</div>
{{/if}}
{{/conditional-loading-spinner}}
</section>
</div>
</section>
</div>
{{else}}
<div class="container">
{{#conditional-loading-spinner condition=noErrorYet}}
{{#if model.notFoundHtml}}
<div class="not-found">{{{model.notFoundHtml}}}</div>
{{else}}
<div class="topic-error">
<div>{{model.message}}</div>
{{#if model.noRetry}}
{{#unless currentUser}}
{{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}}
{{/unless}}
{{else}}
{{d-button action=(action "retryLoading") class="btn-primary topic-retry" icon="sync" label="errors.buttons.again"}}
{{/if}}
</div>
{{conditional-loading-spinner condition=retrying}}
{{/if}}
{{/conditional-loading-spinner}}
</div>
{{/if}}
</div>
{{else}}
<div class="container">
{{#conditional-loading-spinner condition=noErrorYet}}
{{#if model.notFoundHtml}}
<div class="not-found">{{{model.notFoundHtml}}}</div>
{{else}}
<div class="topic-error">
<div>{{model.message}}</div>
{{#if model.noRetry}}
{{#unless currentUser}}
{{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}}
{{/unless}}
{{else}}
{{d-button action=(action "retryLoading") class="btn-primary topic-retry" icon="sync" label="errors.buttons.again"}}
{{/if}}
</div>
{{conditional-loading-spinner condition=retrying}}
{{/if}}
{{/conditional-loading-spinner}}
</div>
{{/if}}
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
{{#if embedQuoteButton}}
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
{{#if embedQuoteButton}}
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
{{/if}}
{{/if}}
{{/discourse-topic}}

View File

@ -16,9 +16,7 @@ export default class Connector {
if (opts.templateName) {
deprecated(
`Using a 'templateName' for a connector is deprecated. Use 'component' instead [${
opts.templateName
}]`
`Using a 'templateName' for a connector is deprecated. Use 'component' instead [${opts.templateName}]`
);
}

View File

@ -0,0 +1,21 @@
import { createWidgetFrom } from "discourse/widgets/widget";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { formatUsername } from "discourse/lib/utilities";
import { iconNode } from "discourse-common/lib/icon-library";
createWidgetFrom(DefaultNotificationItem, "custom-notification-item", {
notificationTitle(notificationName, data) {
return data.title ? I18n.t(data.title) : "";
},
text(notificationName, data) {
const username = formatUsername(data.display_username);
const description = this.description(data);
return I18n.t(data.message, { description, username });
},
icon(notificationName, data) {
return iconNode(`notification.${data.message}`);
}
});

View File

@ -0,0 +1,158 @@
import { wantsNewWindow } from "discourse/lib/intercept-click";
import RawHtml from "discourse/widgets/raw-html";
import { createWidget } from "discourse/widgets/widget";
import DiscourseURL from "discourse/lib/url";
import { h } from "virtual-dom";
import { emojiUnescape } from "discourse/lib/text";
import {
postUrl,
escapeExpression,
formatUsername
} from "discourse/lib/utilities";
import { setTransientHeader } from "discourse/lib/ajax";
import { userPath } from "discourse/lib/url";
import { iconNode } from "discourse-common/lib/icon-library";
export const DefaultNotificationItem = createWidget(
"default-notification-item",
{
tagName: "li",
buildClasses(attrs) {
const classNames = [];
if (attrs.get("read")) {
classNames.push("read");
}
if (attrs.is_warning) {
classNames.push("is-warning");
}
return classNames;
},
url(data) {
const attrs = this.attrs;
const badgeId = data.badge_id;
if (badgeId) {
let badgeSlug = data.badge_slug;
if (!badgeSlug) {
const badgeName = data.badge_name;
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, "-").toLowerCase();
}
let username = data.username;
username = username ? "?username=" + username.toLowerCase() : "";
return Discourse.getURL(
"/badges/" + badgeId + "/" + badgeSlug + username
);
}
const topicId = attrs.topic_id;
if (topicId) {
return postUrl(attrs.slug, topicId, attrs.post_number);
}
if (data.group_id) {
return userPath(data.username + "/messages/group/" + data.group_name);
}
},
description(data) {
const badgeName = data.badge_name;
if (badgeName) {
return escapeExpression(badgeName);
}
if (this.attrs.fancy_title) {
if (this.attrs.topic_id) {
return `<span data-topic-id="${this.attrs.topic_id}">${this.attrs.fancy_title}</span>`;
}
return this.attrs.fancy_title;
}
const description = data.topic_title;
return Ember.isEmpty(description) ? "" : escapeExpression(description);
},
text(notificationName, data) {
const username = formatUsername(data.display_username);
const description = this.description(data);
return I18n.t(`notifications.${notificationName}`, {
description,
username
});
},
icon(notificationName) {
return iconNode(`notification.${notificationName}`);
},
notificationTitle(notificationName) {
if (notificationName) {
return I18n.t(`notifications.titles.${notificationName}`);
} else {
return "";
}
},
html(attrs) {
const notificationType = attrs.notification_type;
const lookup = this.site.get("notificationLookup");
const notificationName = lookup[notificationType];
let { data } = attrs;
let text = emojiUnescape(this.text(notificationName, data));
let icon = this.icon(notificationName, data);
const title = this.notificationTitle(notificationName, data);
// We can use a `<p>` tag here once other languages have fixed their HTML
// translations.
let html = new RawHtml({ html: `<div>${text}</div>` });
let contents = [icon, html];
const href = this.url(data);
return href
? h(
"a",
{ attributes: { href, title, "data-auto-route": true } },
contents
)
: contents;
},
click(e) {
this.attrs.set("read", true);
const id = this.attrs.id;
setTransientHeader("Discourse-Clear-Notifications", id);
if (document && document.cookie) {
let path = Discourse.BaseUri || "/";
document.cookie = `cn=${id}; path=${path}; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
}
if (wantsNewWindow(e)) {
return;
}
e.preventDefault();
this.sendWidgetEvent("linkClicked");
DiscourseURL.routeTo(this.url(this.attrs.data), {
afterRouteComplete: () => {
if (!this.attrs.data.revision_number) {
return;
}
this.appEvents.trigger(
"post:show-revision",
this.attrs.post_number,
this.attrs.data.revision_number
);
}
});
}
}
);

View File

@ -0,0 +1,10 @@
import RawHtml from "discourse/widgets/raw-html";
import renderTags from "discourse/lib/render-tags";
// Right now it's RawHTML. Eventually it should emit nodes
export default class DiscourseTags extends RawHtml {
constructor(attrs) {
attrs.html = renderTags(attrs.topic, attrs);
super(attrs);
}
}

View File

@ -0,0 +1,18 @@
import { createWidgetFrom } from "discourse/widgets/widget";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
createWidgetFrom(
DefaultNotificationItem,
"group-message-summary-notification-item",
{
text(notificationName, data) {
const count = data.inbox_count;
const group_name = data.group_name;
return I18n.t("notifications.group_message_summary", {
count,
group_name
});
}
}
);

View File

@ -59,7 +59,7 @@ export default createWidget("hamburger-menu", {
if (currentUser.admin) {
links.push({
href: "/admin/site_settings/category/required",
href: "/admin/site_settings",
icon: "cog",
label: "admin.site_settings.title",
className: "settings-link"

View File

@ -399,9 +399,7 @@ export default createWidget("header", {
var params = "";
if (context) {
params = `?context=${context.type}&context_id=${
context.id
}&skip_context=${this.state.skipSearchContext}`;
params = `?context=${context.type}&context_id=${context.id}&skip_context=${this.state.skipSearchContext}`;
}
const currentPath = this.register

View File

@ -0,0 +1,13 @@
import { createWidgetFrom } from "discourse/widgets/widget";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { userPath } from "discourse/lib/url";
createWidgetFrom(
DefaultNotificationItem,
"invitee-accepted-notification-item",
{
url(data) {
return userPath(data.display_username);
}
}
);

View File

@ -0,0 +1,31 @@
import { createWidgetFrom } from "discourse/widgets/widget";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { escapeExpression } from "discourse/lib/utilities";
import { userPath } from "discourse/lib/url";
createWidgetFrom(
DefaultNotificationItem,
"liked-consolidated-notification-item",
{
url(data) {
return userPath(
`${this.attrs.username ||
this.currentUser
.username}/notifications/likes-received?acting_username=${
data.display_username
}`
);
},
description(data) {
const description = I18n.t(
"notifications.liked_consolidated_description",
{
count: parseInt(data.count)
}
);
return Ember.isEmpty(description) ? "" : escapeExpression(description);
}
}
);

View File

@ -0,0 +1,32 @@
import { createWidgetFrom } from "discourse/widgets/widget";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { formatUsername } from "discourse/lib/utilities";
createWidgetFrom(DefaultNotificationItem, "liked-notification-item", {
text(notificationName, data) {
const username = formatUsername(data.display_username);
const description = this.description(data);
if (data.count > 1) {
const count = data.count - 2;
const username2 = formatUsername(data.username2);
if (count === 0) {
return I18n.t("notifications.liked_2", {
description,
username,
username2
});
} else {
return I18n.t("notifications.liked_many", {
description,
username,
username2,
count
});
}
}
return I18n.t("notifications.liked", { description, username });
}
});

View File

@ -1,215 +0,0 @@
import { wantsNewWindow } from "discourse/lib/intercept-click";
import RawHtml from "discourse/widgets/raw-html";
import { createWidget } from "discourse/widgets/widget";
import DiscourseURL from "discourse/lib/url";
import { h } from "virtual-dom";
import { emojiUnescape } from "discourse/lib/text";
import {
postUrl,
escapeExpression,
formatUsername
} from "discourse/lib/utilities";
import { setTransientHeader } from "discourse/lib/ajax";
import { userPath } from "discourse/lib/url";
import { iconNode } from "discourse-common/lib/icon-library";
createWidget("notification-item", {
tagName: "li",
buildClasses(attrs) {
const classNames = [];
if (attrs.get("read")) {
classNames.push("read");
}
if (attrs.is_warning) {
classNames.push("is-warning");
}
return classNames;
},
url() {
const attrs = this.attrs;
const data = attrs.data;
const notificationTypes = this.site.notification_types;
const badgeId = data.badge_id;
if (badgeId) {
let badgeSlug = data.badge_slug;
if (!badgeSlug) {
const badgeName = data.badge_name;
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, "-").toLowerCase();
}
let username = data.username;
username = username ? "?username=" + username.toLowerCase() : "";
return Discourse.getURL(
"/badges/" + badgeId + "/" + badgeSlug + username
);
}
const topicId = attrs.topic_id;
if (topicId) {
return postUrl(attrs.slug, topicId, attrs.post_number);
}
if (attrs.notification_type === notificationTypes.invitee_accepted) {
return userPath(data.display_username);
}
if (attrs.notification_type === notificationTypes.liked_consolidated) {
return userPath(
`${this.attrs.username ||
this.currentUser
.username}/notifications/likes-received?acting_username=${
data.display_username
}`
);
}
if (data.group_id) {
return userPath(data.username + "/messages/group/" + data.group_name);
}
},
description() {
const data = this.attrs.data;
const badgeName = data.badge_name;
if (badgeName) {
return escapeExpression(badgeName);
}
if (this.attrs.fancy_title) {
if (this.attrs.topic_id) {
return `<span data-topic-id="${this.attrs.topic_id}">${
this.attrs.fancy_title
}</span>`;
}
return this.attrs.fancy_title;
}
let title;
if (
this.attrs.notification_type ===
this.site.notification_types.liked_consolidated
) {
title = I18n.t("notifications.liked_consolidated_description", {
count: parseInt(data.count)
});
} else {
title = data.topic_title;
}
return Ember.isEmpty(title) ? "" : escapeExpression(title);
},
text(notificationType, notName) {
const { attrs } = this;
const data = attrs.data;
const scope =
notName === "custom" ? data.message : `notifications.${notName}`;
const notificationTypes = this.site.notification_types;
if (notificationType === notificationTypes.group_message_summary) {
const count = data.inbox_count;
const group_name = data.group_name;
return I18n.t(scope, { count, group_name });
}
const username = formatUsername(data.display_username);
const description = this.description();
if (notificationType === notificationTypes.liked && data.count > 1) {
const count = data.count - 2;
const username2 = formatUsername(data.username2);
if (count === 0) {
return I18n.t("notifications.liked_2", {
description,
username,
username2
});
} else {
return I18n.t("notifications.liked_many", {
description,
username,
username2,
count
});
}
}
return I18n.t(scope, { description, username });
},
html(attrs) {
const notificationType = attrs.notification_type;
const lookup = this.site.get("notificationLookup");
const notificationName = lookup[notificationType];
let { data } = attrs;
let infoKey =
notificationName === "custom" ? data.message : notificationName;
let text = emojiUnescape(this.text(notificationType, notificationName));
let icon = iconNode(`notification.${infoKey}`);
let title;
if (notificationName) {
if (notificationName === "custom") {
title = data.title ? I18n.t(data.title) : "";
} else {
title = I18n.t(`notifications.titles.${notificationName}`);
}
} else {
title = "";
}
// We can use a `<p>` tag here once other languages have fixed their HTML
// translations.
let html = new RawHtml({ html: `<div>${text}</div>` });
let contents = [icon, html];
const href = this.url();
return href
? h(
"a",
{ attributes: { href, title, "data-auto-route": true } },
contents
)
: contents;
},
click(e) {
this.attrs.set("read", true);
const id = this.attrs.id;
setTransientHeader("Discourse-Clear-Notifications", id);
if (document && document.cookie) {
let path = Discourse.BaseUri || "/";
document.cookie = `cn=${id}; path=${path}; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
}
if (wantsNewWindow(e)) {
return;
}
e.preventDefault();
this.sendWidgetEvent("linkClicked");
DiscourseURL.routeTo(this.url(), {
afterRouteComplete: () => {
if (!this.attrs.data.revision_number) {
return;
}
this.appEvents.trigger(
"post:show-revision",
this.attrs.post_number,
this.attrs.data.revision_number
);
}
});
}
});

View File

@ -61,9 +61,15 @@ function likeCount(attrs) {
? "post.has_likes_title_only_you"
: "post.has_likes_title_you"
: "post.has_likes_title";
const icon = attrs.yours ? "d-liked" : "";
let icon = attrs.yours ? "d-liked" : "";
let addContainer = attrs.yours;
const additionalClass = attrs.yours ? "my-likes" : "regular-likes";
if (!attrs.showLike) {
icon = attrs.yours ? "d-liked" : "d-unliked";
addContainer = true;
}
return {
action: "toggleWhoLiked",
title,
@ -71,7 +77,7 @@ function likeCount(attrs) {
contents: count,
icon,
iconRight: true,
addContainer: attrs.yours,
addContainer,
titleOptions: { count: attrs.liked ? count - 1 : count }
};
}

View File

@ -1,6 +1,7 @@
import PostCooked from "discourse/widgets/post-cooked";
import DecoratorHelper from "discourse/widgets/decorator-helper";
import { createWidget, applyDecorators } from "discourse/widgets/widget";
import RawHtml from "discourse/widgets/raw-html";
import { iconNode } from "discourse-common/lib/icon-library";
import { transformBasicPost } from "discourse/lib/transform-post";
import { postTransformCallbacks } from "discourse/widgets/post-stream";
@ -459,20 +460,23 @@ createWidget("post-notice", {
let text, icon;
if (attrs.noticeType === "custom") {
icon = "user-shield";
text = attrs.noticeMessage;
text = new RawHtml({ html: `<div>${attrs.noticeMessage}</div>` });
} else if (attrs.noticeType === "new_user") {
icon = "hands-helping";
text = I18n.t("post.notice.new_user", { user });
text = h("p", I18n.t("post.notice.new_user", { user }));
} else if (attrs.noticeType === "returning_user") {
icon = "far-smile";
const distance = (new Date() - new Date(attrs.noticeTime)) / 1000;
text = I18n.t("post.notice.returning_user", {
user,
time: durationTiny(distance, { addAgo: true })
});
text = h(
"p",
I18n.t("post.notice.returning_user", {
user,
time: durationTiny(distance, { addAgo: true })
})
);
}
return h("p", [iconNode(icon), text]);
return [iconNode(icon), text];
}
});
@ -528,7 +532,9 @@ createWidget("post-article", {
},
html(attrs, state) {
const rows = [h("a.tabLoc", { attributes: { href: "" } })];
const rows = [
h("a.tabLoc", { attributes: { href: "", "aria-hidden": true } })
];
if (state.repliesAbove.length) {
const replies = state.repliesAbove.map(p => {
return this.attach("embedded-post", p, {

View File

@ -73,9 +73,7 @@ createSearchResult({
return h(
"span",
{
className: `tag-${tag} discourse-tag ${
Discourse.SiteSettings.tag_style
}`
className: `tag-${tag} discourse-tag ${Discourse.SiteSettings.tag_style}`
},
tag
);
@ -149,13 +147,27 @@ createSearchResult({
linkField: "url",
builder(result, term) {
const topic = result.topic;
const link = h("span.topic", [
const firstLine = [
this.attach("topic-status", { topic, disableActions: true }),
h("span.topic-title", new Highlighted(topic.get("fancyTitle"), term)),
h("span.topic-title", new Highlighted(topic.fancyTitle, term))
];
const secondLine = [
this.attach("category-link", {
category: topic.get("category"),
category: topic.category,
link: false
})
];
if (Discourse.SiteSettings.tagging_enabled) {
secondLine.push(
this.attach("discourse-tags", { topic, tagName: "span" })
);
}
const link = h("span.topic", [
h("div.first-line", firstLine),
h("div.second-line", secondLine)
]);
return postResult.call(this, result, link, term);

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