Merge branch 'main' into 0-reveal-password

This commit is contained in:
Jarek Radosz 2022-12-13 13:34:22 +01:00
commit 02c2f64ae1
No known key found for this signature in database
GPG Key ID: 62D0FBAE5BF9B953
608 changed files with 11382 additions and 9942 deletions

View File

@ -176,14 +176,14 @@ jobs:
- name: Plugin System Tests
if: matrix.build_type == 'system' && matrix.target == 'plugins'
run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system
run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system --format documentation --profile
- name: Upload failed system test screenshots
uses: actions/upload-artifact@v3
if: matrix.build_type == 'system' && failure()
with:
name: failed-system-test-screenshots
path: tmp/screenshots/*.png
path: tmp/capybara/*.png
- name: Check Annotations
if: matrix.build_type == 'annotations'

View File

@ -97,7 +97,6 @@ GEM
xpath (~> 3.2)
cbor (0.5.9.6)
certified (1.0.0)
childprocess (4.1.0)
chunky_png (1.4.0)
coderay (1.1.3)
colored2 (3.1.2)
@ -115,7 +114,7 @@ GEM
debug_inspector (1.1.0)
diff-lcs (1.5.0)
diffy (3.4.2)
digest (3.1.0)
digest (3.1.1)
discourse-fonts (0.0.9)
discourse-seed-fu (2.3.12)
activerecord (>= 3.1)
@ -128,7 +127,7 @@ GEM
regexp_parser (~> 2.2)
email_reply_trimmer (0.1.13)
erubi (1.11.0)
excon (0.94.0)
excon (0.95.0)
execjs (2.8.1)
exifr (1.3.10)
fabrication (2.30.0)
@ -168,7 +167,7 @@ GEM
image_size (3.2.0)
in_threads (1.6.0)
jmespath (1.6.2)
json (2.6.2)
json (2.6.3)
json-schema (3.0.0)
addressable (>= 2.8)
json_schemer (0.2.23)
@ -230,21 +229,21 @@ GEM
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.1.3)
net-protocol (0.2.1)
timeout
net-smtp (0.3.3)
net-protocol
nio4r (2.5.8)
nokogiri (1.13.9)
nokogiri (1.13.10)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.13.9-aarch64-linux)
nokogiri (1.13.10-aarch64-linux)
racc (~> 1.4)
nokogiri (1.13.9-arm64-darwin)
nokogiri (1.13.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.13.9-x86_64-darwin)
nokogiri (1.13.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.13.9-x86_64-linux)
nokogiri (1.13.10-x86_64-linux)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@ -300,11 +299,11 @@ GEM
pry (>= 0.13, < 0.15)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.0)
public_suffix (5.0.1)
puma (6.0.0)
nio4r (~> 2.0)
r2 (0.2.7)
racc (1.6.0)
racc (1.6.1)
rack (2.2.4)
rack-mini-profiler (3.0.0)
rack (>= 1.2.0)
@ -342,7 +341,7 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.7.1)
redis (4.8.0)
redis-namespace (1.9.0)
redis (>= 4)
regexp_parser (2.6.1)
@ -367,7 +366,7 @@ GEM
rspec-html-matchers (0.10.0)
nokogiri (~> 1)
rspec (>= 3.0.0.a)
rspec-mocks (3.12.0)
rspec-mocks (3.12.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.1)
@ -386,7 +385,7 @@ GEM
json-schema (>= 2.2, < 4.0)
railties (>= 3.1, < 7.1)
rspec-core (>= 2.14)
rubocop (1.39.0)
rubocop (1.40.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.2.1)
@ -422,8 +421,7 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
selenium-webdriver (4.6.1)
childprocess (>= 0.5, < 5.0)
selenium-webdriver (4.7.1)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@ -451,10 +449,10 @@ GEM
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.23)
test-prof (1.0.11)
test-prof (1.1.0)
thor (1.2.1)
tilt (2.0.11)
timeout (0.3.0)
timeout (0.3.1)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
uglifier (4.2.0)
@ -467,7 +465,7 @@ GEM
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.16.0)
uri (0.11.0)
uri (0.12.0)
uri_template (0.7.0)
version_gem (1.1.1)
webdrivers (5.2.0)

View File

@ -0,0 +1,29 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
tagName: "",
@discourseComputed("type")
penaltyField(penaltyType) {
if (penaltyType === "suspend") {
return "can_be_suspended";
} else if (penaltyType === "silence") {
return "can_be_silenced";
}
},
@action
selectUserId(userId, event) {
if (!this.selectedUserIds) {
return;
}
if (event.target.checked) {
this.selectedUserIds.pushObject(userId);
} else {
this.selectedUserIds.removeObject(userId);
}
},
});

View File

@ -1,47 +0,0 @@
import Component from "@ember/component";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["hook-event"],
typeName: alias("type.name"),
@discourseComputed("typeName")
name(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
},
@discourseComputed("typeName")
details(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
},
@discourseComputed("model.[]", "typeName")
eventTypeExists(eventTypes, typeName) {
return eventTypes.any((event) => event.name === typeName);
},
@discourseComputed("eventTypeExists")
enabled: {
get(eventTypeExists) {
return eventTypeExists;
},
set(value, eventTypeExists) {
const type = this.type;
const model = this.model;
// add an association when not exists
if (value !== eventTypeExists) {
if (value) {
model.addObject(type);
} else {
model.removeObjects(
model.filter((eventType) => eventType.name === type.name)
);
}
}
return value;
},
},
});

View File

@ -1,110 +0,0 @@
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
import Component from "@ember/component";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Component.extend({
tagName: "li",
expandDetails: null,
expandDetailsRequestKey: "request",
expandDetailsResponseKey: "response",
dialog: service(),
@discourseComputed("model.status")
statusColorClasses(status) {
if (!status) {
return "";
}
if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
},
@discourseComputed("model.created_at")
createdAt(createdAt) {
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
},
@discourseComputed("model.duration")
completion(duration) {
const seconds = Math.floor(duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
},
@discourseComputed("expandDetails")
expandRequestIcon(expandDetails) {
return expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
},
@discourseComputed("expandDetails")
expandResponseIcon(expandDetails) {
return expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
},
actions: {
redeliver() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.events.redeliver_confirm"),
didConfirm: () => {
return ajax(
`/admin/api/web_hooks/${this.get(
"model.web_hook_id"
)}/events/${this.get("model.id")}/redeliver`,
{ type: "POST" }
)
.then((json) => {
this.set("model", json.web_hook_event);
})
.catch(popupAjaxError);
},
});
},
toggleRequest() {
const expandDetailsKey = this.expandDetailsRequestKey;
if (this.expandDetails !== expandDetailsKey) {
let headers = Object.assign(
{
"Request URL": this.get("model.request_url"),
"Request method": "POST",
},
ensureJSON(this.get("model.headers"))
);
this.setProperties({
headers: plainJSON(headers),
body: prettyJSON(this.get("model.payload")),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.payload"),
});
} else {
this.set("expandDetails", null);
}
},
toggleResponse() {
const expandDetailsKey = this.expandDetailsResponseKey;
if (this.expandDetails !== expandDetailsKey) {
this.setProperties({
headers: plainJSON(this.get("model.response_headers")),
body: this.get("model.response_body"),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.body"),
});
} else {
this.set("expandDetails", null);
}
},
},
});

View File

@ -1,39 +0,0 @@
import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library";
import { htmlSafe } from "@ember/template";
export default Component.extend({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["far-circle", "times-circle", "circle", "circle"],
circleIcon: null,
deliveryStatus: null,
@discourseComputed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find((s) => s.id === lastDeliveryStatus);
},
@discourseComputed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},
@discourseComputed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},
didReceiveAttrs() {
this._super(...arguments);
this.set(
"circleIcon",
htmlSafe(iconHTML(this.icon, { class: this.class }))
);
this.set(
"deliveryStatus",
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
);
},
});

View File

@ -0,0 +1,11 @@
<label class="hook-event">
<Input
@type="checkbox"
@checked={{this.enabled}}
name="event-choice"
/>
{{this.name}}
<p>{{this.details}}</p>
</label>

View File

@ -0,0 +1,41 @@
import Component from "@glimmer/component";
import I18n from "I18n";
export default class WebhookEventChooser extends Component {
get name() {
return I18n.t(`admin.web_hooks.${this.args.type.name}_event.name`);
}
get details() {
return I18n.t(`admin.web_hooks.${this.args.type.name}_event.details`);
}
get eventTypeExists() {
return this.args.eventTypes.any(
(event) => event.name === this.args.type.name
);
}
get enabled() {
return this.eventTypeExists;
}
set enabled(value) {
const eventTypes = this.args.eventTypes;
// add an association when not exists
if (value === this.eventTypeExists) {
return value;
}
if (value) {
eventTypes.addObject(this.args.type);
} else {
eventTypes.removeObjects(
eventTypes.filter((eventType) => eventType.name === this.args.type.name)
);
}
return value;
}
}

View File

@ -0,0 +1,39 @@
<li>
<div class="col first status">
<span class={{this.statusColorClasses}}>{{@event.status}}</span>
</div>
<div class="col event-id">{{@event.id}}</div>
<div class="col timestamp">{{this.createdAt}}</div>
<div class="col completion">{{this.completion}}</div>
<div class="col actions">
<DButton
@icon={{this.expandRequestIcon}}
@action={{this.toggleRequest}}
@label="admin.web_hooks.events.request"
/>
<DButton
@icon={{this.expandResponseIcon}}
@action={{this.toggleResponse}}
@label="admin.web_hooks.events.response"
/>
<DButton
@icon="sync"
@action={{this.redeliver}}
@label="admin.web_hooks.events.redeliver"
/>
</div>
{{#if this.expandDetails}}
<div class="details">
<h3>{{i18n "admin.web_hooks.events.headers"}}</h3>
<pre><code>{{this.headers}}</code></pre>
<h3>{{this.bodyLabel}}</h3>
<pre><code>{{this.body}}</code></pre>
</div>
{{/if}}
</li>

View File

@ -0,0 +1,102 @@
import Component from "@glimmer/component";
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
export default class WebhookEvent extends Component {
@service dialog;
@tracked body = "";
@tracked bodyLabel = "";
@tracked expandDetails = null;
@tracked headers = "";
expandDetailsRequestKey = "request";
expandDetailsResponseKey = "response";
get statusColorClasses() {
const { status } = this.args.event;
if (!status) {
return "";
}
if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
}
get createdAt() {
return moment(this.args.event.created_at).format("YYYY-MM-DD HH:mm:ss");
}
get completion() {
const seconds = Math.floor(this.args.event.duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
}
get expandRequestIcon() {
return this.expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
}
get expandResponseIcon() {
return this.expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
}
@action
redeliver() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.events.redeliver_confirm"),
didConfirm: async () => {
try {
const json = await ajax(
`/admin/api/web_hooks/${this.args.event.web_hook_id}/events/${this.args.event.id}/redeliver`,
{ type: "POST" }
);
this.args.event.setProperties(json.web_hook_event);
} catch (e) {
popupAjaxError(e);
}
},
});
}
@action
toggleRequest() {
if (this.expandDetails !== this.expandDetailsRequestKey) {
const headers = {
"Request URL": this.args.event.request_url,
"Request method": "POST",
...ensureJSON(this.args.event.headers),
};
this.headers = plainJSON(headers);
this.body = prettyJSON(this.args.event.payload);
this.expandDetails = this.expandDetailsRequestKey;
this.bodyLabel = I18n.t("admin.web_hooks.events.payload");
} else {
this.expandDetails = null;
}
}
@action
toggleResponse() {
if (this.expandDetails !== this.expandDetailsResponseKey) {
this.headers = plainJSON(this.args.event.response_headers);
this.body = this.args.event.response_body;
this.expandDetails = this.expandDetailsResponseKey;
this.bodyLabel = I18n.t("admin.web_hooks.events.body");
} else {
this.expandDetails = null;
}
}
}

View File

@ -1,16 +1,17 @@
<div class="web-hook-direction">
<LinkTo @route="adminWebHooks" class="btn">
{{d-icon "list"}} {{i18n "admin.web_hooks.events.go_list"}}
</LinkTo>
<DButton @icon="paper-plane" @label="admin.web_hooks.events.ping" @action={{action "ping"}} @disabled={{this.pingDisabled}} />
<LinkTo @route="adminWebHooks.show" @model={{this.model.extras.web_hook_id}} class="btn">
{{d-icon "far-edit"}} {{i18n "admin.web_hooks.events.go_details"}}
</LinkTo>
</div>
<div class="web-hook-events-listing"
{{did-insert this.subscribe}}
{{will-destroy this.unsubscribe}}
>
<DButton
@icon="paper-plane"
@label="admin.web_hooks.events.ping"
@action={{this.ping}}
@disabled={{not this.pingEnabled}}
class="webhook-events__ping-button"
/>
<div class="web-hook-events-listing">
{{#if this.model}}
<LoadMore @selector=".web-hook-events li" @action={{action "loadMore"}}>
{{#if this.events}}
<LoadMore @selector=".web-hook-events li" @action={{this.loadMore}}>
<div class="web-hook-events content-list">
<div class="heading-container">
<div class="col heading first status">{{i18n "admin.web_hooks.events.status"}}</div>
@ -18,20 +19,22 @@
<div class="col heading timestamp">{{i18n "admin.web_hooks.events.timestamp"}}</div>
<div class="col heading completion">{{i18n "admin.web_hooks.events.completion"}}</div>
<div class="col heading actions">{{i18n "admin.web_hooks.events.actions"}}</div>
<div class="clearfix"></div>
</div>
{{#if this.hasIncoming}}
<a href tabindex="0" {{on "click" this.showInserted}} class="alert alert-info clickable">
<CountI18n @key="admin.web_hooks.events.incoming" @count={{this.incomingCount}} />
</a>
{{/if}}
<ul>
{{#each this.model as |webHookEvent|}}
<AdminWebHookEvent @model={{webHookEvent}} />
{{#each this.events as |event|}}
<WebhookEvent @event={{event}} />
{{/each}}
</ul>
</div>
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
<ConditionalLoadingSpinner @condition={{this.events.loadingMore}} />
</LoadMore>
{{else}}
<p>{{i18n "admin.web_hooks.events.none"}}</p>

View File

@ -0,0 +1,89 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { gt, readOnly } from "@ember/object/computed";
import { bind } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default class WebhookEvents extends Component {
@service messageBus;
@service store;
@tracked pingEnabled = true;
@tracked events = [];
@tracked incomingEventIds = [];
@readOnly("incomingEventIds.length") incomingCount;
@gt("incomingCount", 0) hasIncoming;
constructor() {
super(...arguments);
this.loadEvents();
}
async loadEvents() {
this.events = await this.store.findAll(
"web-hook-event",
this.args.webhookId
);
}
@bind
subscribe() {
const channel = `/web_hook_events/${this.args.webhookId}`;
this.messageBus.subscribe(channel, this._addIncoming);
}
@bind
unsubscribe() {
this.messageBus.unsubscribe("/web_hook_events/*", this._addIncoming);
}
@bind
_addIncoming(data) {
if (data.event_type === "ping") {
this.pingEnabled = true;
}
if (!this.incomingEventIds.includes(data.web_hook_event_id)) {
this.incomingEventIds.pushObject(data.web_hook_event_id);
}
}
@action
async showInserted(event) {
event?.preventDefault();
const path = `/admin/api/web_hooks/${this.args.webhookId}/events/bulk`;
const data = await ajax(path, {
data: { ids: this.incomingEventIds },
});
const objects = data.map((webhookEvent) =>
this.store.createRecord("web-hook-event", webhookEvent)
);
this.events.unshiftObjects(objects);
this.incomingEventIds = [];
}
@action
loadMore() {
this.events.loadMore();
}
@action
async ping() {
this.pingEnabled = false;
try {
await ajax(`/admin/api/web_hooks/${this.args.webhookId}/ping`, {
type: "POST",
});
} catch (error) {
this.pingEnabled = true;
popupAjaxError(error);
}
}
}

View File

@ -0,0 +1,2 @@
{{d-icon this.iconName (hash class=this.iconClass)}}
{{this.deliveryStatus}}

View File

@ -0,0 +1,24 @@
import Component from "@glimmer/component";
import I18n from "I18n";
export default class WebhookStatus extends Component {
iconNames = ["far-circle", "times-circle", "circle", "circle"];
iconClasses = ["text-muted", "text-danger", "text-successful", "text-muted"];
get status() {
const lastStatus = this.args.webhook.last_delivery_status;
return this.args.deliveryStatuses.find((s) => s.id === lastStatus);
}
get deliveryStatus() {
return I18n.t(`admin.web_hooks.delivery_status.${this.status.name}`);
}
get iconName() {
return this.iconNames[this.status.id - 1];
}
get iconClass() {
return this.iconClasses[this.status.id - 1];
}
}

View File

@ -0,0 +1,101 @@
import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { action } from "@ember/object";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminWebHooks: controller(),
dialog: service(),
eventTypes: alias("adminWebHooks.eventTypes"),
defaultEventTypes: alias("adminWebHooks.defaultEventTypes"),
contentTypes: alias("adminWebHooks.contentTypes"),
@discourseComputed
showTagsFilter() {
return this.siteSettings.tagging_enabled;
},
@discourseComputed("model.isSaving", "saved", "saveButtonDisabled")
savingStatus(isSaving, saved, saveButtonDisabled) {
if (isSaving) {
return I18n.t("saving");
} else if (!saveButtonDisabled && saved) {
return I18n.t("saved");
}
// Use side effect of validation to clear saved text
this.set("saved", false);
return "";
},
@discourseComputed("model.isNew")
saveButtonText(isNew) {
return isNew
? I18n.t("admin.web_hooks.create")
: I18n.t("admin.web_hooks.save");
},
@discourseComputed("model.secret")
secretValidation(secret) {
if (!isEmpty(secret)) {
if (secret.includes(" ")) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.secret_invalid"),
});
}
if (secret.length < 12) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.secret_too_short"),
});
}
}
},
@discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]")
eventTypeValidation(isWildcard, eventTypes) {
if (!isWildcard && isEmpty(eventTypes)) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.event_type_missing"),
});
}
},
@discourseComputed(
"model.isSaving",
"secretValidation",
"eventTypeValidation",
"model.payload_url"
)
saveButtonDisabled(
isSaving,
secretValidation,
eventTypeValidation,
payloadUrl
) {
return isSaving
? false
: secretValidation || eventTypeValidation || isEmpty(payloadUrl);
},
@action
async save() {
this.set("saved", false);
try {
await this.model.save();
this.set("saved", true);
this.adminWebHooks.model.addObject(this.model);
this.transitionToRoute("adminWebHooks.show", this.model);
} catch (e) {
popupAjaxError(e);
}
},
});

View File

@ -0,0 +1,36 @@
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
export default Controller.extend({
adminWebHooks: controller(),
dialog: service(),
contentTypes: alias("adminWebHooks.contentTypes"),
defaultEventTypes: alias("adminWebHooks.defaultEventTypes"),
deliveryStatuses: alias("adminWebHooks.deliveryStatuses"),
eventTypes: alias("adminWebHooks.eventTypes"),
model: alias("adminWebHooks.model"),
@action
destroy(webhook) {
return this.dialog.deleteConfirm({
message: I18n.t("admin.web_hooks.delete_confirm"),
didConfirm: async () => {
try {
await webhook.destroyRecord();
this.model.removeObject(webhook);
} catch (e) {
popupAjaxError(e);
}
},
});
},
@action
loadMore() {
this.model.loadMore();
},
});

View File

@ -1,83 +0,0 @@
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
pingDisabled: false,
incomingCount: alias("incomingEventIds.length"),
init() {
this._super(...arguments);
this.incomingEventIds = [];
},
@discourseComputed("incomingCount")
hasIncoming(incomingCount) {
return incomingCount > 0;
},
subscribe() {
this.messageBus.subscribe(
`/web_hook_events/${this.get("model.extras.web_hook_id")}`,
(data) => {
if (data.event_type === "ping") {
this.set("pingDisabled", false);
}
this._addIncoming(data.web_hook_event_id);
}
);
},
unsubscribe() {
this.messageBus.unsubscribe("/web_hook_events/*");
},
_addIncoming(eventId) {
const incomingEventIds = this.incomingEventIds;
if (!incomingEventIds.includes(eventId)) {
incomingEventIds.pushObject(eventId);
}
},
@action
showInserted(event) {
event?.preventDefault();
const webHookId = this.get("model.extras.web_hook_id");
ajax(`/admin/api/web_hooks/${webHookId}/events/bulk`, {
type: "GET",
data: { ids: this.incomingEventIds },
}).then((data) => {
const objects = data.map((webHookEvent) =>
this.store.createRecord("web-hook-event", webHookEvent)
);
this.model.unshiftObjects(objects);
this.set("incomingEventIds", []);
});
},
actions: {
loadMore() {
this.model.loadMore();
},
ping() {
this.set("pingDisabled", true);
ajax(
`/admin/api/web_hooks/${this.get("model.extras.web_hook_id")}/ping`,
{
type: "POST",
}
).catch((error) => {
this.set("pingDisabled", false);
popupAjaxError(error);
});
},
},
});

View File

@ -1,121 +1,32 @@
import Controller, { inject as controller } from "@ember/controller";
import EmberObject from "@ember/object";
import { action } from "@ember/object";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminWebHooks: controller(),
dialog: service(),
eventTypes: alias("adminWebHooks.eventTypes"),
defaultEventTypes: alias("adminWebHooks.defaultEventTypes"),
contentTypes: alias("adminWebHooks.contentTypes"),
router: service(),
@discourseComputed
showTagsFilter() {
return this.siteSettings.tagging_enabled;
@action
edit() {
return this.router.transitionTo("adminWebHooks.edit", this.model);
},
@discourseComputed("model.isSaving", "saved", "saveButtonDisabled")
savingStatus(isSaving, saved, saveButtonDisabled) {
if (isSaving) {
return I18n.t("saving");
} else if (!saveButtonDisabled && saved) {
return I18n.t("saved");
}
// Use side effect of validation to clear saved text
this.set("saved", false);
return "";
},
@discourseComputed("model.isNew")
saveButtonText(isNew) {
return isNew
? I18n.t("admin.web_hooks.create")
: I18n.t("admin.web_hooks.save");
},
@discourseComputed("model.secret")
secretValidation(secret) {
if (!isEmpty(secret)) {
if (secret.includes(" ")) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.secret_invalid"),
});
}
if (secret.length < 12) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.secret_too_short"),
});
}
}
},
@discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]")
eventTypeValidation(isWildcard, eventTypes) {
if (!isWildcard && isEmpty(eventTypes)) {
return EmberObject.create({
failed: true,
reason: I18n.t("admin.web_hooks.event_type_missing"),
});
}
},
@discourseComputed(
"model.isSaving",
"secretValidation",
"eventTypeValidation",
"model.payload_url"
)
saveButtonDisabled(
isSaving,
secretValidation,
eventTypeValidation,
payloadUrl
) {
return isSaving
? false
: secretValidation || eventTypeValidation || isEmpty(payloadUrl);
},
actions: {
save() {
this.set("saved", false);
const model = this.model;
const isNew = model.get("isNew");
return model
.save()
.then(() => {
this.set("saved", true);
this.adminWebHooks.get("model").addObject(model);
if (isNew) {
this.transitionToRoute("adminWebHooks.show", model.get("id"));
}
})
.catch(popupAjaxError);
},
destroy() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.delete_confirm"),
didConfirm: () => {
this.model
.destroyRecord()
.then(() => {
this.adminWebHooks.get("model").removeObject(this.model);
this.transitionToRoute("adminWebHooks");
})
.catch(popupAjaxError);
},
});
},
@action
destroy() {
return this.dialog.deleteConfirm({
message: I18n.t("admin.web_hooks.delete_confirm"),
didConfirm: async () => {
try {
await this.model.destroyRecord();
this.adminWebHooks.model.removeObject(this.model);
this.transitionToRoute("adminWebHooks");
} catch (e) {
popupAjaxError(e);
}
},
});
},
});

View File

@ -1,29 +1,3 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default Controller.extend({
dialog: service(),
@action
destroy(webhook) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.delete_confirm"),
didConfirm: () => {
webhook
.destroyRecord()
.then(() => {
this.model.removeObject(webhook);
})
.catch(popupAjaxError);
},
});
},
@action
loadMore() {
this.model.loadMore();
},
});
export default Controller.extend({});

View File

@ -2,30 +2,33 @@ import Controller from "@ember/controller";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import messageBus from "message-bus-client";
import { bind } from "discourse-common/utils/decorators";
export default Controller.extend(ModalFunctionality, {
message: I18n.t("admin.user.merging_user"),
onShow() {
messageBus.subscribe("/merge_user", (data) => {
if (data.merged) {
if (/^\/admin\/users\/list\//.test(location)) {
DiscourseURL.redirectTo(location);
} else {
DiscourseURL.redirectTo(
`/admin/users/${data.user.id}/${data.user.username}`
);
}
} else if (data.message) {
this.set("message", data.message);
} else if (data.failed) {
this.set("message", I18n.t("admin.user.merge_failed"));
}
});
this.messageBus.subscribe("/merge_user", this.onMessage);
},
onClose() {
this.messageBus.unsubscribe("/merge_user");
this.messageBus.unsubscribe("/merge_user", this.onMessage);
},
@bind
onMessage(data) {
if (data.merged) {
if (/^\/admin\/users\/list\//.test(location)) {
DiscourseURL.redirectTo(location);
} else {
DiscourseURL.redirectTo(
`/admin/users/${data.user.id}/${data.user.username}`
);
}
} else if (data.message) {
this.set("message", data.message);
} else if (data.failed) {
this.set("message", I18n.t("admin.user.merge_failed"));
}
},
});

View File

@ -9,7 +9,11 @@ export default Controller.extend(PenaltyController, {
onShow() {
this.resetModal();
this.setProperties({ silenceUntil: null, silencing: false });
this.setProperties({
silenceUntil: null,
silencing: false,
otherUserIds: [],
});
},
finishedSetup() {
@ -36,6 +40,7 @@ export default Controller.extend(PenaltyController, {
post_id: this.postId,
post_action: this.postAction,
post_edit: this.postEdit,
other_user_ids: this.otherUserIds,
});
}).finally(() => this.set("silencing", false));
},

View File

@ -9,7 +9,11 @@ export default Controller.extend(PenaltyController, {
onShow() {
this.resetModal();
this.setProperties({ suspendUntil: null, suspending: false });
this.setProperties({
suspendUntil: null,
suspending: false,
otherUserIds: [],
});
},
finishedSetup() {
@ -28,7 +32,6 @@ export default Controller.extend(PenaltyController, {
}
this.set("suspending", true);
this.penalize(() => {
return this.user.suspend({
suspend_until: this.suspendUntil,
@ -37,6 +40,7 @@ export default Controller.extend(PenaltyController, {
post_id: this.postId,
post_action: this.postAction,
post_edit: this.postEdit,
other_user_ids: this.otherUserIds,
});
}).finally(() => this.set("suspending", false));
},

View File

@ -15,7 +15,7 @@ export default RestModel.extend({
groupsFilterInName: null,
@discourseComputed("wildcard_web_hook")
webHookType: {
webhookType: {
get(wildcard) {
return wildcard ? "wildcard" : "individual";
},

View File

@ -1,14 +1,14 @@
import Backup from "admin/models/backup";
import Route from "@ember/routing/route";
import { bind } from "discourse-common/utils/decorators";
export default Route.extend({
activate() {
this.messageBus.subscribe("/admin/backups", (backups) =>
this.controller.set(
"model",
backups.map((backup) => Backup.create(backup))
)
);
this.messageBus.subscribe("/admin/backups", this.onMessage);
},
deactivate() {
this.messageBus.unsubscribe("/admin/backups", this.onMessage);
},
model() {
@ -17,7 +17,11 @@ export default Route.extend({
);
},
deactivate() {
this.messageBus.unsubscribe("/admin/backups");
@bind
onMessage(backups) {
this.controller.set(
"model",
backups.map((backup) => Backup.create(backup))
);
},
});

View File

@ -10,46 +10,19 @@ import { extractError } from "discourse/lib/ajax-error";
import getURL from "discourse-common/lib/get-url";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
const LOG_CHANNEL = "/admin/backups/logs";
export default DiscourseRoute.extend({
dialog: service(),
activate() {
this.messageBus.subscribe(LOG_CHANNEL, (log) => {
if (log.message === "[STARTED]") {
User.currentProp("hideReadOnlyAlert", true);
this.controllerFor("adminBackups").set(
"model.isOperationRunning",
true
);
this.controllerFor("adminBackupsLogs").get("logs").clear();
} else if (log.message === "[FAILED]") {
this.controllerFor("adminBackups").set(
"model.isOperationRunning",
false
);
this.dialog.alert(
I18n.t("admin.backups.operations.failed", {
operation: log.operation,
})
);
} else if (log.message === "[SUCCESS]") {
User.currentProp("hideReadOnlyAlert", false);
this.controllerFor("adminBackups").set(
"model.isOperationRunning",
false
);
if (log.operation === "restore") {
// redirect to homepage when the restore is done (session might be lost)
window.location = getURL("/");
}
} else {
this.controllerFor("adminBackupsLogs")
.get("logs")
.pushObject(EmberObject.create(log));
}
});
this.messageBus.subscribe(LOG_CHANNEL, this.onMessage);
},
deactivate() {
this.messageBus.unsubscribe(LOG_CHANNEL, this.onMessage);
},
model() {
@ -64,8 +37,31 @@ export default DiscourseRoute.extend({
);
},
deactivate() {
this.messageBus.unsubscribe(LOG_CHANNEL);
@bind
onMessage(log) {
if (log.message === "[STARTED]") {
User.currentProp("hideReadOnlyAlert", true);
this.controllerFor("adminBackups").set("model.isOperationRunning", true);
this.controllerFor("adminBackupsLogs").get("logs").clear();
} else if (log.message === "[FAILED]") {
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
this.dialog.alert(
I18n.t("admin.backups.operations.failed", {
operation: log.operation,
})
);
} else if (log.message === "[SUCCESS]") {
User.currentProp("hideReadOnlyAlert", false);
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
if (log.operation === "restore") {
// redirect to homepage when the restore is done (session might be lost)
window.location = getURL("/");
}
} else {
this.controllerFor("adminBackupsLogs")
.get("logs")
.pushObject(EmberObject.create(log));
}
},
actions: {

View File

@ -123,7 +123,7 @@ export default function () {
{ path: "/web_hooks", resetNamespace: true },
function () {
this.route("show", { path: "/:web_hook_id" });
this.route("showEvents", { path: "/:web_hook_id/events" });
this.route("edit", { path: "/:web_hook_id/edit" });
}
);
});

View File

@ -0,0 +1,28 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
serialize(model) {
return { web_hook_id: model.id || "new" };
},
model(params) {
if (params.web_hook_id === "new") {
return this.store.createRecord("web-hook");
}
return this.store.find("web-hook", params.web_hook_id);
},
setupController(controller, model) {
this._super(...arguments);
if (model.get("isNew")) {
model.set(
"web_hook_event_types",
this.controllerFor("adminWebHooks").defaultEventTypes
);
}
controller.set("saved", false);
},
});

View File

@ -1,21 +0,0 @@
import DiscourseRoute from "discourse/routes/discourse";
import { get } from "@ember/object";
export default DiscourseRoute.extend({
model(params) {
return this.store.findAll("web-hook-event", get(params, "web_hook_id"));
},
setupController(controller, model) {
controller.set("model", model);
controller.subscribe();
},
deactivate() {
this.controllerFor("adminWebHooks.showEvents").unsubscribe();
},
renderTemplate() {
this.render("admin/templates/web-hooks-show-events", { into: "adminApi" });
},
});

View File

@ -1,26 +1,7 @@
import DiscourseRoute from "discourse/routes/discourse";
import { get } from "@ember/object";
export default DiscourseRoute.extend({
serialize(model) {
return { web_hook_id: model.get("id") || "new" };
},
model(params) {
if (params.web_hook_id === "new") {
return this.store.createRecord("web-hook");
}
return this.store.find("web-hook", get(params, "web_hook_id"));
},
setupController(controller, model) {
if (model.get("isNew")) {
model.set("web_hook_event_types", controller.get("defaultEventTypes"));
}
model.set("category_ids", model.get("category_ids"));
model.set("tag_names", model.get("tag_names"));
model.set("group_ids", model.get("group_ids"));
controller.setProperties({ model, saved: false });
return this.store.find("web-hook", params.web_hook_id);
},
});

View File

@ -1,4 +1,5 @@
import Route from "@ember/routing/route";
export default Route.extend({
model() {
return this.store.findAll("web-hook");

View File

@ -0,0 +1,35 @@
<div class="penalty-similar-users">
<p class="alert alert-danger">
{{i18n "admin.user.other_matches" (hash count=this.user.similar_users_count username=this.user.username)}}
</p>
<table class="table">
<thead>
<tr>
<th></th>
<th>{{i18n "username"}}</th>
<th>{{i18n "last_seen"}}</th>
<th>{{i18n "admin.user.topics_entered"}}</th>
<th>{{i18n "admin.user.posts_read_count"}}</th>
<th>{{i18n "admin.user.time_read"}}</th>
<th>{{i18n "created"}}</th>
</tr>
</thead>
<tbody>
{{#each this.user.similar_users as |user|}}
<tr>
<td>
<Input @type="checkbox" disabled={{not (get user this.penaltyField)}} {{on "click" (action "selectUserId" user.id)}} />
</td>
<td>{{avatar user imageSize="small"}} {{user.username}}</td>
<td>{{format-duration user.last_seen_age}}</td>
<td>{{number user.topics_entered}}</td>
<td>{{number user.posts_read_count}}</td>
<td>{{format-duration user.time_read}}</td>
<td>{{format-duration user.created_at_age}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>

View File

@ -1,3 +0,0 @@
<Input id={{this.typeName}} @type="checkbox" name="event-choice" @checked={{this.enabled}} />
<label for={{this.typeName}}>{{this.name}}</label>
<p>{{this.details}}</p>

View File

@ -1,19 +0,0 @@
<div class="col first status">
<span class={{this.statusColorClasses}}>{{this.model.status}}</span>
</div>
<div class="col event-id">{{this.model.id}}</div>
<div class="col timestamp">{{this.createdAt}}</div>
<div class="col completion">{{this.completion}}</div>
<div class="col actions">
<DButton @icon={{this.expandRequestIcon}} @action={{action "toggleRequest"}} @label="admin.web_hooks.events.request" />
<DButton @icon={{this.expandResponseIcon}} @action={{action "toggleResponse"}} @label="admin.web_hooks.events.response" />
<DButton @icon="sync" @action={{action "redeliver"}} @label="admin.web_hooks.events.redeliver" />
</div>
{{#if this.expandDetails}}
<div class="details">
<h3>{{i18n "admin.web_hooks.events.headers"}}</h3>
<pre><code>{{this.headers}}</code></pre>
<h3>{{this.bodyLabel}}</h3>
<pre><code>{{this.body}}</code></pre>
</div>
{{/if}}

View File

@ -1 +0,0 @@
{{this.circleIcon}} {{this.deliveryStatus}}

View File

@ -173,11 +173,12 @@
{{/if}}
{{#unless this.model.component}}
<DSection @class="form-horizontal theme settings control-unit">
<DSection @class="form-horizontal theme settings control-unit theme-settings__color-scheme">
<div class="row setting">
<div class="setting-label">
{{i18n "admin.customize.theme.color_scheme"}}
</div>
<div class="setting-value">
<ColorPalettes @content={{this.colorSchemes}} @value={{this.colorSchemeId}} @icon="paint-brush" @options={{hash
filterable=true
@ -185,6 +186,7 @@
<div class="desc">{{i18n "admin.customize.theme.color_scheme_select"}}</div>
</div>
<div class="setting-controls">
{{#if this.colorSchemeChanged}}
<DButton @action={{action "changeScheme"}} @class="ok submit-edit" @icon="check" />

View File

@ -54,10 +54,8 @@
</td>
<td class="col last_match_at">
{{#if item.last_match_at}}
<div class="label">
{{i18n "admin.logs.last_match_at"}}
{{age-with-tooltip item.last_match_at}}
</div>
<div class="label">{{i18n "admin.logs.last_match_at"}}</div>
{{age-with-tooltip item.last_match_at}}
{{/if}}
</td>
<td class="col actions">

View File

@ -18,6 +18,10 @@
<PenaltyPostAction @postId={{this.postId}} @postAction={{this.postAction}} @postEdit={{this.postEdit}} />
{{/if}}
{{#if this.user.similar_users}}
<AdminPenaltySimilarUsers @type="silence" @user={{this.user}} @selectedUserIds={{this.otherUserIds}} />
{{/if}}
</ConditionalLoadingSpinner>
</DModalBody>

View File

@ -13,12 +13,15 @@
<FutureDateInput @class="suspend-until" @label="admin.user.suspend_duration" @clearable={{false}} @input={{this.suspendUntil}} @onChangeInput={{action (mut this.suspendUntil)}} />
</label>
</div>
<SuspensionDetails @reason={{this.reason}} @message={{this.message}} />
<SuspensionDetails @reason={{this.reason}} @message={{this.message}} />
{{#if this.postId}}
<PenaltyPostAction @postId={{this.postId}} @postAction={{this.postAction}} @postEdit={{this.postEdit}} />
{{/if}}
{{#if this.user.similar_users}}
<AdminPenaltySimilarUsers @type="suspend" @user={{this.user}} @selectedUserIds={{this.otherUserIds}} />
{{/if}}
{{else}}
<div class="cant-suspend">
{{i18n "admin.user.cant_suspend"}}

View File

@ -0,0 +1,122 @@
<LinkTo @route="adminWebHooks" class="go-back">
{{d-icon "arrow-left"}}
{{i18n "admin.web_hooks.go_back"}}
</LinkTo>
<div class="web-hook-container">
<p>{{i18n "admin.web_hooks.detailed_instruction"}}</p>
<form class="web-hook form-horizontal">
<div class="control-group">
<label for="payload-url">{{i18n "admin.web_hooks.payload_url"}}</label>
<TextField @name="payload-url" @value={{this.model.payload_url}} @placeholderKey="admin.web_hooks.payload_url_placeholder" />
<InputTip @validation={{this.urlValidation}} />
</div>
<div class="control-group">
<label for="content-type">{{i18n "admin.web_hooks.content_type"}}</label>
<ComboBox @content={{this.contentTypes}} @name="content-type" @value={{this.model.content_type}} @onChange={{action (mut this.model.content_type)}} />
</div>
<div class="control-group">
<label for="secret">{{i18n "admin.web_hooks.secret"}}</label>
<TextField @name="secret" @value={{this.model.secret}} @placeholderKey="admin.web_hooks.secret_placeholder" />
<InputTip @validation={{this.secretValidation}} />
</div>
<div class="control-group">
<label>{{i18n "admin.web_hooks.event_chooser"}}</label>
<label class="subscription-choice">
<RadioButton
@name="subscription-choice"
@value="individual"
@selection={{this.model.webhookType}}
/>
{{i18n "admin.web_hooks.individual_event"}}
<InputTip @validation={{this.eventTypeValidation}} />
</label>
{{#unless this.model.wildcard_web_hook}}
<div class="event-selector">
{{#each this.eventTypes as |type|}}
<WebhookEventChooser
@type={{type}}
@eventTypes={{this.model.web_hook_event_types}}
/>
{{/each}}
</div>
{{/unless}}
<label class="subscription-choice">
<RadioButton
@name="subscription-choice"
@value="wildcard"
@selection={{this.model.webhookType}}
/>
{{i18n "admin.web_hooks.wildcard_event"}}
</label>
</div>
<div class="filters control-group">
<div class="filter">
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.categories_filter"}}</label>
<CategorySelector @categories={{this.model.categories}} @onChange={{action (mut this.model.categories)}} />
<div class="instructions">{{i18n "admin.web_hooks.categories_filter_instructions"}}</div>
</div>
{{#if this.showTagsFilter}}
<div class="filter">
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.tags_filter"}}</label>
<TagChooser @tags={{this.model.tag_names}} @everyTag={{true}} @excludeSynonyms={{true}} />
<div class="instructions">{{i18n "admin.web_hooks.tags_filter_instructions"}}</div>
</div>
{{/if}}
<div class="filter">
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.groups_filter"}}</label>
<GroupSelector @groupNames={{this.model.groupsFilterInName}} @groupFinder={{this.model.groupFinder}} />
<div class="instructions">{{i18n "admin.web_hooks.groups_filter_instructions"}}</div>
</div>
</div>
<PluginOutlet @name="web-hook-fields" @tagName="span" @connectorTagName="div" @args={{hash model=this.model}} />
<label>
<Input @type="checkbox" name="verify_certificate" @checked={{this.model.verify_certificate}} />
{{i18n "admin.web_hooks.verify_certificate"}}
</label>
<div>
<label>
<Input @type="checkbox" name="active" @checked={{this.model.active}} />
{{i18n "admin.web_hooks.active"}}
</label>
{{#if this.model.active}}
<div class="instructions">{{i18n "admin.web_hooks.active_notice"}}</div>
{{/if}}
</div>
</form>
<div class="controls">
<DButton
@translatedLabel={{this.saveButtonText}}
@action={{action "save"}}
@disabled={{this.saveButtonDisabled}}
@class="btn-primary admin-webhooks__save-button"
/>
{{#if this.model.isNew}}
<LinkTo @route="adminWebHooks" class="btn btn-default">
{{i18n "cancel"}}
</LinkTo>
{{else}}
<LinkTo @route="adminWebHooks.show" @model={{this.model}} class="btn btn-default">
{{i18n "cancel"}}
</LinkTo>
{{/if}}
<span class="saving">{{this.savingStatus}}</span>
</div>
</div>

View File

@ -0,0 +1,70 @@
<div class="web-hooks-listing">
<p>{{i18n "admin.web_hooks.instruction"}}</p>
<div class="new-webhook">
<LinkTo
@route="adminWebHooks.edit"
@model="new"
class="btn btn-default admin-webhooks__new-button"
>
{{d-icon "plus"}}
{{i18n "admin.web_hooks.new"}}
</LinkTo>
</div>
{{#if this.model}}
<LoadMore @selector=".web-hooks tr" @action={{this.loadMore}}>
<table class="web-hooks grid">
<thead>
<tr>
<th>{{i18n "admin.web_hooks.delivery_status.title"}}</th>
<th>{{i18n "admin.web_hooks.payload_url"}}</th>
<th>{{i18n "admin.web_hooks.description_label"}}</th>
<th>{{i18n "admin.web_hooks.controls"}}</th>
</tr>
</thead>
<tbody>
{{#each this.model as |webhook|}}
<tr>
<td class="delivery-status">
<LinkTo @route="adminWebHooks.show" @model={{webhook}}>
<WebhookStatus
@deliveryStatuses={{this.deliveryStatuses}}
@webhook={{webhook}}
/>
</LinkTo>
</td>
<td class="payload-url">
<LinkTo @route="adminWebHooks.edit" @model={{webhook}}>
{{webhook.payload_url}}
</LinkTo>
</td>
<td class="description">{{webhook.description}}</td>
<td class="controls">
<LinkTo
@route="adminWebHooks.edit"
@model={{webhook}}
class="btn btn-default no-text"
title={{i18n "admin.web_hooks.edit"}}
>
{{d-icon "far-edit"}}
</LinkTo>
<DButton
@class="destroy btn-danger"
@action={{this.destroy}}
@actionParam={{webhook}}
@icon="times"
@title="delete"
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
</LoadMore>
{{else}}
<p>{{i18n "admin.web_hooks.none"}}</p>
{{/if}}
</div>

View File

@ -3,90 +3,32 @@
{{i18n "admin.web_hooks.go_back"}}
</LinkTo>
<div class="web-hook-container">
<p>{{i18n "admin.web_hooks.detailed_instruction"}}</p>
<form class="web-hook form-horizontal">
<div class="control-group">
<label for="payload-url">{{i18n "admin.web_hooks.payload_url"}}</label>
<TextField @name="payload-url" @value={{this.model.payload_url}} @placeholderKey="admin.web_hooks.payload_url_placeholder" />
<InputTip @validation={{this.urlValidation}} />
</div>
<div class="admin-webhooks__summary">
<h1>
{{this.model.payload_url}}
<div class="control-group">
<label for="content-type">{{i18n "admin.web_hooks.content_type"}}</label>
<ComboBox @content={{this.contentTypes}} @name="content-type" @value={{this.model.content_type}} @onChange={{action (mut this.model.content_type)}} />
</div>
<DButton
@action={{this.edit}}
@icon="far-edit"
title={{i18n "admin.web_hooks.edit"}}
class="btn no-text admin-webhooks__edit-button"
/>
<div class="control-group">
<label for="secret">{{i18n "admin.web_hooks.secret"}}</label>
<TextField @name="secret" @value={{this.model.secret}} @placeholderKey="admin.web_hooks.secret_placeholder" />
<InputTip @validation={{this.secretValidation}} />
</div>
<DButton
@action={{this.destroy}}
@icon="times"
@title="delete"
class="destroy btn-danger admin-webhooks__delete-button"
/>
</h1>
<div class="control-group">
<label>{{i18n "admin.web_hooks.event_chooser"}}</label>
<div>
<RadioButton @class="subscription-choice" @name="subscription-choice" @value="individual" @selection={{this.model.webHookType}} />
{{i18n "admin.web_hooks.individual_event"}}
<InputTip @validation={{this.eventTypeValidation}} />
</div>
{{#unless this.model.wildcard_web_hook}}
<div class="event-selector">
{{#each this.eventTypes as |type|}}
<AdminWebHookEventChooser @type={{type}} @model={{this.model.web_hook_event_types}} />
{{/each}}
</div>
{{/unless}}
<div>
<RadioButton @class="subscription-choice" @name="subscription-choice" @value="wildcard" @selection={{this.model.webHookType}} />
{{i18n "admin.web_hooks.wildcard_event"}}
</div>
</div>
<div>
<span class="admin-webhooks__description-label">
{{i18n "admin.web_hooks.description_label"}}:
</span>
<div class="filters control-group">
<div class="filter">
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.categories_filter"}}</label>
<CategorySelector @categories={{this.model.categories}} @onChange={{action (mut this.model.categories)}} />
<div class="instructions">{{i18n "admin.web_hooks.categories_filter_instructions"}}</div>
</div>
{{#if this.showTagsFilter}}
<div class="filter">
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.tags_filter"}}</label>
<TagChooser @tags={{this.model.tag_names}} @everyTag={{true}} @excludeSynonyms={{true}} />
<div class="instructions">{{i18n "admin.web_hooks.tags_filter_instructions"}}</div>
</div>
{{/if}}
<div class="filter">
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.groups_filter"}}</label>
<GroupSelector @groupNames={{this.model.groupsFilterInName}} @groupFinder={{this.model.groupFinder}} />
<div class="instructions">{{i18n "admin.web_hooks.groups_filter_instructions"}}</div>
</div>
</div>
<PluginOutlet @name="web-hook-fields" @tagName="span" @connectorTagName="div" @args={{hash model=this.model}} />
<div>
<Input @type="checkbox" name="verify_certificate" @checked={{this.model.verify_certificate}} /> {{i18n "admin.web_hooks.verify_certificate"}}
</div>
<div>
<div>
<Input @type="checkbox" name="active" @checked={{this.model.active}} /> {{i18n "admin.web_hooks.active"}}
</div>
{{#if this.model.active}}
<div class="instructions">{{i18n "admin.web_hooks.active_notice"}}</div>
{{/if}}
</div>
</form>
<div class="controls">
<DButton @class="btn-default" @translatedLabel={{this.saveButtonText}} @action={{action "save"}} @disabled={{this.saveButtonDisabled}} />
{{#unless this.model.isNew}}
<DButton @class="btn-danger" @label="admin.web_hooks.destroy" @action={{action "destroy"}} />
<LinkTo @route="adminWebHooks.showEvents" @model={{this.model.id}} class="btn">
{{i18n "admin.web_hooks.events.go_events"}}
</LinkTo>
{{/unless}}
<span class="saving">{{this.savingStatus}}</span>
{{this.model.description}}
</div>
</div>
<WebhookEvents @webhookId={{this.model.id}} />

View File

@ -1,38 +1 @@
<div class="web-hooks-listing">
<p>{{i18n "admin.web_hooks.instruction"}}</p>
<div class="new-webhook">
<LinkTo @route="adminWebHooks.show" @model="new" class="btn btn-default">
{{d-icon "plus"}} {{i18n "admin.web_hooks.new"}}
</LinkTo>
</div>
{{#if this.model}}
<LoadMore @selector=".web-hooks tr" @action={{action "loadMore"}}>
<table class="web-hooks grid">
<thead>
<tr>
<th>{{i18n "admin.web_hooks.delivery_status.title"}}</th>
<th>{{i18n "admin.web_hooks.payload_url"}}</th>
<th>{{i18n "admin.web_hooks.description"}}</th>
<th>{{i18n "admin.web_hooks.controls"}}</th>
</tr>
</thead>
<tbody>
{{#each this.model as |webHook|}}
<tr>
<td class="delivery-status"><LinkTo @route="adminWebHooks.showEvents" @model={{webHook.id}}><AdminWebHookStatus @deliveryStatuses={{this.deliveryStatuses}} @model={{webHook}} /></LinkTo></td>
<td class="payload-url"><LinkTo @route="adminWebHooks.show" @model={{webHook}}>{{webHook.payload_url}}</LinkTo></td>
<td class="description">{{webHook.description}}</td>
<td class="controls">
<LinkTo @route="adminWebHooks.show" @model={{webHook}} class="btn btn-default no-text">{{d-icon "far-edit"}}</LinkTo>
<DButton @class="destroy btn-danger" @action={{action "destroy"}} @actionParam={{webHook}} @icon="times" />
</td>
</tr>
{{/each}}
</tbody>
</table>
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
</LoadMore>
{{else}}
<p>{{i18n "admin.web_hooks.none"}}</p>
{{/if}}
</div>
{{outlet}}

View File

@ -319,7 +319,13 @@ async function handleRequest(proxy, baseURL, req, res) {
});
response.headers.forEach((value, header) => {
res.set(header, value);
if (header === "set-cookie") {
// Special handling to get array of multiple Set-Cookie header values
// per https://github.com/node-fetch/node-fetch/issues/251#issuecomment-428143940
res.set("set-cookie", response.headers.raw()["set-cookie"]);
} else {
res.set(header, value);
}
});
res.set("content-encoding", null);

View File

@ -10,9 +10,14 @@
</div>
{{/if}}
{{#if this.dialog.message}}
{{#if (or this.dialog.message this.dialog.confirmPhrase)}}
<div class="dialog-body">
{{this.dialog.message}}
{{#if this.dialog.message}}
<p>{{this.dialog.message}}</p>
{{/if}}
{{#if this.dialog.confirmPhrase}}
<TextField @value={{this.dialog.confirmPhraseInput}} {{on "input" this.dialog.onConfirmPhraseInput}} @id="confirm-phrase" @autocorrect="off" @autocapitalize="off" />
{{/if}}
</div>
{{/if}}
@ -21,9 +26,9 @@
{{#each this.dialog.buttons as |button|}}
<DButton @icon={{button.icon}} @class={{button.class}} @action={{action "handleButtonAction" button}} @translatedLabel={{button.label}} />
{{else}}
<DButton @class={{this.dialog.confirmButtonClass}} @action={{this.dialog.didConfirmWrapped}} @icon={{this.dialog.confirmButtonIcon}} @label={{this.dialog.confirmButtonLabel}} />
<DButton @class={{this.dialog.confirmButtonClass}} @disabled={{this.dialog.confirmButtonDisabled}} @action={{this.dialog.didConfirmWrapped}} @icon={{this.dialog.confirmButtonIcon}} @label={{this.dialog.confirmButtonLabel}} />
{{#if this.dialog.shouldDisplayCancel}}
<DButton @class="btn-default" @action={{this.dialog.cancel}} @label={{this.dialog.cancelButtonLabel}} />
<DButton @class={{this.dialog.cancelButtonClass}} @action={{this.dialog.cancel}} @label={{this.dialog.cancelButtonLabel}} />
{{/if}}
{{/each}}
</div>

View File

@ -1,6 +1,7 @@
import Service from "@ember/service";
import A11yDialog from "a11y-dialog";
import { bind } from "discourse-common/utils/decorators";
import { isBlank } from "@ember/utils";
export default Service.extend({
message: null,
@ -13,7 +14,10 @@ export default Service.extend({
confirmButtonIcon: null,
confirmButtonLabel: null,
confirmButtonClass: null,
confirmPhrase: null,
confirmPhraseInput: null,
cancelButtonLabel: null,
cancelButtonClass: null,
shouldDisplayCancel: null,
didConfirm: null,
@ -32,6 +36,8 @@ export default Service.extend({
confirmButtonLabel = "ok_value",
confirmButtonClass = "btn-primary",
cancelButtonLabel = "cancel_value",
cancelButtonClass = "btn-default",
confirmPhrase,
shouldDisplayCancel,
didConfirm,
@ -39,6 +45,8 @@ export default Service.extend({
buttons,
} = params;
let confirmButtonDisabled = !isBlank(confirmPhrase);
const element = document.getElementById("dialog-holder");
this.setProperties({
@ -49,10 +57,13 @@ export default Service.extend({
title,
titleElementId: title !== null ? "dialog-title" : null,
confirmButtonDisabled,
confirmButtonClass,
confirmButtonLabel,
confirmButtonIcon,
confirmPhrase,
cancelButtonLabel,
cancelButtonClass,
shouldDisplayCancel,
didConfirm,
@ -131,7 +142,10 @@ export default Service.extend({
confirmButtonLabel: null,
confirmButtonIcon: null,
cancelButtonLabel: null,
cancelButtonClass: null,
shouldDisplayCancel: null,
confirmPhrase: null,
confirmPhraseInput: null,
didConfirm: null,
didCancel: null,
@ -160,4 +174,12 @@ export default Service.extend({
cancel() {
this.dialogInstance.hide();
},
@bind
onConfirmPhraseInput() {
this.set(
"confirmButtonDisabled",
this.confirmPhrase && this.confirmPhraseInput !== this.confirmPhrase
);
},
});

View File

@ -65,16 +65,19 @@ export function enableMissingIconWarning() {
}
export function renderIcon(renderType, id, params) {
for (let i = 0; i < _renderers.length; i++) {
let renderer = _renderers[i];
let rendererForType = renderer[renderType];
params ||= {};
if (rendererForType) {
const icon = { id, replacementId: REPLACEMENTS[id] };
let result = rendererForType(icon, params || {});
if (result) {
return result;
}
for (const renderer of _renderers) {
const rendererForType = renderer[renderType];
if (!rendererForType) {
continue;
}
const icon = { id, replacementId: REPLACEMENTS[id] };
const result = rendererForType(icon, params);
if (result) {
return result;
}
}
}

View File

@ -15,12 +15,12 @@
"start": "ember serve"
},
"dependencies": {
"@uppy/aws-s3": "^2.2.1",
"@uppy/aws-s3-multipart": "^2.4.1",
"@uppy/core": "^2.3.1",
"@uppy/drop-target": "^1.1.3",
"@uppy/utils": "^4.1.0",
"@uppy/xhr-upload": "^2.1.2",
"@uppy/aws-s3": "^3.0.4",
"@uppy/aws-s3-multipart": "^3.1.1",
"@uppy/core": "^3.0.4",
"@uppy/drop-target": "^2.0.1",
"@uppy/utils": "^5.1.1",
"@uppy/xhr-upload": "^3.0.4",
"ember-auto-import": "^2.5.0",
"ember-cli-babel": "^7.26.10",
"ember-cli-htmlbars": "^6.1.1",

View File

@ -3,12 +3,12 @@ const ColocatedTemplateProcessor = require("ember-cli-htmlbars/lib/colocated-bro
module.exports = class DiscoursePluginColocatedTemplateProcessor extends (
ColocatedTemplateProcessor
) {
constructor(tree, discoursePluginName) {
constructor(tree, rootName) {
super(tree);
this.discoursePluginName = discoursePluginName;
this.rootName = rootName;
}
detectRootName() {
return `discourse/plugins/${this.discoursePluginName}/discourse`;
return this.rootName;
}
};

View File

@ -176,7 +176,15 @@ module.exports = {
tree = RawHandlebarsCompiler(tree);
tree = new DiscoursePluginColocatedTemplateProcessor(tree, pluginName);
const colocateBase = `discourse/plugins/${pluginName}`;
tree = new DiscoursePluginColocatedTemplateProcessor(
tree,
`${colocateBase}/discourse`
);
tree = new DiscoursePluginColocatedTemplateProcessor(
tree,
`${colocateBase}/admin`
);
tree = this.compileTemplates(tree);
tree = this.processedAddonJsFiles(tree);

View File

@ -1,5 +1,8 @@
import { alias, not } from "@ember/object/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import discourseComputed, {
bind,
observes,
} from "discourse-common/utils/decorators";
import Component from "@ember/component";
export default Component.extend({
@ -40,18 +43,11 @@ export default Component.extend({
this._super(...arguments);
this.topics.forEach((topic) => {
const includeUnreadIndicator =
typeof topic.unread_by_group_member !== "undefined";
if (includeUnreadIndicator) {
const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`;
this.messageBus.subscribe(unreadIndicatorChannel, (data) => {
const nodeClassList = document.querySelector(
`.indicator-topic-${data.topic_id}`
).classList;
nodeClassList.toggle("read", !data.show_indicator);
});
if (typeof topic.unread_by_group_member !== "undefined") {
this.messageBus.subscribe(
`/private-messages/unread-indicator/${topic.id}`,
this.onMessage
);
}
});
},
@ -59,15 +55,19 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
this.topics.forEach((topic) => {
const includeUnreadIndicator =
typeof topic.unread_by_group_member !== "undefined";
this.messageBus.unsubscribe(
"/private-messages/unread-indicator/*",
this.onMessage
);
},
if (includeUnreadIndicator) {
const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`;
this.messageBus.unsubscribe(unreadIndicatorChannel);
}
});
@bind
onMessage(data) {
const nodeClassList = document.querySelector(
`.indicator-topic-${data.topic_id}`
).classList;
nodeClassList.toggle("read", !data.show_indicator);
},
@discourseComputed("topics")

View File

@ -41,7 +41,7 @@ export default class BookmarkIcon extends Component {
if (!isEmpty(this.bookmark.reminder_at)) {
const formattedTime = formattedReminderTime(
this.bookmark.reminder_at,
this.currentUser.timezone
this.currentUser.user_option.timezone
);
return I18n.t("bookmarks.created_with_reminder_generic", {
date: formattedTime,

View File

@ -57,7 +57,7 @@ export default Component.extend({
postDetectedLocalTime: null,
postDetectedLocalTimezone: null,
prefilledDatetime: null,
userTimezone: this.currentUser.timezone,
userTimezone: this.currentUser.user_option.timezone,
showOptions: false,
_itsatrap: new ItsATrap(),
autoDeletePreference: this.model.autoDeletePreference || 0,
@ -154,7 +154,7 @@ export default Component.extend({
}
this.currentUser.set(
"bookmark_auto_delete_preference",
"user_option.bookmark_auto_delete_preference",
this.autoDeletePreference
);

View File

@ -23,7 +23,6 @@ import {
linkSeenHashtagsInContext,
} from "discourse/lib/hashtag-autocomplete";
import {
cannotSee,
fetchUnseenMentions,
linkSeenMentions,
} from "discourse/lib/link-mentions";
@ -126,6 +125,7 @@ export default Component.extend(ComposerUploadUppy, {
init() {
this._super(...arguments);
this.warnedCannotSeeMentions = [];
this.warnedGroupMentions = [];
},
@discourseComputed("composer.requiredCategoryMissing")
@ -474,13 +474,15 @@ export default Component.extend(ComposerUploadUppy, {
},
_renderUnseenMentions(preview, unseen) {
// 'Create a New Topic' scenario is not supported (per conversation with codinghorror)
// https://meta.discourse.org/t/taking-another-1-7-release-task/51986/7
fetchUnseenMentions(unseen, this.get("composer.topic.id")).then((r) => {
fetchUnseenMentions({
names: unseen,
topicId: this.get("composer.topic.id"),
allowedNames: this.get("composer.targetRecipients")?.split(","),
}).then((response) => {
linkSeenMentions(preview, this.siteSettings);
this._warnMentionedGroups(preview);
this._warnCannotSeeMention(preview);
this._warnHereMention(r.here_count);
this._warnHereMention(response.here_count);
});
},
@ -506,28 +508,27 @@ export default Component.extend(ComposerUploadUppy, {
}
},
@debounce(2000)
_warnMentionedGroups(preview) {
schedule("afterRender", () => {
let found = this.warnedGroupMentions || [];
preview?.querySelectorAll(".mention-group.notify")?.forEach((mention) => {
if (this._isInQuote(mention)) {
return;
}
preview
.querySelectorAll(".mention-group[data-mentionable-user-count]")
.forEach((mention) => {
const { name } = mention.dataset;
if (
this.warnedGroupMentions.includes(name) ||
this._isInQuote(mention)
) {
return;
}
let name = mention.dataset.name;
if (!found.includes(name)) {
this.groupsMentioned([
{
name,
user_count: mention.dataset.mentionableUserCount,
max_mentions: mention.dataset.maxMentions,
},
]);
found.push(name);
}
});
this.set("warnedGroupMentions", found);
this.warnedGroupMentions.push(name);
this.groupsMentioned({
name,
userCount: mention.dataset.mentionableUserCount,
maxMentions: mention.dataset.maxMentions,
});
});
});
},
@ -539,22 +540,35 @@ export default Component.extend(ComposerUploadUppy, {
return;
}
const warnings = [];
preview.querySelectorAll(".mention.cannot-see").forEach((mention) => {
preview.querySelectorAll(".mention[data-reason]").forEach((mention) => {
const { name } = mention.dataset;
if (this.warnedCannotSeeMentions.includes(name)) {
return;
}
this.warnedCannotSeeMentions.push(name);
warnings.push({ name, reason: cannotSee[name] });
this.cannotSeeMention({
name,
reason: mention.dataset.reason,
});
});
if (warnings.length > 0) {
this.cannotSeeMention(warnings);
}
preview
.querySelectorAll(".mention-group[data-reason]")
.forEach((mention) => {
const { name } = mention.dataset;
if (this.warnedCannotSeeMentions.includes(name)) {
return;
}
this.warnedCannotSeeMentions.push(name);
this.cannotSeeMention({
name,
reason: mention.dataset.reason,
notifiedCount: mention.dataset.notifiedUserCount,
isGroup: true,
});
});
},
_warnHereMention(hereCount) {
@ -562,13 +576,7 @@ export default Component.extend(ComposerUploadUppy, {
return;
}
discourseLater(
this,
() => {
this.hereMention(hereCount);
},
2000
);
this.hereMention(hereCount);
},
@bind

View File

@ -464,9 +464,11 @@ export default Component.extend(TextareaTextManipulation, {
this.site.hashtag_configurations["topic-composer"],
this._$textarea,
this.siteSettings,
(value) => {
this.set("value", value);
schedule("afterRender", this, this.focusTextArea);
{
afterComplete: (value) => {
this.set("value", value);
schedule("afterRender", this, this.focusTextArea);
},
}
);
},

View File

@ -84,7 +84,7 @@ export default Component.extend({
@discourseComputed()
timeOptions() {
const timezone = this.currentUser.timezone;
const timezone = this.currentUser.user_option.timezone;
const shortcuts = timeShortcuts(timezone);
return [

View File

@ -28,7 +28,7 @@ export default Component.extend({
init() {
this._super(...arguments);
this.userTimezone = this.currentUser.timezone;
this.userTimezone = this.currentUser.user_option.timezone;
},
didReceiveAttrs() {

View File

@ -7,6 +7,7 @@
@jumpBottom={{@jumpBottom}}
@jumpEnd={{@jumpEnd}}
@jumpToIndex={{@jumpToIndex}}
@jumpToPostPrompt={{@jumpToPostPrompt}}
@fullscreen={{@fullscreen}}
@mobileView={{@mobileView}}
@currentUser={{this.currentUser}}
@ -23,50 +24,7 @@
@resetBumpDate={{@resetBumpDate}}
@convertToPublicTopic={{@convertToPublicTopic}}
@convertToPrivateMessage={{@convertToPrivateMessage}}
@replyToPost={{@replyToPost}}
/>
<div class="timeline-footer-controls">
{{#if this.displaySummary}}
<button type="button" class="show-summary btn-small" label={{i18n "summary.short_label"}} title={{i18n "summary.short_title"}} {{on "click" @showSummary}}>
{{d-icon "layer-group"}}
</button>
{{/if}}
{{#if (and this.currentUser (not @fullscreen))}}
{{#if this.canCreatePost}}
<button type="button" class="btn btn-default create reply-to-post no-text btn-icon" title={{i18n "topic.reply.help"}} {{on "click" (fn @replyToPost null)}}>
{{d-icon "reply"}}
</button>
{{/if}}
{{/if}}
{{#if @fullscreen}}
<button
type="button"
{{!-- we need to keep this a widget-button to not close the modal when opening form --}}
class="widget-button btn btn-text jump-to-post"
title={{i18n "topic.progress.jump_prompt_long"}}
{{on "click" @jumpToPostPrompt}}
>
<span class="d-button-label">
{{i18n "topic.progress.jump_prompt"}}
</span>
</button>
{{/if}}
{{#if this.currentUser}}
<TopicNotificationsButton
@notificationLevel={{@model.details.notification_level}}
@topic={{@model}}
@showFullTitle={{false}}
@appendReason={{false}}
@placement={{"bottom-end"}}
@showCaret={{false}}
/>
{{#if @mobileView}}
<TopicAdminMenuButton @topic={{@model}} @addKeyboardTargetClass={{true}} @openUpwards={{true}} />
{{/if}}
{{/if}}
</div>
</div>
</div>

View File

@ -79,10 +79,6 @@ export default class GlimmerTopicTimeline extends Component {
return this.args.fullscreen && !this.args.addShowClass;
}
get canCreatePost() {
return this.args.model.details?.can_create_post;
}
get createdAt() {
return new Date(this.args.model.created_at);
}

View File

@ -424,7 +424,7 @@ export default Component.extend(KeyEnterEscape, {
embedQuoteButton(canCreatePost, canReplyAsNewTopic) {
return (
(canCreatePost || canReplyAsNewTopic) &&
this.currentUser?.get("enable_quoting")
this.currentUser?.get("user_option.enable_quoting")
);
},

View File

@ -42,12 +42,10 @@ export default Component.extend({
}
},
@discourseComputed("backupEnabled", "totpEnabled", "secondFactorMethod")
showToggleMethodLink(backupEnabled, totpEnabled, secondFactorMethod) {
@discourseComputed("backupEnabled", "secondFactorMethod")
showToggleMethodLink(backupEnabled, secondFactorMethod) {
return (
backupEnabled &&
totpEnabled &&
secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
);
},

View File

@ -42,10 +42,9 @@ const SiteHeaderComponent = MountWidget.extend(
@observes("site.narrowDesktopView")
narrowDesktopViewChanged() {
if (
this.siteSettings.enable_experimental_sidebar_hamburger &&
(!this.sidebarEnabled || this.site.narrowDesktopView)
) {
this.eventDispatched("dom:clean", "header");
if (this._dropDownHeaderEnabled()) {
this.appEvents.on(
"sidebar-hamburger-dropdown:rendered",
this,
@ -231,10 +230,7 @@ const SiteHeaderComponent = MountWidget.extend(
this.appEvents.on("user-menu:rendered", this, "_animateMenu");
}
if (
this.siteSettings.enable_experimental_sidebar_hamburger &&
(!this.sidebarEnabled || this.site.narrowDesktopView)
) {
if (this._dropDownHeaderEnabled()) {
this.appEvents.on(
"sidebar-hamburger-dropdown:rendered",
this,
@ -323,10 +319,7 @@ const SiteHeaderComponent = MountWidget.extend(
this.appEvents.off("user-menu:rendered", this, "_animateMenu");
}
if (
this.siteSettings.enable_experimental_sidebar_hamburger &&
!this.sidebarEnabled
) {
if (this._dropDownHeaderEnabled()) {
this.appEvents.off(
"sidebar-hamburger-dropdown:rendered",
this,
@ -468,6 +461,14 @@ const SiteHeaderComponent = MountWidget.extend(
this._animate = false;
});
},
_dropDownHeaderEnabled() {
return (
(!this.sidebarEnabled &&
this.siteSettings.navigation_menu !== "legacy") ||
this.site.narrowDesktopView
);
},
}
);

View File

@ -1,7 +1,7 @@
import getURL from "discourse-common/lib/get-url";
import { cancel } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import discourseComputed, { bind, on } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { action } from "@ember/object";
import { isTesting } from "discourse-common/config/environment";
@ -13,36 +13,49 @@ export default Component.extend({
animatePrompt: false,
_timeoutHandler: null,
init() {
this._super(...arguments);
this.messageBus.subscribe("/refresh_client", this.onRefresh);
this.messageBus.subscribe("/global/asset-version", this.onAsset);
},
willDestroy() {
this._super(...arguments);
this.messageBus.unsubscribe("/refresh_client", this.onRefresh);
this.messageBus.unsubscribe("/global/asset-version", this.onAsset);
},
@bind
onRefresh() {
this.session.requiresRefresh = true;
},
@bind
onAsset(version) {
if (this.session.assetVersion !== version) {
this.session.requiresRefresh = true;
}
if (!this._timeoutHandler && this.session.requiresRefresh) {
if (isTesting()) {
this.updatePromptState(true);
} else {
// Since we can do this transparently for people browsing the forum
// hold back the message 24 hours.
this._timeoutHandler = discourseLater(() => {
this.updatePromptState(true);
}, 1000 * 60 * 24 * 60);
}
}
},
@discourseComputed
rootUrl() {
return getURL("/");
},
@on("init")
initSubscriptions() {
this.messageBus.subscribe("/refresh_client", () => {
this.session.requiresRefresh = true;
});
this.messageBus.subscribe("/global/asset-version", (version) => {
if (this.session.assetVersion !== version) {
this.session.requiresRefresh = true;
}
if (!this._timeoutHandler && this.session.requiresRefresh) {
if (isTesting()) {
this.updatePromptState(true);
} else {
// Since we can do this transparently for people browsing the forum
// hold back the message 24 hours.
this._timeoutHandler = discourseLater(() => {
this.updatePromptState(true);
}, 1000 * 60 * 24 * 60);
}
}
});
},
updatePromptState(value) {
// when adding the message, we inject the HTML then add the animation
// when dismissing, things need to happen in the opposite order

View File

@ -1,7 +1,6 @@
import { computed, get } from "@ember/object";
import Component from "@ember/component";
import I18n from "I18n";
import Site from "discourse/models/site";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
@ -51,7 +50,7 @@ export default Component.extend({
if (suggestedGroupName) {
return I18n.messageFormat("user.messages.read_more_group_pm_MF", {
BOTH: hasBoth,
HAS_UNREAD_AND_NEW: hasBoth,
UNREAD: unreadCount,
NEW: newCount,
username,
@ -61,7 +60,7 @@ export default Component.extend({
});
} else {
return I18n.messageFormat("user.messages.read_more_personal_pm_MF", {
BOTH: hasBoth,
HAS_UNREAD_AND_NEW: hasBoth,
UNREAD: unreadCount,
NEW: newCount,
username,
@ -81,31 +80,15 @@ export default Component.extend({
},
_topicBrowseMoreMessage(topic) {
const opts = {
latestLink: `<a href="${getURL("/latest")}">${I18n.t(
"topic.view_latest_topics"
)}</a>`,
};
let category = topic.get("category");
if (
category &&
get(category, "id") === Site.currentProp("uncategorized_category_id")
get(category, "id") === this.site.uncategorized_category_id
) {
category = null;
}
if (category) {
opts.catLink = categoryBadgeHTML(category);
} else {
opts.catLink =
'<a href="' +
getURL("/categories") +
'">' +
I18n.t("topic.browse_all_categories") +
"</a>";
}
let unreadTopics = 0;
let newTopics = 0;
@ -115,21 +98,24 @@ export default Component.extend({
}
if (newTopics + unreadTopics > 0) {
const hasBoth = unreadTopics > 0 && newTopics > 0;
return I18n.messageFormat("topic.read_more_MF", {
BOTH: hasBoth,
HAS_UNREAD_AND_NEW: unreadTopics > 0 && newTopics > 0,
UNREAD: unreadTopics,
NEW: newTopics,
CATEGORY: category ? true : false,
latestLink: opts.latestLink,
catLink: opts.catLink,
HAS_CATEGORY: category ? true : false,
categoryLink: category ? categoryBadgeHTML(category) : null,
basePath: getURL(""),
});
} else if (category) {
return I18n.t("topic.read_more_in_category", opts);
return I18n.t("topic.read_more_in_category", {
categoryLink: categoryBadgeHTML(category),
latestLink: getURL("/latest"),
});
} else {
return I18n.t("topic.read_more", opts);
return I18n.t("topic.read_more", {
categoryLink: getURL("/categories"),
latestLink: getURL("/latest"),
});
}
},

View File

@ -68,7 +68,7 @@ export default Component.extend({
@on("init")
_setupPicker() {
this.setProperties({
userTimezone: this.currentUser.timezone,
userTimezone: this.currentUser.user_option.timezone,
hiddenOptions: this.hiddenOptions || [],
customOptions: this.customOptions || [],
customLabels: this.customLabels || {},

View File

@ -56,7 +56,7 @@ export default Component.extend({
canInviteTo: alias("topic.details.can_invite_to"),
canDefer: alias("currentUser.enable_defer"),
canDefer: alias("currentUser.user_option.enable_defer"),
inviteDisabled: or("topic.archived", "topic.closed", "topic.deleted"),

View File

@ -77,13 +77,7 @@ export default Component.extend({
this._super(...arguments);
if (this.includeUnreadIndicator) {
this.messageBus.subscribe(this.unreadIndicatorChannel, (data) => {
const nodeClassList = document.querySelector(
`.indicator-topic-${data.topic_id}`
).classList;
nodeClassList.toggle("read", !data.show_indicator);
});
this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage);
}
schedule("afterRender", () => {
@ -101,9 +95,8 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
if (this.includeUnreadIndicator) {
this.messageBus.unsubscribe(this.unreadIndicatorChannel);
}
this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage);
if (this._shouldFocusLastVisited()) {
const title = this._titleElement();
if (title) {
@ -113,6 +106,15 @@ export default Component.extend({
}
},
@bind
onMessage(data) {
const nodeClassList = document.querySelector(
`.indicator-topic-${data.topic_id}`
).classList;
nodeClassList.toggle("read", !data.show_indicator);
},
@discourseComputed("topic.id")
unreadIndicatorChannel(topicId) {
return `/private-messages/unread-indicator/${topicId}`;

View File

@ -79,7 +79,7 @@ export default Component.extend({
: this._reset("invisible");
},
_set(name, icon, key, iconArgs = null) {
_set(name, icon, key, iconArgs) {
this.set(`${name}Icon`, htmlSafe(iconHTML(`${icon}`, iconArgs)));
this.set(`${name}Title`, I18n.t(`topic_statuses.${key}.help`));
return true;

View File

@ -1,7 +1,7 @@
{{#if @fullscreen}}
<div class="title">
<h2>
<a class="fancy-title" href {{on "click" @jumpTop}}>{{if @mobileView @model.fancyTitle ""}}</a>
<a class="fancy-title" href {{on "click" @jumpTop}}>{{this.topicTitle}}</a>
</h2>
{{#if (or this.siteSettings.topic_featured_link_enabled this.showTags)}}
<div class="topic-header-extra">
@ -94,4 +94,48 @@
</a>
</div>
</div>
<div class="timeline-footer-controls">
{{#if this.displaySummary}}
<button type="button" class="show-summary btn-small" label={{i18n "summary.short_label"}} title={{i18n "summary.short_title"}} {{on "click" @showSummary}}>
{{d-icon "layer-group"}}
</button>
{{/if}}
{{#if (and @currentUser (not @fullscreen))}}
{{#if this.canCreatePost}}
<button type="button" class="btn btn-default create reply-to-post no-text btn-icon" title={{i18n "topic.reply.help"}} {{on "click" (fn @replyToPost null)}}>
{{d-icon "reply"}}
</button>
{{/if}}
{{/if}}
{{#if @fullscreen}}
<button
type="button"
{{!-- we need to keep this a widget-button to not close the modal when opening form --}}
class="widget-button btn btn-text jump-to-post"
title={{i18n "topic.progress.jump_prompt_long"}}
{{on "click" @jumpToPostPrompt}}
>
<span class="d-button-label">
{{i18n "topic.progress.jump_prompt"}}
</span>
</button>
{{/if}}
{{#if @currentUser}}
<TopicNotificationsButton
@notificationLevel={{@model.details.notification_level}}
@topic={{@model}}
@showFullTitle={{false}}
@appendReason={{false}}
@placement={{"bottom-end"}}
@showCaret={{false}}
/>
{{#if @mobileView}}
<TopicAdminMenuButton @topic={{@model}} @addKeyboardTargetClass={{true}} @openUpwards={{true}} />
{{/if}}
{{/if}}
</div>
{{/if}}

View File

@ -27,7 +27,6 @@ export default class TopicTimelineScrollArea extends Component {
@tracked total;
@tracked date;
@tracked lastReadPercentage = null;
@tracked displayTimeLineScrollArea = true;
@tracked before;
@tracked after;
@tracked timelineScrollareaStyle;
@ -38,15 +37,6 @@ export default class TopicTimelineScrollArea extends Component {
super(...arguments);
if (!this.args.mobileView) {
const streamLength = this.args.model.postStream?.stream?.length;
if (streamLength === 1) {
const postsWrapper = document.querySelector(".posts-wrapper");
if (postsWrapper && postsWrapper.offsetHeight < 1000) {
this.displayTimeLineScrollArea = false;
}
}
// listen for scrolling event to update timeline
this.appEvents.on("topic:current-post-scrolled", this.postScrolled);
// listen for composer sizing changes to update timeline
@ -58,6 +48,30 @@ export default class TopicTimelineScrollArea extends Component {
this.calculatePosition();
}
get displayTimeLineScrollArea() {
if (this.args.mobileView) {
return true;
}
const streamLength = this.args.model.postStream?.stream?.length;
if (streamLength === 1) {
const postsWrapper = document.querySelector(".posts-wrapper");
if (postsWrapper && postsWrapper.offsetHeight < 1000) {
return false;
}
}
return true;
}
get canCreatePost() {
return this.args.model.details?.can_create_post;
}
get topicTitle() {
return htmlSafe(this.args.mobileView ? this.args.model.fancyTitle : "");
}
get showTags() {
return (
this.siteSettings.tagging_enabled && this.args.model.tags?.length > 0

View File

@ -92,7 +92,7 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
if (!this.showUserLocalTime) {
return;
}
return user.timezone;
return user.get("user_option.timezone");
},
@discourseComputed("userTimezone")

View File

@ -83,7 +83,7 @@ const CORE_TOP_TABS = [
}
get shouldDisplay() {
return !this.currentUser.likes_notifications_disabled;
return !this.currentUser.user_option.likes_notifications_disabled;
}
get count() {

View File

@ -56,7 +56,7 @@
</LinkTo>
</li>
{{#if @siteSettings.enable_experimental_sidebar_hamburger}}
{{#if (not (eq @siteSettings.navigation_menu "legacy"))}}
<li class="indent nav-sidebar">
<LinkTo @route="preferences.sidebar">
{{d-icon "bars"}}

View File

@ -12,10 +12,10 @@ export default class UserStatusMessage extends Component {
return null;
}
return until(
this.status.ends_at,
this.currentUser.timezone,
this.currentUser.locale
);
const timezone = this.currentUser
? this.currentUser.timezone
: moment.tz.guess();
return until(this.status.ends_at, timezone, this.currentUser?.locale);
}
}

View File

@ -0,0 +1,55 @@
{{#if this.editing}}
<form class="form-horizontal">
<div class="control-group">
<Input
{{on "input" this.onInput}}
@value={{this.newUsername}}
maxlength={{this.maxLength}}
class="input-xxlarge username-preference__input"
/>
<div class="instructions">
<p>
{{#if this.taken}}
{{i18n "user.change_username.taken"}}
{{/if}}
<span>{{this.errorMessage}}</span>
</p>
</div>
</div>
<div class="control-group">
<DButton
@action={{this.changeUsername}}
@type="submit"
@disabled={{this.saveDisabled}}
@translatedLabel={{this.saveButtonText}}
class="btn-primary username-preference__submit"
/>
<DModalCancel @close={{this.toggleEditing}} />
{{#if this.saved}}{{i18n "saved"}}{{/if}}
</div>
</form>
{{else}}
<div class="controls">
<span class="static username-preference__current-username">{{@user.username}}</span>
{{#if @user.can_edit_username}}
<DButton
@action={{this.toggleEditing}}
@actionParam={{@user}}
@icon="pencil-alt"
@title="user.username.edit"
class="btn-small username-preference__edit-username"
/>
{{/if}}
</div>
{{#if this.siteSettings.enable_mentions}}
<div class="instructions">
{{html-safe (i18n "user.username.short_instructions" username=@user.username)}}
</div>
{{/if}}
{{/if}}

View File

@ -0,0 +1,100 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import DiscourseURL, { userPath } from "discourse/lib/url";
import { empty, or } from "@ember/object/computed";
import { setting } from "discourse/lib/computed";
import I18n from "I18n";
import User from "discourse/models/user";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class UsernamePreference extends Component {
@service siteSettings;
@service dialog;
@tracked editing = false;
@tracked newUsername = this.args.user.username;
@tracked errorMessage = null;
@tracked saving = false;
@tracked taken = false;
@setting("max_username_length") maxLength;
@setting("min_username_length") minLength;
@empty("newUsername") newUsernameEmpty;
@or("saving", "newUsernameEmpty", "taken", "unchanged", "errorMessage")
saveDisabled;
get unchanged() {
return this.newUsername === this.args.user.username;
}
get saveButtonText() {
return this.saving ? I18n.t("saving") : I18n.t("user.change");
}
@action
toggleEditing() {
this.editing = !this.editing;
this.newUsername = this.args.user.username;
this.errorMessage = null;
this.saving = false;
this.taken = false;
}
@action
async onInput(event) {
this.newUsername = event.target.value;
this.taken = false;
this.errorMessage = null;
if (isEmpty(this.newUsername)) {
return;
}
if (this.newUsername === this.args.user.username) {
return;
}
if (this.newUsername.length < this.minLength) {
this.errorMessage = I18n.t("user.name.too_short");
return;
}
const result = await User.checkUsername(
this.newUsername,
undefined,
this.args.user.id
);
if (result.errors) {
this.errorMessage = result.errors.join(" ");
} else if (result.available === false) {
this.taken = true;
}
}
@action
changeUsername() {
return this.dialog.yesNoConfirm({
title: I18n.t("user.change_username.confirm"),
didConfirm: async () => {
this.saving = true;
try {
await this.args.user.changeUsername(this.newUsername);
DiscourseURL.redirectTo(
userPath(this.newUsername.toLowerCase() + "/preferences")
);
} catch (e) {
popupAjaxError(e);
} finally {
this.saving = false;
}
},
});
}
}

View File

@ -64,13 +64,13 @@ export default Controller.extend({
@discourseComputed(
"sidebarQueryParamOverride",
"siteSettings.enable_sidebar",
"siteSettings.navigation_menu",
"canDisplaySidebar",
"sidebarDisabledRouteOverride"
)
sidebarEnabled(
sidebarQueryParamOverride,
enableSidebar,
navigationMenu,
canDisplaySidebar,
sidebarDisabledRouteOverride
) {
@ -95,7 +95,7 @@ export default Controller.extend({
return false;
}
return enableSidebar;
return navigationMenu === "sidebar";
},
calculateShowSidebar() {

View File

@ -782,51 +782,62 @@ export default Controller.extend({
}
},
groupsMentioned(groups) {
groupsMentioned({ name, userCount, maxMentions }) {
if (
!this.get("model.creatingPrivateMessage") &&
!this.get("model.topic.isPrivateMessage")
this.get("model.creatingPrivateMessage") ||
this.get("model.topic.isPrivateMessage")
) {
groups.forEach((group) => {
let body;
const groupLink = getURL(`/g/${group.name}/members`);
const maxMentions = parseInt(group.max_mentions, 10);
const userCount = parseInt(group.user_count, 10);
return;
}
if (maxMentions < userCount) {
body = I18n.t("composer.group_mentioned_limit", {
group: `@${group.name}`,
count: maxMentions,
group_link: groupLink,
});
} else if (group.user_count > 0) {
body = I18n.t("composer.group_mentioned", {
group: `@${group.name}`,
count: userCount,
group_link: groupLink,
});
}
maxMentions = parseInt(maxMentions, 10);
userCount = parseInt(userCount, 10);
if (body) {
this.appEvents.trigger("composer-messages:create", {
extraClass: "custom-body",
templateName: "education",
body,
});
}
let body;
const groupLink = getURL(`/g/${name}/members`);
if (userCount > maxMentions) {
body = I18n.t("composer.group_mentioned_limit", {
group: `@${name}`,
count: maxMentions,
group_link: groupLink,
});
} else if (userCount > 0) {
body = I18n.t("composer.group_mentioned", {
group: `@${name}`,
count: userCount,
group_link: groupLink,
});
}
if (body) {
this.appEvents.trigger("composer-messages:create", {
extraClass: "custom-body",
templateName: "education",
body,
});
}
},
cannotSeeMention(mentions) {
mentions.forEach((mention) => {
this.appEvents.trigger("composer-messages:create", {
extraClass: "custom-body",
templateName: "education",
body: I18n.t(`composer.cannot_see_mention.${mention.reason}`, {
username: mention.name,
}),
cannotSeeMention({ name, reason, notifiedCount, isGroup }) {
notifiedCount = parseInt(notifiedCount, 10);
let body;
if (isGroup) {
body = I18n.t(`composer.cannot_see_group_mention.${reason}`, {
group: name,
count: notifiedCount,
});
} else {
body = I18n.t(`composer.cannot_see_mention.${reason}`, {
username: name,
});
}
this.appEvents.trigger("composer-messages:create", {
extraClass: "custom-body",
templateName: "education",
body,
});
},

View File

@ -192,7 +192,7 @@ export default Controller.extend(
@discourseComputed
timeShortcuts() {
const timezone = this.currentUser.timezone;
const timezone = this.currentUser.user_option.timezone;
const shortcuts = timeShortcuts(timezone);
return [
shortcuts.laterToday(),

View File

@ -22,7 +22,7 @@ const controllerOpts = {
canStar: alias("currentUser.id"),
showTopicPostBadges: not("new"),
redirectedReason: alias("currentUser.redirected_to_top.reason"),
redirectedReason: alias("currentUser.user_option.redirected_to_top.reason"),
expandGloballyPinned: false,
expandAllPinned: false,

View File

@ -10,10 +10,10 @@ export default Controller.extend(ModalFunctionality, {
@action
downloadCalendar() {
if (this.remember) {
this.currentUser.setProperties({
default_calendar: this.selectedCalendar,
user_option: { default_calendar: this.selectedCalendar },
});
this.currentUser.user_option.set(
"default_calendar",
this.selectedCalendar
);
this.currentUser.save(["default_calendar"]);
}
if (this.selectedCalendar === "ics") {

View File

@ -110,7 +110,7 @@ export default Controller.extend(ModalFunctionality, {
@discourseComputed
timeShortcuts() {
const timezone = this.currentUser.timezone;
const timezone = this.currentUser.user_option.timezone;
const shortcuts = timeShortcuts(timezone);
const nextWeek = shortcuts.monday();

View File

@ -47,7 +47,7 @@ export default Controller.extend(ModalFunctionality, {
});
}
if (this.currentUser.staff) {
if (this.model.details.can_delete) {
types.push({
id: DELETE_STATUS_TYPE,
name: I18n.t("topic.auto_delete.title"),
@ -59,7 +59,7 @@ export default Controller.extend(ModalFunctionality, {
name: I18n.t("topic.auto_bump.title"),
});
if (this.currentUser.staff) {
if (this.model.details.can_delete) {
types.push({
id: DELETE_REPLIES_TYPE,
name: I18n.t("topic.auto_delete_replies.title"),

View File

@ -13,7 +13,7 @@ export default Controller.extend(ModalFunctionality, {
@discourseComputed
timeShortcuts() {
const timezone = this.currentUser.timezone;
const timezone = this.currentUser.user_option.timezone;
const shortcuts = timeShortcuts(timezone);
return [
shortcuts.laterToday(),

View File

@ -11,7 +11,7 @@ export default Controller.extend(ModalFunctionality, {
@discourseComputed
timeShortcuts() {
const timezone = this.currentUser.timezone;
const timezone = this.currentUser.user_option.timezone;
const shortcuts = timeShortcuts(timezone);
return [
shortcuts.laterToday(),

View File

@ -225,30 +225,22 @@ export default Controller.extend(ModalFunctionality, {
this.clearFlash();
if (
(result.security_key_enabled ||
result.totp_enabled ||
result.backup_enabled) &&
(result.security_key_enabled || result.totp_enabled) &&
!this.secondFactorRequired
) {
let secondFactorMethod;
if (result.security_key_enabled) {
secondFactorMethod = SECOND_FACTOR_METHODS.SECURITY_KEY;
} else if (result.totp_enabled) {
secondFactorMethod = SECOND_FACTOR_METHODS.TOTP;
} else {
secondFactorMethod = SECOND_FACTOR_METHODS.BACKUP_CODE;
}
this.setProperties({
otherMethodAllowed: result.multiple_second_factor_methods,
secondFactorRequired: true,
showLoginButtons: false,
backupEnabled: result.backup_enabled,
totpEnabled: result.totp_enabled,
showSecondFactor: result.totp_enabled || result.backup_enabled,
showSecondFactor: result.totp_enabled,
showSecurityKey: result.security_key_enabled,
secondFactorMethod: result.security_key_enabled
? SECOND_FACTOR_METHODS.SECURITY_KEY
: SECOND_FACTOR_METHODS.TOTP,
securityKeyChallenge: result.challenge,
securityKeyAllowedCredentialIds: result.allowed_credential_ids,
secondFactorMethod,
});
// only need to focus the 2FA input for TOTP

View File

@ -82,13 +82,11 @@ export default Controller.extend({
@discourseComputed()
emailFrequencyInstructions() {
if (this.siteSettings.email_time_window_mins) {
return I18n.t("user.email.frequency", {
count: this.siteSettings.email_time_window_mins,
});
} else {
return I18n.t("user.email.frequency_immediately");
}
return this.siteSettings.email_time_window_mins
? I18n.t("user.email.frequency", {
count: this.siteSettings.email_time_window_mins,
})
: null;
},
actions: {

View File

@ -51,7 +51,7 @@ export default Controller.extend({
});
},
@discourseComputed("model.default_calendar")
@discourseComputed("model.user_option.default_calendar")
canChangeDefaultCalendar(defaultCalendar) {
return defaultCalendar !== "none_selected";
},
@ -125,12 +125,6 @@ export default Controller.extend({
return model
.save(this.saveAttrNames)
.then(() => {
// update the timezone in memory so we can use the new
// one if we change routes without reloading the user
if (this.currentUser.id === this.model.id) {
this.currentUser.timezone = this.model.user_option.timezone;
}
cookAsync(model.get("bio_raw"))
.then(() => {
model.set("bio_cooked");

View File

@ -10,6 +10,7 @@ import { findAll } from "discourse/models/login-method";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
export default Controller.extend(CanCheckEmails, {
dialog: service(),
@ -113,6 +114,29 @@ export default Controller.extend(CanCheckEmails, {
.finally(() => this.set("resetPasswordLoading", false));
},
disableAllMessage() {
let templateElements = [I18n.t("user.second_factor.delete_confirm_header")];
templateElements.push("<ul>");
this.totps.forEach((totp) => {
templateElements.push(`<li>${totp.name}</li>`);
});
this.security_keys.forEach((key) => {
templateElements.push(`<li>${key.name}</li>`);
});
if (this.currentUser.second_factor_backup_enabled) {
templateElements.push(
`<li>${I18n.t("user.second_factor_backup.title")}</li>`
);
}
templateElements.push("</ul>");
templateElements.push(
I18n.t("user.second_factor.delete_confirm_instruction", {
confirm: I18n.t("user.second_factor.disable"),
})
);
return htmlSafe(templateElements.join(""));
},
actions: {
confirmPassword() {
if (!this.password) {
@ -130,8 +154,11 @@ export default Controller.extend(CanCheckEmails, {
this.dialog.deleteConfirm({
title: I18n.t("user.second_factor.disable_confirm"),
message: this.disableAllMessage(),
confirmButtonLabel: "user.second_factor.disable",
confirmPhrase: I18n.t("user.second_factor.disable"),
confirmButtonIcon: "ban",
cancelButtonClass: "btn-flat",
didConfirm: () => {
this.model
.disableAllSecondFactors()
@ -144,6 +171,88 @@ export default Controller.extend(CanCheckEmails, {
},
});
},
disableSingleSecondFactor(secondFactorMethod) {
if (this.totps.concat(this.security_keys).length === 1) {
this.send("disableAllSecondFactors");
return;
}
this.dialog.deleteConfirm({
title: I18n.t("user.second_factor.delete_single_confirm_title"),
message: I18n.t("user.second_factor.delete_single_confirm_message", {
name: secondFactorMethod.name,
}),
confirmButtonLabel: "user.second_factor.delete",
confirmButtonIcon: "ban",
cancelButtonClass: "btn-flat",
didConfirm: () => {
this.currentUser
.updateSecondFactor(
secondFactorMethod.id,
secondFactorMethod.name,
true,
secondFactorMethod.method
)
.then((response) => {
if (response.error) {
return;
}
this.markDirty();
})
.catch((e) => this.handleError(e))
.finally(() => {
this.setProperties({
totps: this.totps.filter(
(totp) =>
totp.id !== secondFactorMethod.id ||
totp.method !== secondFactorMethod.method
),
security_keys: this.security_keys.filter(
(key) =>
key.id !== secondFactorMethod.id ||
key.method !== secondFactorMethod.method
),
});
this.set("loading", false);
});
},
});
},
disableSecondFactorBackup() {
this.dialog.deleteConfirm({
title: I18n.t("user.second_factor.delete_backup_codes_confirm_title"),
message: I18n.t(
"user.second_factor.delete_backup_codes_confirm_message"
),
confirmButtonLabel: "user.second_factor.delete",
confirmButtonIcon: "ban",
cancelButtonClass: "btn-flat",
didConfirm: () => {
this.set("backupCodes", []);
this.set("loading", true);
this.model
.updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
.then((response) => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.set("errorMessage", null);
this.model.set("second_factor_backup_enabled", false);
this.markDirty();
this.send("closeModal");
})
.catch((error) => {
this.send("closeModal");
this.onError(error);
})
.finally(() => this.set("loading", false));
},
});
},
createTotp() {
const controller = showModal("second-factor-add-totp", {

View File

@ -13,6 +13,12 @@ export default class extends Controller {
@tracked selectedSidebarCategories = [];
@tracked selectedSidebarTagNames = [];
saveAttrNames = [
"sidebar_category_ids",
"sidebar_tag_names",
"sidebar_list_destination",
];
sidebarListDestinations = [
{
name: I18n.t("user.experimental_sidebar.list_destination_default"),
@ -42,7 +48,7 @@ export default class extends Controller {
);
this.model
.save()
.save(this.saveAttrNames)
.then((result) => {
if (result.user.sidebar_tags) {
this.model.set("sidebar_tags", result.user.sidebar_tags);

View File

@ -1,91 +0,0 @@
import DiscourseURL, { userPath } from "discourse/lib/url";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { empty, or } from "@ember/object/computed";
import { propertyEqual, setting } from "discourse/lib/computed";
import Controller from "@ember/controller";
import I18n from "I18n";
import User from "discourse/models/user";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend({
dialog: service(),
taken: false,
saving: false,
errorMessage: null,
newUsername: null,
maxLength: setting("max_username_length"),
minLength: setting("min_username_length"),
newUsernameEmpty: empty("newUsername"),
saveDisabled: or(
"saving",
"newUsernameEmpty",
"taken",
"unchanged",
"errorMessage"
),
unchanged: propertyEqual("newUsername", "username"),
@observes("newUsername")
checkTaken() {
let newUsername = this.newUsername;
if (newUsername && newUsername.length < this.minLength) {
this.set("errorMessage", I18n.t("user.name.too_short"));
} else {
this.set("taken", false);
this.set("errorMessage", null);
if (isEmpty(this.newUsername)) {
return;
}
if (this.unchanged) {
return;
}
User.checkUsername(newUsername, undefined, this.get("model.id")).then(
(result) => {
if (result.errors) {
this.set("errorMessage", result.errors.join(" "));
} else if (result.available === false) {
this.set("taken", true);
}
}
);
}
},
@discourseComputed("saving")
saveButtonText(saving) {
if (saving) {
return I18n.t("saving");
}
return I18n.t("user.change");
},
actions: {
changeUsername() {
if (this.saveDisabled) {
return;
}
return this.dialog.yesNoConfirm({
title: I18n.t("user.change_username.confirm"),
didConfirm: () => {
this.set("saving", true);
this.model
.changeUsername(this.newUsername)
.then(() => {
DiscourseURL.redirectTo(
userPath(this.newUsername.toLowerCase() + "/preferences")
);
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
},
});
},
},
});

View File

@ -40,30 +40,6 @@ export default Controller.extend(ModalFunctionality, {
this._hideCopyMessage();
},
disableSecondFactorBackup() {
this.set("backupCodes", []);
this.set("loading", true);
this.model
.updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
.then((response) => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.set("errorMessage", null);
this.model.set("second_factor_backup_enabled", false);
this.markDirty();
this.send("closeModal");
})
.catch((error) => {
this.send("closeModal");
this.onError(error);
})
.finally(() => this.set("loading", false));
},
generateSecondFactorCodes() {
this.set("loading", true);
this.model

View File

@ -3,30 +3,6 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
export default 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(

View File

@ -62,7 +62,7 @@ addBulkButton("deletePostTiming", "defer", {
icon: "circle",
class: "btn-default",
buttonVisible() {
return this.currentUser.enable_defer;
return this.currentUser.user_option.enable_defer;
},
});
addBulkButton("unlistTopics", "unlist_topics", {

View File

@ -2,7 +2,10 @@ import Category from "discourse/models/category";
import Controller, { inject as controller } from "@ember/controller";
import DiscourseURL, { userPath } from "discourse/lib/url";
import { alias, and, not, or } from "@ember/object/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import discourseComputed, {
bind,
observes,
} from "discourse-common/utils/decorators";
import { isEmpty, isPresent } from "@ember/utils";
import { next, schedule } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later";
@ -837,7 +840,7 @@ export default Controller.extend(bufferedProperty("model"), {
bookmarkable_id: post.id,
bookmarkable_type: "Post",
auto_delete_preference:
this.currentUser.bookmark_auto_delete_preference,
this.currentUser.user_option.bookmark_auto_delete_preference,
}),
post
);
@ -1350,7 +1353,7 @@ export default Controller.extend(bufferedProperty("model"), {
bookmarkable_id: this.model.id,
bookmarkable_type: "Topic",
auto_delete_preference:
this.currentUser.bookmark_auto_delete_preference,
this.currentUser.user_option.bookmark_auto_delete_preference,
})
);
}
@ -1599,157 +1602,9 @@ export default Controller.extend(bufferedProperty("model"), {
subscribe() {
this.unsubscribe();
const refresh = (args) =>
this.appEvents.trigger("post-stream:refresh", args);
this.messageBus.subscribe(
`/topic/${this.get("model.id")}`,
(data) => {
const topic = this.model;
if (isPresent(data.notification_level_change)) {
topic.set(
"details.notification_level",
data.notification_level_change
);
topic.set(
"details.notifications_reason_id",
data.notifications_reason_id
);
return;
}
const postStream = this.get("model.postStream");
if (data.reload_topic) {
topic.reload().then(() => {
this.send("postChangedRoute", topic.get("post_number") || 1);
this.appEvents.trigger("header:update-topic", topic);
if (data.refresh_stream) {
postStream.refresh();
}
});
return;
}
switch (data.type) {
case "acted":
postStream
.triggerChangedPost(data.id, data.updated_at, {
preserveCooked: true,
})
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
case "read": {
postStream
.triggerReadPost(data.id, data.readers_count)
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
}
case "liked":
case "unliked": {
postStream
.triggerLikedPost(
data.id,
data.likes_count,
data.user_id,
data.type
)
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
}
case "revised":
case "rebaked": {
postStream
.triggerChangedPost(data.id, data.updated_at)
.then(() => refresh({ id: data.id }));
break;
}
case "deleted": {
postStream
.triggerDeletedPost(data.id)
.then(() => refresh({ id: data.id }));
break;
}
case "destroyed": {
postStream
.triggerDestroyedPost(data.id)
.then(() => refresh({ id: data.id }));
break;
}
case "recovered": {
postStream
.triggerRecoveredPost(data.id)
.then(() => refresh({ id: data.id }));
break;
}
case "created": {
this._newPostsInStream.push(data.id);
this.retryOnRateLimit(RETRIES_ON_RATE_LIMIT, () => {
const postIds = this._newPostsInStream;
this._newPostsInStream = [];
return postStream
.triggerNewPostsInStream(postIds, { background: true })
.then(() => refresh())
.catch((e) => {
this._newPostsInStream = postIds.concat(
this._newPostsInStream
);
throw e;
});
});
if (this.get("currentUser.id") !== data.user_id) {
this.documentTitle.incrementBackgroundContextCount();
}
break;
}
case "move_to_inbox": {
topic.set("message_archived", false);
break;
}
case "archived": {
topic.set("message_archived", true);
break;
}
case "stats": {
let updateStream = false;
["last_posted_at", "like_count", "posts_count"].forEach(
(property) => {
const value = data[property];
if (typeof value !== "undefined") {
topic.set(property, value);
updateStream = true;
}
}
);
if (data["last_poster"]) {
topic.details.set("last_poster", data["last_poster"]);
updateStream = true;
}
if (updateStream) {
postStream
.triggerChangedTopicStats()
.then((firstPostId) => refresh({ id: firstPostId }));
}
break;
}
default: {
let callback = customPostMessageCallbacks[data.type];
if (callback) {
callback(this, data);
} else {
// eslint-disable-next-line no-console
console.warn("unknown topic bus message type", data);
}
}
}
},
this.onMessage,
this.get("model.message_bus_last_id")
);
},
@ -1759,7 +1614,146 @@ export default Controller.extend(bufferedProperty("model"), {
if (!this.get("model.id")) {
return;
}
this.messageBus.unsubscribe("/topic/*");
this.messageBus.unsubscribe("/topic/*", this.onMessage);
},
@bind
onMessage(data) {
const topic = this.model;
const refresh = (args) =>
this.appEvents.trigger("post-stream:refresh", args);
if (isPresent(data.notification_level_change)) {
topic.set("details.notification_level", data.notification_level_change);
topic.set(
"details.notifications_reason_id",
data.notifications_reason_id
);
return;
}
const postStream = this.get("model.postStream");
if (data.reload_topic) {
topic.reload().then(() => {
this.send("postChangedRoute", topic.get("post_number") || 1);
this.appEvents.trigger("header:update-topic", topic);
if (data.refresh_stream) {
postStream.refresh();
}
});
return;
}
switch (data.type) {
case "acted":
postStream
.triggerChangedPost(data.id, data.updated_at, {
preserveCooked: true,
})
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
case "read": {
postStream
.triggerReadPost(data.id, data.readers_count)
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
}
case "liked":
case "unliked": {
postStream
.triggerLikedPost(data.id, data.likes_count, data.user_id, data.type)
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
}
case "revised":
case "rebaked": {
postStream
.triggerChangedPost(data.id, data.updated_at)
.then(() => refresh({ id: data.id }));
break;
}
case "deleted": {
postStream
.triggerDeletedPost(data.id)
.then(() => refresh({ id: data.id }));
break;
}
case "destroyed": {
postStream
.triggerDestroyedPost(data.id)
.then(() => refresh({ id: data.id }));
break;
}
case "recovered": {
postStream
.triggerRecoveredPost(data.id)
.then(() => refresh({ id: data.id }));
break;
}
case "created": {
this._newPostsInStream.push(data.id);
this.retryOnRateLimit(RETRIES_ON_RATE_LIMIT, () => {
const postIds = this._newPostsInStream;
this._newPostsInStream = [];
return postStream
.triggerNewPostsInStream(postIds, { background: true })
.then(() => refresh())
.catch((e) => {
this._newPostsInStream = postIds.concat(this._newPostsInStream);
throw e;
});
});
if (this.get("currentUser.id") !== data.user_id) {
this.documentTitle.incrementBackgroundContextCount();
}
break;
}
case "move_to_inbox": {
topic.set("message_archived", false);
break;
}
case "archived": {
topic.set("message_archived", true);
break;
}
case "stats": {
let updateStream = false;
["last_posted_at", "like_count", "posts_count"].forEach((property) => {
const value = data[property];
if (typeof value !== "undefined") {
topic.set(property, value);
updateStream = true;
}
});
if (data["last_poster"]) {
topic.details.set("last_poster", data["last_poster"]);
updateStream = true;
}
if (updateStream) {
postStream
.triggerChangedTopicStats()
.then((firstPostId) => refresh({ id: firstPostId }));
}
break;
}
default: {
let callback = customPostMessageCallbacks[data.type];
if (callback) {
callback(this, data);
} else {
// eslint-disable-next-line no-console
console.warn("unknown topic bus message type", data);
}
}
}
},
reply() {
@ -1781,7 +1775,7 @@ export default Controller.extend(bufferedProperty("model"), {
if (
this.siteSettings.automatically_unpin_topics &&
this.currentUser &&
this.currentUser.automatically_unpin_topics
this.currentUser.user_option.automatically_unpin_topics
) {
// automatically unpin topics when the user reaches the bottom
const max = Math.max(...postNumbers);

View File

@ -2,11 +2,27 @@ import Controller, { inject as controller } from "@ember/controller";
import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service";
import { alias, and, equal, readOnly } from "@ember/object/computed";
import { tracked } from "@glimmer/tracking";
import { cached, tracked } from "@glimmer/tracking";
import I18n from "I18n";
import DiscourseURL from "discourse/lib/url";
export const PERSONAL_INBOX = "__personal_inbox__";
const customUserNavMessagesDropdownRows = [];
export function registerCustomUserNavMessagesDropdownRow(
routeName,
name,
icon
) {
customUserNavMessagesDropdownRows.push({
routeName,
name,
icon,
});
}
export function resetCustomUserNavMessagesDropdownRows() {
customUserNavMessagesDropdownRows.length = 0;
}
export default class extends Controller {
@service router;
@ -25,19 +41,21 @@ export default class extends Controller {
@readOnly("site.can_tag_pms") pmTaggingEnabled;
get messagesDropdownValue() {
const parentRoute = this.router.currentRoute.parent;
let value;
if (Object.keys(parentRoute.params).length > 0) {
return this.router.urlFor(
parentRoute.name,
this.model.username,
parentRoute.params
);
} else {
return this.router.urlFor(parentRoute.name, this.model.username);
for (let i = this.messagesDropdownContent.length - 1; i >= 0; i--) {
const row = this.messagesDropdownContent[i];
if (this.router.currentURL.includes(row.id)) {
value = row.id;
break;
}
}
return value;
}
@cached
get messagesDropdownContent() {
const content = [
{
@ -66,6 +84,14 @@ export default class extends Controller {
});
}
customUserNavMessagesDropdownRows.forEach((row) => {
content.push({
id: this.router.urlFor(row.routeName, this.model.username),
name: row.name,
icon: row.icon,
});
});
return content;
}

View File

@ -84,7 +84,7 @@ export default Controller.extend(ModalFunctionality, {
},
_buildTimeShortcuts() {
const timezone = this.currentUser.timezone;
const timezone = this.currentUser.user_option.timezone;
const shortcuts = timeShortcuts(timezone);
return [shortcuts.oneHour(), shortcuts.twoHours(), shortcuts.tomorrow()];
},

View File

@ -1,7 +1,6 @@
import { helperContext, registerUnbound } from "discourse-common/lib/helpers";
import Category from "discourse/models/category";
import I18n from "I18n";
import Site from "discourse/models/site";
import { escapeExpression } from "discourse/lib/utilities";
import { get } from "@ember/object";
import getURL from "discourse-common/lib/get-url";
@ -39,13 +38,13 @@ export function addExtraIconRenderer(renderer) {
@param {Number} [opts.depth] Current category depth, used for limiting recursive calls
**/
export function categoryBadgeHTML(category, opts) {
let siteSettings = helperContext().siteSettings;
const { site, siteSettings } = helperContext();
opts = opts || {};
if (
!category ||
(!opts.allowUncategorized &&
get(category, "id") === Site.currentProp("uncategorized_category_id") &&
get(category, "id") === site.uncategorized_category_id &&
siteSettings.suppress_uncategorized_badge)
) {
return "";

View File

@ -1,4 +1,5 @@
import EmberObject from "@ember/object";
import { bind } from "discourse-common/utils/decorators";
import PreloadStore from "discourse/lib/preload-store";
export default {
@ -6,14 +7,21 @@ export default {
after: "message-bus",
initialize(container) {
const site = container.lookup("service:site");
this.site = container.lookup("service:site");
this.messageBus = container.lookup("service:message-bus");
const banner = EmberObject.create(PreloadStore.get("banner") || {});
const messageBus = container.lookup("service:message-bus");
this.site.set("banner", banner);
site.set("banner", banner);
this.messageBus.subscribe("/site/banner", this.onMessage);
},
messageBus.subscribe("/site/banner", (data) => {
site.set("banner", EmberObject.create(data || {}));
});
teardown() {
this.messageBus.unsubscribe("/site/banner", this.onMessage);
},
@bind
onMessage(data = {}) {
this.site.set("banner", EmberObject.create(data));
},
};

View File

@ -1,13 +1,14 @@
import DiscourseURL from "discourse/lib/url";
import { isDevelopment } from "discourse-common/config/environment";
import discourseLater from "discourse-common/lib/later";
import { bind } from "discourse-common/utils/decorators";
// Use the message bus for live reloading of components for faster development.
export default {
name: "live-development",
initialize(container) {
const messageBus = container.lookup("service:message-bus");
this.messageBus = container.lookup("service:message-bus");
const session = container.lookup("service:session");
// Preserve preview_theme_id=## and pp=async-flamegraph parameters across pages
@ -38,37 +39,46 @@ export default {
}
// Observe file changes
messageBus.subscribe(
this.messageBus.subscribe(
"/file-change",
(data) => {
data.forEach((me) => {
if (me === "refresh") {
// Refresh if necessary
document.location.reload(true);
} else if (me.new_href && me.target) {
const link_target = !!me.theme_id
? `[data-target='${me.target}'][data-theme-id='${me.theme_id}']`
: `[data-target='${me.target}']`;
const links = document.querySelectorAll(`link${link_target}`);
if (links.length > 0) {
const lastLink = links[links.length - 1];
// this check is useful when message-bus has multiple file updates
// it avoids the browser doing a lot of work for nothing
// should the filenames be unchanged
if (
lastLink.href.split("/").pop() !== me.new_href.split("/").pop()
) {
this.refreshCSS(lastLink, me.new_href);
}
}
}
});
},
this.onFileChange,
session.mbLastFileChangeId
);
},
teardown() {
this.messageBus.unsubscribe("/file-change", this.onFileChange);
},
@bind
onFileChange(data) {
data.forEach((me) => {
if (me === "refresh") {
// Refresh if necessary
document.location.reload(true);
} else if (me.new_href && me.target) {
let query = `link[data-target='${me.target}']`;
if (me.theme_id) {
query += `[data-theme-id='${me.theme_id}']`;
}
const links = document.querySelectorAll(query);
if (links.length > 0) {
const lastLink = links[links.length - 1];
// this check is useful when message-bus has multiple file updates
// it avoids the browser doing a lot of work for nothing
// should the filenames be unchanged
if (lastLink.href.split("/").pop() !== me.new_href.split("/").pop()) {
this.refreshCSS(lastLink, me.new_href);
}
}
}
});
},
refreshCSS(node, newHref) {
const reloaded = node.cloneNode(true);
reloaded.href = newHref;

View File

@ -1,35 +1,39 @@
import I18n from "I18n";
import logout from "discourse/lib/logout";
import { bind } from "discourse-common/utils/decorators";
let _showingLogout = false;
// Subscribe to "logout" change events via the Message Bus
// Subscribe to "logout" change events via the Message Bus
export default {
name: "logout",
after: "message-bus",
initialize(container) {
const messageBus = container.lookup("service:message-bus"),
dialog = container.lookup("service:dialog");
this.messageBus = container.lookup("service:message-bus");
this.dialog = container.lookup("service:dialog");
if (!messageBus) {
this.messageBus.subscribe("/logout", this.onMessage);
},
teardown() {
this.messageBus.unsubscribe("/logout", this.onMessage);
},
@bind
onMessage() {
if (_showingLogout) {
return;
}
messageBus.subscribe("/logout", function () {
if (!_showingLogout) {
_showingLogout = true;
_showingLogout = true;
dialog.alert({
message: I18n.t("logout"),
confirmButtonLabel: "home",
didConfirm: logout,
didCancel: logout,
shouldDisplayCancel: false,
});
}
_showingLogout = true;
this.dialog.alert({
message: I18n.t("logout"),
confirmButtonLabel: "home",
didConfirm: logout,
didCancel: logout,
shouldDisplayCancel: false,
});
},
};

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