Version bump
This commit is contained in:
commit
2672410743
95
.eslintrc
95
.eslintrc
@ -1,96 +1,3 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"builtin": true,
|
||||
"es6": true,
|
||||
"jasmine": true,
|
||||
"mocha": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 7,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"globals": {
|
||||
"$": true,
|
||||
"_": true,
|
||||
"andThen": true,
|
||||
"asyncRender": true,
|
||||
"Blob": true,
|
||||
"bootbox": true,
|
||||
"click": true,
|
||||
"waitUntil": true,
|
||||
"getSettledState": true,
|
||||
"count": true,
|
||||
"currentPath": true,
|
||||
"currentRouteName": true,
|
||||
"currentURL": true,
|
||||
"define": true,
|
||||
"Discourse": true,
|
||||
"Ember": true,
|
||||
"exists": true,
|
||||
"File": true,
|
||||
"fillIn": true,
|
||||
"find": true,
|
||||
"Handlebars": true,
|
||||
"hasModule": true,
|
||||
"I18n": true,
|
||||
"invisible": true,
|
||||
"jQuery": true,
|
||||
"keyboardHelper": true,
|
||||
"keyEvent": true,
|
||||
"moduleFor": true,
|
||||
"moduleForComponent": true,
|
||||
"moment": true,
|
||||
"Pretender": true,
|
||||
"QUnit": true,
|
||||
"require": true,
|
||||
"requirejs": true,
|
||||
"RSVP": true,
|
||||
"sandbox": true,
|
||||
"sinon": true,
|
||||
"test": true,
|
||||
"triggerEvent": true,
|
||||
"visible": true,
|
||||
"visit": true,
|
||||
"pauseTest": true
|
||||
},
|
||||
"rules": {
|
||||
"block-scoped-var": 2,
|
||||
"dot-notation": 0,
|
||||
"eqeqeq": [2, "allow-null"],
|
||||
"guard-for-in": 2,
|
||||
"no-alert": 2,
|
||||
"no-bitwise": 2,
|
||||
"no-caller": 2,
|
||||
"no-cond-assign": 0,
|
||||
"no-console": 2,
|
||||
"no-debugger": 2,
|
||||
"no-empty": 0,
|
||||
"no-eval": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-parens": 0,
|
||||
"no-inner-declarations": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-iterator": 2,
|
||||
"no-loop-func": 2,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-new": 2,
|
||||
"no-plusplus": 0,
|
||||
"no-proto": 2,
|
||||
"no-script-url": 2,
|
||||
"no-sequences": 2,
|
||||
"no-shadow": 2,
|
||||
"no-this-before-super": 2,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-undef": 2,
|
||||
"no-unused-vars": 2,
|
||||
"no-with": 2,
|
||||
"semi": 2,
|
||||
"strict": 0,
|
||||
"valid-typeof": 2,
|
||||
"wrap-iife": [2, "inside"]
|
||||
},
|
||||
"parser": "babel-eslint"
|
||||
"extends": "eslint-config-discourse"
|
||||
}
|
||||
|
||||
8
Gemfile
8
Gemfile
@ -26,6 +26,10 @@ else
|
||||
gem 'sprockets-rails'
|
||||
end
|
||||
|
||||
# this will eventually be added to rails,
|
||||
# allows us to precompile all our templates in the unicorn master
|
||||
gem 'actionview_precompiler', require: false
|
||||
|
||||
gem 'seed-fu'
|
||||
|
||||
gem 'mail', require: false
|
||||
@ -46,7 +50,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.9.13'
|
||||
gem 'onebox', '1.9.17'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -114,6 +118,8 @@ gem 'execjs', require: false
|
||||
gem 'mini_racer'
|
||||
gem 'highline', '~> 1.7.0', require: false
|
||||
gem 'rack-protection' # security
|
||||
gem 'cbor', require: false
|
||||
gem 'cose', require: false
|
||||
|
||||
# Gems used only for assets and not required in production environments by default.
|
||||
# Allow everywhere for now cause we are allowing asset debugging in production
|
||||
|
||||
18
Gemfile.lock
18
Gemfile.lock
@ -20,6 +20,8 @@ GEM
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
actionview_precompiler (0.2.1)
|
||||
actionview (>= 6.0.a)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
activejob (6.0.0)
|
||||
@ -77,12 +79,15 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
byebug (11.0.1)
|
||||
cbor (0.5.9.6)
|
||||
certified (1.0.0)
|
||||
chunky_png (1.3.11)
|
||||
coderay (1.1.2)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.1.5)
|
||||
connection_pool (2.2.2)
|
||||
cose (0.9.0)
|
||||
cbor (~> 0.5.9)
|
||||
cppjieba_rb (0.3.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
@ -92,7 +97,7 @@ GEM
|
||||
debug_inspector (0.0.3)
|
||||
diff-lcs (1.3)
|
||||
diffy (3.3.0)
|
||||
discourse-ember-source (3.10.0.1)
|
||||
discourse-ember-source (3.10.0.2)
|
||||
discourse_image_optim (0.26.2)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
fspath (~> 3.0)
|
||||
@ -168,7 +173,7 @@ GEM
|
||||
logstash-event (1.2.02)
|
||||
logstash-logger (0.26.1)
|
||||
logstash-event (~> 1.2)
|
||||
logster (2.3.2)
|
||||
logster (2.3.3)
|
||||
loofah (2.2.3)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
@ -240,7 +245,7 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.9.13)
|
||||
onebox (1.9.17)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -355,7 +360,7 @@ GEM
|
||||
ruby_dep (1.5.0)
|
||||
rubyzip (2.0.0)
|
||||
safe_yaml (1.0.5)
|
||||
sanitize (5.0.0)
|
||||
sanitize (5.1.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.8.0)
|
||||
nokogumbo (~> 2.0)
|
||||
@ -425,6 +430,7 @@ DEPENDENCIES
|
||||
actionmailer (= 6.0.0)
|
||||
actionpack (= 6.0.0)
|
||||
actionview (= 6.0.0)
|
||||
actionview_precompiler
|
||||
active_model_serializers (~> 0.8.3)
|
||||
activemodel (= 6.0.0)
|
||||
activerecord (= 6.0.0)
|
||||
@ -438,8 +444,10 @@ DEPENDENCIES
|
||||
bootsnap
|
||||
bullet
|
||||
byebug
|
||||
cbor
|
||||
certified
|
||||
colored2
|
||||
cose
|
||||
cppjieba_rb
|
||||
css_parser
|
||||
diffy
|
||||
@ -493,7 +501,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.9.13)
|
||||
onebox (= 1.9.17)
|
||||
openid-redis-store
|
||||
parallel_tests
|
||||
pg
|
||||
|
||||
46
app/assets/javascripts/admin-login/admin-login.js.es6
Normal file
46
app/assets/javascripts/admin-login/admin-login.js.es6
Normal file
@ -0,0 +1,46 @@
|
||||
import { getWebauthnCredential } from "discourse/lib/webauthn";
|
||||
|
||||
export default function() {
|
||||
document.getElementById(
|
||||
"activate-security-key-alternative"
|
||||
).onclick = function() {
|
||||
document.getElementById("second-factor-forms").style.display = "block";
|
||||
document.getElementById("primary-security-key-form").style.display = "none";
|
||||
};
|
||||
|
||||
document.getElementById("submit-security-key").onclick = function(e) {
|
||||
e.preventDefault();
|
||||
getWebauthnCredential(
|
||||
document.getElementById("security-key-challenge").value,
|
||||
document
|
||||
.getElementById("security-key-allowed-credential-ids")
|
||||
.value.split(","),
|
||||
credentialData => {
|
||||
document.getElementById(
|
||||
"security-key-credential"
|
||||
).value = JSON.stringify(credentialData);
|
||||
e.target.parentElement.submit();
|
||||
},
|
||||
errorMessage => {
|
||||
document.getElementById("security-key-error").innerText = errorMessage;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const useTotp = I18n.t("login.second_factor_toggle.totp");
|
||||
const useBackup = I18n.t("login.second_factor_toggle.backup_code");
|
||||
const backupForm = document.getElementById("backup-second-factor-form");
|
||||
const primaryForm = document.getElementById("primary-second-factor-form");
|
||||
document.getElementById("toggle-form").onclick = function(event) {
|
||||
event.preventDefault();
|
||||
if (backupForm.style.display === "none") {
|
||||
backupForm.style.display = "block";
|
||||
primaryForm.style.display = "none";
|
||||
document.getElementById("toggle-form").innerHTML = useTotp;
|
||||
} else {
|
||||
backupForm.style.display = "none";
|
||||
primaryForm.style.display = "block";
|
||||
document.getElementById("toggle-form").innerHTML = useBackup;
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
require("admin-login/admin-login").default();
|
||||
@ -33,6 +33,10 @@ export default Ember.Component.extend({
|
||||
|
||||
_scheduleChartRendering() {
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._renderChart(
|
||||
this.model,
|
||||
this.element.querySelector(".chart-canvas")
|
||||
|
||||
@ -243,12 +243,12 @@ const AdminUser = Discourse.User.extend({
|
||||
this.set("originalTrustLevel", this.trust_level);
|
||||
},
|
||||
|
||||
dirty: propertyNotEqual("originalTrustLevel", "trustLevel.id"),
|
||||
dirty: propertyNotEqual("originalTrustLevel", "trust_level"),
|
||||
|
||||
saveTrustLevel() {
|
||||
return ajax(`/admin/users/${this.id}/trust_level`, {
|
||||
type: "PUT",
|
||||
data: { level: this.get("trustLevel.id") }
|
||||
data: { level: this.trust_level }
|
||||
})
|
||||
.then(() => window.location.reload())
|
||||
.catch(e => {
|
||||
@ -266,7 +266,7 @@ const AdminUser = Discourse.User.extend({
|
||||
},
|
||||
|
||||
restoreTrustLevel() {
|
||||
this.set("trustLevel.id", this.originalTrustLevel);
|
||||
this.set("trust_level", this.originalTrustLevel);
|
||||
},
|
||||
|
||||
lockTrustLevel(locked) {
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
<thead>
|
||||
<th>{{i18n "admin.api.key"}}</th>
|
||||
<th>{{i18n "admin.api.user"}}</th>
|
||||
<th>{{i18n "admin.api.created"}}</th>
|
||||
<th>{{i18n "admin.api.last_used"}}</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -10,6 +12,7 @@
|
||||
<tr>
|
||||
<td class="key">{{k.key}}</td>
|
||||
<td class="key-user">
|
||||
<div class="label">{{i18n 'admin.api.user'}}</div>
|
||||
{{#if k.user}}
|
||||
{{#link-to "adminUser" k.user}}
|
||||
{{avatar k.user imageSize="small"}}
|
||||
@ -18,6 +21,18 @@
|
||||
{{i18n "admin.api.all_users"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="key-created">
|
||||
<div class="label">{{i18n 'admin.api.created'}}</div>
|
||||
{{format-date k.created_at}}
|
||||
</td>
|
||||
<td class="key-last-used">
|
||||
<div class="label">{{i18n 'admin.api.last_used'}}</div>
|
||||
{{#if k.last_used_at}}
|
||||
{{format-date k.last_used_at}}
|
||||
{{else}}
|
||||
{{i18n "admin.api.never_used"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="key-controls">
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div class='field'>{{i18n name}}</div>
|
||||
<div class='value'>
|
||||
{{#if editing}}
|
||||
{{text-field value=buffer autofocus="autofocus"}}
|
||||
{{text-field value=buffer autofocus="autofocus" autocomplete="discourse"}}
|
||||
{{else}}
|
||||
<span {{action "edit"}}>{{value}}</span>
|
||||
{{/if}}
|
||||
|
||||
@ -40,9 +40,8 @@
|
||||
{{#each colors as |c|}}
|
||||
<tr class="{{if c.changed 'changed'}} {{if c.valid 'valid' 'invalid'}}">
|
||||
<td class="name" title={{c.name}}>
|
||||
<b>{{c.translatedName}}</b>
|
||||
<br/>
|
||||
<span class="description">{{c.description}}</span>
|
||||
<h3>{{c.translatedName}}</h3>
|
||||
<p class="description">{{c.description}}</p>
|
||||
</td>
|
||||
<td class="hex">{{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}}</td>
|
||||
<td class="actions">
|
||||
|
||||
@ -369,8 +369,9 @@
|
||||
<div class="value">
|
||||
{{combo-box
|
||||
content=site.trustLevels
|
||||
value=model.trustLevel.id
|
||||
nameProperty="detailedName"}}
|
||||
value=model.trust_level
|
||||
nameProperty="detailedName"
|
||||
}}
|
||||
|
||||
{{#if model.dirty}}
|
||||
<div>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
//= require ./discourse/lib/hash
|
||||
//= require ./discourse/lib/load-script
|
||||
//= require ./discourse/lib/notification-levels
|
||||
//= require ./discourse/lib/app-events
|
||||
//= require ./discourse/services/app-events
|
||||
//= require ./discourse/lib/offset-calculator
|
||||
//= require ./discourse/lib/lock-on
|
||||
//= require ./discourse/lib/url
|
||||
|
||||
@ -1,40 +1,43 @@
|
||||
function gotFocus() {
|
||||
if (!Discourse.get("hasFocus")) {
|
||||
Discourse.setProperties({ hasFocus: true, notify: false });
|
||||
}
|
||||
}
|
||||
|
||||
function lostFocus() {
|
||||
if (Discourse.get("hasFocus")) {
|
||||
Discourse.set("hasFocus", false);
|
||||
}
|
||||
}
|
||||
|
||||
let onchange;
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
ready() {
|
||||
this._super(...arguments);
|
||||
|
||||
onchange = () => {
|
||||
document.visibilityState === "hidden" ? lostFocus() : gotFocus();
|
||||
};
|
||||
this._onChangeHandler = Ember.run.bind(this, this._onChange);
|
||||
|
||||
// Default to true
|
||||
Discourse.set("hasFocus", true);
|
||||
|
||||
document.addEventListener("visibilitychange", onchange);
|
||||
document.addEventListener("resume", onchange);
|
||||
document.addEventListener("freeze", onchange);
|
||||
document.addEventListener("visibilitychange", this._onChangeHandler);
|
||||
document.addEventListener("resume", this._onChangeHandler);
|
||||
document.addEventListener("freeze", this._onChangeHandler);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this._super(...arguments);
|
||||
|
||||
document.removeEventListener("visibilitychange", onchange);
|
||||
document.removeEventListener("resume", onchange);
|
||||
document.removeEventListener("freeze", onchange);
|
||||
document.removeEventListener("visibilitychange", this._onChangeHandler);
|
||||
document.removeEventListener("resume", this._onChangeHandler);
|
||||
document.removeEventListener("freeze", this._onChangeHandler);
|
||||
|
||||
onchange = undefined;
|
||||
this._onChangeHandler = null;
|
||||
},
|
||||
|
||||
_onChange() {
|
||||
const container = getOwner(this);
|
||||
const appEvents = container.lookup("app-events:main");
|
||||
|
||||
if (document.visibilityState === "hidden") {
|
||||
if (Discourse.hasFocus) {
|
||||
Discourse.set("hasFocus", false);
|
||||
appEvents.trigger("discourse:focus-changed", false);
|
||||
}
|
||||
} else {
|
||||
if (!Discourse.hasFocus) {
|
||||
Discourse.set("hasFocus", true);
|
||||
appEvents.trigger("discourse:focus-changed", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { findHelper } from "discourse-common/lib/helpers";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
|
||||
/* global requirejs, require */
|
||||
var classify = Ember.String.classify;
|
||||
@ -45,6 +46,14 @@ export function buildResolver(baseName) {
|
||||
},
|
||||
|
||||
normalize(fullName) {
|
||||
if (fullName === "app-events:main") {
|
||||
deprecated(
|
||||
"`app-events:main` has been replaced with `service:app-events`",
|
||||
{ since: "2.4.0" }
|
||||
);
|
||||
return "service:app-events";
|
||||
}
|
||||
|
||||
const split = fullName.split(":");
|
||||
if (split.length > 1) {
|
||||
const appBase = `${baseName}/${split[0]}s/`;
|
||||
|
||||
@ -92,14 +92,6 @@ const Discourse = Ember.Application.extend(FocusEvent, {
|
||||
}
|
||||
},
|
||||
|
||||
// The classes of buttons to show on a post
|
||||
@computed
|
||||
postButtons() {
|
||||
return Discourse.SiteSettings.post_menu.split("|").map(function(i) {
|
||||
return i.replace(/\+/, "").capitalize();
|
||||
});
|
||||
},
|
||||
|
||||
updateContextCount(count) {
|
||||
this.set("contextCount", count);
|
||||
},
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { formatUsername, escapeExpression } from "discourse/lib/utilities";
|
||||
import { normalize } from "discourse/components/user-info";
|
||||
import { renderAvatar } from "discourse/helpers/user-avatar";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
usersTemplates: Ember.computed("users.[]", function() {
|
||||
return (this.users || []).map(user => {
|
||||
let name = "";
|
||||
if (user.name && normalize(user.username) !== normalize(user.name)) {
|
||||
name = user.name;
|
||||
}
|
||||
|
||||
return {
|
||||
username: user.username,
|
||||
name,
|
||||
userPath: userPath(user.username),
|
||||
avatar: renderAvatar(user, { imageSize: "large" }),
|
||||
title: escapeExpression(user.title || ""),
|
||||
formatedUsername: formatUsername(user.username)
|
||||
};
|
||||
});
|
||||
})
|
||||
});
|
||||
@ -7,6 +7,7 @@ import afterTransition from "discourse/lib/after-transition";
|
||||
import positioningWorkaround from "discourse/lib/safari-hacks";
|
||||
import { headerHeight } from "discourse/components/site-header";
|
||||
import KeyEnterEscape from "discourse/mixins/key-enter-escape";
|
||||
import { iOSWithVisualViewport } from "discourse/lib/utilities";
|
||||
|
||||
const START_EVENTS = "touchstart mousedown";
|
||||
const DRAG_EVENTS = "touchmove mousemove";
|
||||
@ -132,7 +133,7 @@ export default Ember.Component.extend(KeyEnterEscape, {
|
||||
$document.on(END_EVENTS, endDrag);
|
||||
});
|
||||
|
||||
if (window.visualViewport !== undefined) {
|
||||
if (iOSWithVisualViewport()) {
|
||||
this.viewportResize();
|
||||
window.visualViewport.addEventListener("resize", this.viewportResize);
|
||||
}
|
||||
@ -141,11 +142,6 @@ export default Ember.Component.extend(KeyEnterEscape, {
|
||||
viewportResize() {
|
||||
const composerVH = window.visualViewport.height * 0.01;
|
||||
|
||||
if (window.visualViewport.height !== window.innerHeight) {
|
||||
document.documentElement.classList.add("keyboard-visible");
|
||||
} else {
|
||||
document.documentElement.classList.remove("keyboard-visible");
|
||||
}
|
||||
document.documentElement.style.setProperty(
|
||||
"--composer-vh",
|
||||
`${composerVH}px`
|
||||
@ -174,7 +170,7 @@ export default Ember.Component.extend(KeyEnterEscape, {
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.appEvents.off("composer:resize", this, this.resize);
|
||||
if (window.visualViewport !== undefined) {
|
||||
if (iOSWithVisualViewport()) {
|
||||
window.visualViewport.removeEventListener("resize", this.viewportResize);
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import { observes, on } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [":d-editor-modal", "hidden"],
|
||||
|
||||
@observes("hidden")
|
||||
_hiddenChanged() {
|
||||
if (!this.hidden) {
|
||||
Ember.run.scheduleOnce("afterRender", () => {
|
||||
const $modal = $(this.element);
|
||||
const $parent = $(this.element).closest(".d-editor");
|
||||
const w = $parent.width();
|
||||
const h = $parent.height();
|
||||
const dir = $("html").css("direction") === "rtl" ? "right" : "left";
|
||||
const offset = w / 2 - $modal.outerWidth() / 2;
|
||||
$modal.css(dir, offset + "px");
|
||||
parent
|
||||
.$(".d-editor-overlay")
|
||||
.removeClass("hidden")
|
||||
.css({ width: w, height: h });
|
||||
this.$("input:eq(0)").focus();
|
||||
});
|
||||
} else {
|
||||
parent.$(".d-editor-overlay").addClass("hidden");
|
||||
}
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
_listenKeys() {
|
||||
$(this.element).on("keydown.d-modal", key => {
|
||||
if (this.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.keyCode === 27) {
|
||||
this.send("cancel");
|
||||
return false;
|
||||
}
|
||||
if (key.keyCode === 13) {
|
||||
this.send("ok");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
_stopListening() {
|
||||
$(this.element).off("keydown.d-modal");
|
||||
},
|
||||
|
||||
actions: {
|
||||
ok() {
|
||||
this.set("hidden", true);
|
||||
this.okAction();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.set("hidden", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -21,6 +21,7 @@ import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { translations } from "pretty-text/emoji/data";
|
||||
import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
|
||||
import { emojiUrlFor } from "discourse/lib/text";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
@ -89,7 +90,7 @@ class Toolbar {
|
||||
id: "link",
|
||||
group: "insertions",
|
||||
shortcut: "K",
|
||||
action: (...args) => this.context.send("showLinkModal", args)
|
||||
sendAction: event => this.context.send("showLinkModal", event)
|
||||
});
|
||||
}
|
||||
|
||||
@ -213,9 +214,6 @@ export function onToolbarCreate(func) {
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["d-editor"],
|
||||
ready: false,
|
||||
insertLinkHidden: true,
|
||||
linkUrl: "",
|
||||
linkText: "",
|
||||
lastSel: null,
|
||||
_mouseTrap: null,
|
||||
showLink: true,
|
||||
@ -946,21 +944,23 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
showLinkModal() {
|
||||
showLinkModal(toolbarEvent) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("linkUrl", "");
|
||||
this.set("linkText", "");
|
||||
|
||||
let linkText = "";
|
||||
this._lastSel = this._getSelected();
|
||||
|
||||
if (this._lastSel) {
|
||||
this.set("linkText", this._lastSel.value.trim());
|
||||
linkText = this._lastSel.value.trim();
|
||||
}
|
||||
|
||||
this.set("insertLinkHidden", false);
|
||||
showModal("insert-hyperlink").setProperties({
|
||||
linkText: linkText,
|
||||
_lastSel: this._lastSel,
|
||||
toolbarEvent
|
||||
});
|
||||
},
|
||||
|
||||
formatCode() {
|
||||
@ -1004,29 +1004,6 @@ export default Ember.Component.extend({
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
insertLink() {
|
||||
const origLink = this.linkUrl;
|
||||
const linkUrl =
|
||||
origLink.indexOf("://") === -1 ? `http://${origLink}` : origLink;
|
||||
const sel = this._lastSel;
|
||||
|
||||
if (Ember.isEmpty(linkUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkText = this.linkText || "";
|
||||
if (linkText.length) {
|
||||
this._addText(sel, `[${linkText}](${linkUrl})`);
|
||||
} else {
|
||||
if (sel.value) {
|
||||
this._addText(sel, `[${sel.value}](${linkUrl})`);
|
||||
} else {
|
||||
this._addText(sel, `[${origLink}](${linkUrl})`);
|
||||
this._selectText(sel.start + 1, origLink.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -33,6 +33,10 @@ export default Ember.Component.extend({
|
||||
if (e.which === 27 && this.dismissable) {
|
||||
Ember.run.next(() => $(".modal-header a.close").click());
|
||||
}
|
||||
|
||||
if (e.which === 13 && this.triggerClickOnEnter(e)) {
|
||||
Ember.run.next(() => $(".modal-footer .btn-primary").click());
|
||||
}
|
||||
});
|
||||
|
||||
this.appEvents.on("modal:body-shown", this, "_modalBodyShown");
|
||||
@ -44,6 +48,18 @@ export default Ember.Component.extend({
|
||||
this.appEvents.off("modal:body-shown", this, "_modalBodyShown");
|
||||
},
|
||||
|
||||
triggerClickOnEnter(e) {
|
||||
// skip when in a form or a textarea element
|
||||
if (
|
||||
e.target.closest("form") ||
|
||||
(document.activeElement && document.activeElement.nodeName === "TEXTAREA")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
mouseDown(e) {
|
||||
if (!this.dismissable) {
|
||||
return;
|
||||
|
||||
@ -110,6 +110,7 @@ export default Ember.Component.extend(
|
||||
}
|
||||
);
|
||||
|
||||
this.appEvents.on("discourse:focus-changed", this, "gotFocus");
|
||||
this.appEvents.on("post:highlight", this, "_highlightPost");
|
||||
this.appEvents.on("header:update-topic", this, "_updateTopic");
|
||||
},
|
||||
@ -129,13 +130,13 @@ export default Ember.Component.extend(
|
||||
|
||||
// this happens after route exit, stuff could have trickled in
|
||||
this._hideTopicInHeader();
|
||||
this.appEvents.off("discourse:focus-changed", this, "gotFocus");
|
||||
this.appEvents.off("post:highlight", this, "_highlightPost");
|
||||
this.appEvents.off("header:update-topic", this, "_updateTopic");
|
||||
},
|
||||
|
||||
@observes("Discourse.hasFocus")
|
||||
gotFocus() {
|
||||
if (Discourse.get("hasFocus")) {
|
||||
gotFocus(hasFocus) {
|
||||
if (hasFocus) {
|
||||
this.scrolled();
|
||||
}
|
||||
},
|
||||
|
||||
@ -16,6 +16,13 @@ export default Ember.Component.extend({
|
||||
this.set("actions", connectorClass.actions);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
const connectorClass = this.get("connector.connectorClass");
|
||||
connectorClass.teardownComponent.call(this, this);
|
||||
},
|
||||
|
||||
@observes("args")
|
||||
_argsChanged() {
|
||||
const args = this.args || {};
|
||||
|
||||
@ -4,16 +4,26 @@ import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
export default Ember.Component.extend({
|
||||
@computed("secondFactorMethod")
|
||||
secondFactorTitle(secondFactorMethod) {
|
||||
return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP
|
||||
? I18n.t("login.second_factor_title")
|
||||
: I18n.t("login.second_factor_backup_title");
|
||||
switch (secondFactorMethod) {
|
||||
case SECOND_FACTOR_METHODS.TOTP:
|
||||
return I18n.t("login.second_factor_title");
|
||||
case SECOND_FACTOR_METHODS.SECURITY_KEY:
|
||||
return I18n.t("login.second_factor_title");
|
||||
case SECOND_FACTOR_METHODS.BACKUP_CODE:
|
||||
return I18n.t("login.second_factor_backup_title");
|
||||
}
|
||||
},
|
||||
|
||||
@computed("secondFactorMethod")
|
||||
secondFactorDescription(secondFactorMethod) {
|
||||
return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP
|
||||
? I18n.t("login.second_factor_description")
|
||||
: I18n.t("login.second_factor_backup_description");
|
||||
switch (secondFactorMethod) {
|
||||
case SECOND_FACTOR_METHODS.TOTP:
|
||||
return I18n.t("login.second_factor_description");
|
||||
case SECOND_FACTOR_METHODS.SECURITY_KEY:
|
||||
return I18n.t("login.security_key_description");
|
||||
case SECOND_FACTOR_METHODS.BACKUP_CODE:
|
||||
return I18n.t("login.second_factor_backup_description");
|
||||
}
|
||||
},
|
||||
|
||||
@computed("secondFactorMethod", "isLogin")
|
||||
@ -29,6 +39,13 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@computed("backupEnabled", "secondFactorMethod")
|
||||
showToggleMethodLink(backupEnabled, secondFactorMethod) {
|
||||
return (
|
||||
backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
);
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleSecondFactorMethod() {
|
||||
const secondFactorMethod = this.secondFactorMethod;
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
actions: {
|
||||
useAnotherMethod() {
|
||||
this.set("showSecurityKey", false);
|
||||
this.set("showSecondFactor", true);
|
||||
this.set("secondFactorMethod", SECOND_FACTOR_METHODS.TOTP);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -161,7 +161,7 @@ export default Ember.Component.extend({
|
||||
: $("#topic-progress").outerHeight();
|
||||
const maximumOffset = $("#topic-bottom").offset().top + progressHeight;
|
||||
const windowHeight = $(window).height();
|
||||
const composerHeight = $("#reply-control").height() || 0;
|
||||
let composerHeight = $("#reply-control").height() || 0;
|
||||
const isDocked = offset >= maximumOffset - windowHeight + composerHeight;
|
||||
let bottom = $("body").height() - maximumOffset;
|
||||
|
||||
@ -170,8 +170,15 @@ export default Ember.Component.extend({
|
||||
bottom += $iPadFooterNav.outerHeight();
|
||||
}
|
||||
const wrapperDir = $html.hasClass("rtl") ? "left" : "right";
|
||||
const draftComposerHeight = 40;
|
||||
|
||||
if (composerHeight > 0) {
|
||||
const $iPhoneFooterNav = $(".footer-nav-visible .footer-nav");
|
||||
const $replyDraft = $("#reply-control.draft");
|
||||
if ($iPhoneFooterNav.outerHeight() && $replyDraft.outerHeight()) {
|
||||
composerHeight =
|
||||
$replyDraft.outerHeight() + $iPhoneFooterNav.outerHeight();
|
||||
}
|
||||
$wrapper.css("bottom", isDocked ? bottom : composerHeight);
|
||||
} else {
|
||||
$wrapper.css("bottom", isDocked ? bottom : "");
|
||||
@ -185,6 +192,11 @@ export default Ember.Component.extend({
|
||||
} else {
|
||||
$wrapper.css(wrapperDir, "1em");
|
||||
}
|
||||
|
||||
$wrapper.css(
|
||||
"margin-bottom",
|
||||
!isDocked && composerHeight > draftComposerHeight ? "0px" : ""
|
||||
);
|
||||
},
|
||||
|
||||
click(e) {
|
||||
|
||||
@ -3,7 +3,14 @@ import Docking from "discourse/mixins/docking";
|
||||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
import optionalService from "discourse/lib/optional-service";
|
||||
|
||||
const headerPadding = () => parseInt($("#main-outlet").css("padding-top")) + 3;
|
||||
const headerPadding = () => {
|
||||
let topPadding = parseInt($("#main-outlet").css("padding-top")) + 3;
|
||||
const iPadNavHeight = $(".footer-nav-ipad .footer-nav").height();
|
||||
if (iPadNavHeight) {
|
||||
topPadding += iPadNavHeight;
|
||||
}
|
||||
return topPadding;
|
||||
};
|
||||
|
||||
export default MountWidget.extend(Docking, {
|
||||
adminTools: optionalService(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
|
||||
function normalize(name) {
|
||||
export function normalize(name) {
|
||||
return name.replace(/[\-\_ \.]/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,40 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { getWebauthnCredential } from "discourse/lib/webauthn";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
|
||||
lockImageUrl: Discourse.getURL("/images/lock.svg"),
|
||||
|
||||
@computed("model")
|
||||
secondFactorRequired(model) {
|
||||
return model.security_key_required || model.second_factor_required;
|
||||
},
|
||||
|
||||
@computed("model")
|
||||
secondFactorMethod(model) {
|
||||
return model.security_key_required
|
||||
? SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
: SECOND_FACTOR_METHODS.TOTP;
|
||||
},
|
||||
|
||||
actions: {
|
||||
finishLogin() {
|
||||
let data = {};
|
||||
if (this.securityKeyCredential) {
|
||||
data = { security_key_credential: this.securityKeyCredential };
|
||||
} else {
|
||||
data = {
|
||||
second_factor_token: this.secondFactorToken,
|
||||
second_factor_method: this.secondFactorMethod
|
||||
};
|
||||
}
|
||||
ajax({
|
||||
url: `/session/email-login/${this.model.token}`,
|
||||
type: "POST",
|
||||
data: {
|
||||
second_factor_token: this.secondFactorToken,
|
||||
second_factor_method: this.secondFactorMethod
|
||||
}
|
||||
data: data
|
||||
})
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
@ -24,6 +44,19 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
authenticateSecurityKey() {
|
||||
getWebauthnCredential(
|
||||
this.model.challenge,
|
||||
this.model.allowed_credential_ids,
|
||||
credentialData => {
|
||||
this.set("securityKeyCredential", credentialData);
|
||||
this.send("finishLogin");
|
||||
},
|
||||
errorMessage => {
|
||||
this.set("model.error", errorMessage);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
linkUrl: "",
|
||||
linkText: "",
|
||||
|
||||
onShow() {
|
||||
Ember.run.next(() =>
|
||||
$(this)
|
||||
.find("input.link-url")
|
||||
.focus()
|
||||
);
|
||||
},
|
||||
|
||||
actions: {
|
||||
ok() {
|
||||
const origLink = this.linkUrl;
|
||||
const linkUrl =
|
||||
origLink.indexOf("://") === -1 ? `http://${origLink}` : origLink;
|
||||
const sel = this._lastSel;
|
||||
|
||||
if (Ember.isEmpty(linkUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkText = this.linkText || "";
|
||||
|
||||
if (linkText.length) {
|
||||
this.toolbarEvent.addText(`[${linkText}](${linkUrl})`);
|
||||
} else {
|
||||
if (sel.value) {
|
||||
this.toolbarEvent.addText(`[${sel.value}](${linkUrl})`);
|
||||
} else {
|
||||
this.toolbarEvent.addText(`[${origLink}](${linkUrl})`);
|
||||
this.toolbarEvent.selectText(sel.start + 1, origLink.length);
|
||||
}
|
||||
}
|
||||
this.set("linkUrl", "");
|
||||
this.set("linkText", "");
|
||||
this.send("closeModal");
|
||||
},
|
||||
cancel() {
|
||||
this.send("closeModal");
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -8,6 +8,7 @@ import { escapeExpression, areCookiesEnabled } from "discourse/lib/utilities";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
import { getWebauthnCredential } from "discourse/lib/webauthn";
|
||||
|
||||
// This is happening outside of the app via popup
|
||||
const AuthErrors = [
|
||||
@ -23,7 +24,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
forgotPassword: Ember.inject.controller(),
|
||||
application: Ember.inject.controller(),
|
||||
|
||||
authenticate: null,
|
||||
loggingIn: false,
|
||||
loggedIn: false,
|
||||
processingEmailLink: false,
|
||||
@ -38,24 +38,24 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
resetForm() {
|
||||
this.setProperties({
|
||||
authenticate: null,
|
||||
loggingIn: false,
|
||||
loggedIn: false,
|
||||
secondFactorRequired: false,
|
||||
showSecondFactor: false,
|
||||
showSecurityKey: false,
|
||||
showLoginButtons: true,
|
||||
awaitingApproval: false
|
||||
});
|
||||
},
|
||||
|
||||
@computed("showSecondFactor")
|
||||
credentialsClass(showSecondFactor) {
|
||||
return showSecondFactor ? "hidden" : "";
|
||||
@computed("showSecondFactor", "showSecurityKey")
|
||||
credentialsClass(showSecondFactor, showSecurityKey) {
|
||||
return showSecondFactor || showSecurityKey ? "hidden" : "";
|
||||
},
|
||||
|
||||
@computed("showSecondFactor")
|
||||
secondFactorClass(showSecondFactor) {
|
||||
return showSecondFactor ? "" : "hidden";
|
||||
@computed("showSecondFactor", "showSecurityKey")
|
||||
secondFactorClass(showSecondFactor, showSecurityKey) {
|
||||
return showSecondFactor || showSecurityKey ? "" : "hidden";
|
||||
},
|
||||
|
||||
@computed("awaitingApproval", "hasAtLeastOneLoginButton")
|
||||
@ -66,6 +66,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
return classes.join(" ");
|
||||
},
|
||||
|
||||
@computed("showSecondFactor", "showSecurityKey")
|
||||
disableLoginFields(showSecondFactor, showSecurityKey) {
|
||||
return showSecondFactor || showSecurityKey;
|
||||
},
|
||||
|
||||
@computed("canLoginLocalWithEmail")
|
||||
hasAtLeastOneLoginButton(canLoginLocalWithEmail) {
|
||||
return findAll().length > 0 || canLoginLocalWithEmail;
|
||||
@ -78,12 +83,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
loginDisabled: Ember.computed.or("loggingIn", "loggedIn"),
|
||||
|
||||
@computed("loggingIn", "authenticate", "application.canSignUp")
|
||||
showSignupLink(loggingIn, authenticate, canSignUp) {
|
||||
return canSignUp && !loggingIn && Ember.isEmpty(authenticate);
|
||||
@computed("loggingIn", "application.canSignUp")
|
||||
showSignupLink(loggingIn, canSignUp) {
|
||||
return canSignUp && !loggingIn;
|
||||
},
|
||||
|
||||
showSpinner: Ember.computed.or("loggingIn", "authenticate"),
|
||||
showSpinner: Ember.computed.readOnly("loggingIn"),
|
||||
|
||||
@computed("canLoginLocalWithEmail", "processingEmailLink")
|
||||
showLoginWithEmailLink(canLoginLocalWithEmail, processingEmailLink) {
|
||||
@ -109,15 +114,20 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
login: this.loginName,
|
||||
password: this.loginPassword,
|
||||
second_factor_token: this.secondFactorToken,
|
||||
second_factor_method: this.secondFactorMethod
|
||||
second_factor_method: this.secondFactorMethod,
|
||||
security_key_credential: this.securityKeyCredential
|
||||
}
|
||||
}).then(
|
||||
result => {
|
||||
// Successful login
|
||||
if (result && result.error) {
|
||||
this.set("loggingIn", false);
|
||||
const invalidSecurityKey = result.reason === "invalid_security_key";
|
||||
const invalidSecondFactor =
|
||||
result.reason === "invalid_second_factor";
|
||||
|
||||
if (
|
||||
result.reason === "invalid_second_factor" &&
|
||||
(invalidSecondFactor || invalidSecurityKey) &&
|
||||
!this.secondFactorRequired
|
||||
) {
|
||||
document.getElementById("modal-alert").style.display = "none";
|
||||
@ -126,15 +136,24 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
secondFactorRequired: true,
|
||||
showLoginButtons: false,
|
||||
backupEnabled: result.backup_enabled,
|
||||
showSecondFactor: true
|
||||
showSecondFactor: invalidSecondFactor,
|
||||
showSecurityKey: invalidSecurityKey,
|
||||
secondFactorMethod: invalidSecurityKey
|
||||
? SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
: SECOND_FACTOR_METHODS.TOTP,
|
||||
securityKeyChallenge: result.challenge,
|
||||
securityKeyAllowedCredentialIds: result.allowed_credential_ids
|
||||
});
|
||||
|
||||
Ember.run.schedule("afterRender", () =>
|
||||
document
|
||||
.getElementById("second-factor")
|
||||
.querySelector("input")
|
||||
.focus()
|
||||
);
|
||||
// only need to focus the 2FA input for TOTP
|
||||
if (!this.showSecurityKey) {
|
||||
Ember.run.scheduleOnce("afterRender", () =>
|
||||
document
|
||||
.getElementById("second-factor")
|
||||
.querySelector("input")
|
||||
.focus()
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (result.reason === "not_activated") {
|
||||
@ -212,20 +231,13 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
return false;
|
||||
},
|
||||
|
||||
externalLogin(loginMethod, { fullScreenLogin = false } = {}) {
|
||||
const capabilities = this.capabilities;
|
||||
// On Mobile, Android or iOS always go with full screen
|
||||
if (
|
||||
this.isMobileDevice ||
|
||||
(capabilities &&
|
||||
(capabilities.isIOS ||
|
||||
capabilities.isAndroid ||
|
||||
capabilities.isSafari))
|
||||
) {
|
||||
fullScreenLogin = true;
|
||||
externalLogin(loginMethod) {
|
||||
if (this.loginDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
loginMethod.doLogin({ fullScreenLogin });
|
||||
this.set("loggingIn", true);
|
||||
loginMethod.doLogin().catch(() => this.set("loggingIn", false));
|
||||
},
|
||||
|
||||
createAccount() {
|
||||
@ -286,16 +298,20 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
})
|
||||
.catch(e => this.flash(extractError(e), "error"))
|
||||
.finally(() => this.set("processingEmailLink", false));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@computed("authenticate")
|
||||
authMessage(authenticate) {
|
||||
if (Ember.isEmpty(authenticate)) return "";
|
||||
|
||||
const method = findAll().findBy("name", authenticate);
|
||||
if (method) {
|
||||
return method.message;
|
||||
authenticateSecurityKey() {
|
||||
getWebauthnCredential(
|
||||
this.securityKeyChallenge,
|
||||
this.securityKeyAllowedCredentialIds,
|
||||
credentialData => {
|
||||
this.set("securityKeyCredential", credentialData);
|
||||
this.send("login");
|
||||
},
|
||||
errorMessage => {
|
||||
this.flash(errorMessage, "error");
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -306,7 +322,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
Ember.run.next(() => {
|
||||
if (callback) callback();
|
||||
this.flash(errorMsg, className || "success");
|
||||
this.set("authenticate", null);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -4,13 +4,21 @@ import { ajax } from "discourse/lib/ajax";
|
||||
import PasswordValidation from "discourse/mixins/password-validation";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
import { getWebauthnCredential } from "discourse/lib/webauthn";
|
||||
|
||||
export default Ember.Controller.extend(PasswordValidation, {
|
||||
isDeveloper: Ember.computed.alias("model.is_developer"),
|
||||
admin: Ember.computed.alias("model.admin"),
|
||||
secondFactorRequired: Ember.computed.alias("model.second_factor_required"),
|
||||
securityKeyRequired: Ember.computed.alias("model.security_key_required"),
|
||||
backupEnabled: Ember.computed.alias("model.backup_enabled"),
|
||||
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
|
||||
securityKeyOrSecondFactorRequired: Ember.computed.or(
|
||||
"model.second_factor_required",
|
||||
"model.security_key_required"
|
||||
),
|
||||
secondFactorMethod: Ember.computed.alias("model.security_key_required")
|
||||
? SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
: SECOND_FACTOR_METHODS.TOTP,
|
||||
passwordRequired: true,
|
||||
errorMessage: null,
|
||||
successMessage: null,
|
||||
@ -39,7 +47,8 @@ export default Ember.Controller.extend(PasswordValidation, {
|
||||
data: {
|
||||
password: this.accountPassword,
|
||||
second_factor_token: this.secondFactorToken,
|
||||
second_factor_method: this.secondFactorMethod
|
||||
second_factor_method: this.secondFactorMethod,
|
||||
security_key_credential: this.securityKeyCredential
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
@ -53,15 +62,17 @@ export default Ember.Controller.extend(PasswordValidation, {
|
||||
DiscourseURL.redirectTo(result.redirect_to || "/");
|
||||
}
|
||||
} else {
|
||||
if (result.errors && result.errors.user_second_factors) {
|
||||
if (result.errors && !result.errors.password) {
|
||||
this.setProperties({
|
||||
secondFactorRequired: true,
|
||||
secondFactorRequired: this.secondFactorRequired,
|
||||
securityKeyRequired: this.securityKeyRequired,
|
||||
password: null,
|
||||
errorMessage: result.message
|
||||
});
|
||||
} else if (this.secondFactorRequired) {
|
||||
} else if (this.secondFactorRequired || this.securityKeyRequired) {
|
||||
this.setProperties({
|
||||
secondFactorRequired: false,
|
||||
securityKeyRequired: false,
|
||||
errorMessage: null
|
||||
});
|
||||
} else if (
|
||||
@ -90,6 +101,24 @@ export default Ember.Controller.extend(PasswordValidation, {
|
||||
});
|
||||
},
|
||||
|
||||
authenticateSecurityKey() {
|
||||
getWebauthnCredential(
|
||||
this.model.challenge,
|
||||
this.model.allowed_credential_ids,
|
||||
credentialData => {
|
||||
this.set("securityKeyCredential", credentialData);
|
||||
this.send("submit");
|
||||
},
|
||||
errorMessage => {
|
||||
this.setProperties({
|
||||
securityKeyRequired: true,
|
||||
password: null,
|
||||
errorMessage: errorMessage
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
done() {
|
||||
this.set("redirected", true);
|
||||
DiscourseURL.redirectTo(this.redirectTo || "/");
|
||||
|
||||
@ -252,7 +252,7 @@ export default Ember.Controller.extend(
|
||||
},
|
||||
|
||||
connectAccount(method) {
|
||||
method.doLogin({ reconnect: true, fullScreenLogin: false });
|
||||
method.doLogin({ reconnect: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,11 @@ import {
|
||||
setLocalTheme
|
||||
} from "discourse/lib/theme-selector";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { safariHacksDisabled, isiPad } from "discourse/lib/utilities";
|
||||
import {
|
||||
safariHacksDisabled,
|
||||
isiPad,
|
||||
iOSWithVisualViewport
|
||||
} from "discourse/lib/utilities";
|
||||
|
||||
const USER_HOMES = {
|
||||
1: "latest",
|
||||
@ -51,7 +55,9 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
|
||||
@computed()
|
||||
isiPad() {
|
||||
return isiPad();
|
||||
// TODO: remove this preference checkbox when iOS adoption > 90%
|
||||
// (currently only applies to iOS 12 and below)
|
||||
return isiPad() && !iOSWithVisualViewport();
|
||||
},
|
||||
|
||||
@computed()
|
||||
|
||||
@ -68,12 +68,14 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
errorMessage: null,
|
||||
loaded: true,
|
||||
totps: response.totps,
|
||||
security_keys: response.security_keys,
|
||||
password: null,
|
||||
dirty: false
|
||||
});
|
||||
this.set(
|
||||
"model.second_factor_enabled",
|
||||
response.totps && response.totps.length > 0
|
||||
(response.totps && response.totps.length > 0) ||
|
||||
(response.security_keys && response.security_keys.length > 0)
|
||||
);
|
||||
})
|
||||
.catch(e => this.handleError(e))
|
||||
@ -147,6 +149,31 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
});
|
||||
},
|
||||
|
||||
createSecurityKey() {
|
||||
const controller = showModal("second-factor-add-security-key", {
|
||||
model: this.model,
|
||||
title: "user.second_factor.security_key.add"
|
||||
});
|
||||
controller.setProperties({
|
||||
onClose: () => this.loadSecondFactors(),
|
||||
markDirty: () => this.markDirty(),
|
||||
onError: e => this.handleError(e)
|
||||
});
|
||||
},
|
||||
|
||||
editSecurityKey(security_key) {
|
||||
const controller = showModal("second-factor-edit-security-key", {
|
||||
model: security_key,
|
||||
title: "user.second_factor.security_key.edit"
|
||||
});
|
||||
controller.setProperties({
|
||||
user: this.model,
|
||||
onClose: () => this.loadSecondFactors(),
|
||||
markDirty: () => this.markDirty(),
|
||||
onError: e => this.handleError(e)
|
||||
});
|
||||
},
|
||||
|
||||
editSecondFactor(second_factor) {
|
||||
const controller = showModal("second-factor-edit", {
|
||||
model: second_factor,
|
||||
|
||||
@ -0,0 +1,136 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import {
|
||||
bufferToBase64,
|
||||
stringToBuffer,
|
||||
isWebauthnSupported
|
||||
} from "discourse/lib/webauthn";
|
||||
|
||||
// model for this controller is user.js.es6
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: false,
|
||||
errorMessage: null,
|
||||
|
||||
onShow() {
|
||||
// clear properties every time because the controller is a singleton
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
loading: true,
|
||||
securityKeyName: I18n.t("user.second_factor.security_key.default_name"),
|
||||
webauthnUnsupported: !isWebauthnSupported()
|
||||
});
|
||||
|
||||
this.model
|
||||
.requestSecurityKeyChallenge()
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
errorMessage: isWebauthnSupported()
|
||||
? null
|
||||
: I18n.t("login.security_key_support_missing_error"),
|
||||
loading: false,
|
||||
challenge: response.challenge,
|
||||
relayingParty: {
|
||||
id: response.rp_id,
|
||||
name: response.rp_name
|
||||
},
|
||||
supported_algoriths: response.supported_algoriths,
|
||||
user_secure_id: response.user_secure_id,
|
||||
existing_active_credential_ids:
|
||||
response.existing_active_credential_ids
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
|
||||
actions: {
|
||||
registerSecurityKey() {
|
||||
const publicKeyCredentialCreationOptions = {
|
||||
challenge: Uint8Array.from(this.challenge, c => c.charCodeAt(0)),
|
||||
rp: {
|
||||
name: this.relayingParty.name,
|
||||
id: this.relayingParty.id
|
||||
},
|
||||
user: {
|
||||
id: Uint8Array.from(this.user_secure_id, c => c.charCodeAt(0)),
|
||||
displayName: this.model.username_lower,
|
||||
name: this.model.username_lower
|
||||
},
|
||||
pubKeyCredParams: this.supported_algoriths.map(alg => {
|
||||
return { type: "public-key", alg: alg };
|
||||
}),
|
||||
excludeCredentials: this.existing_active_credential_ids.map(
|
||||
credentialId => {
|
||||
return {
|
||||
type: "public-key",
|
||||
id: stringToBuffer(atob(credentialId))
|
||||
};
|
||||
}
|
||||
),
|
||||
timeout: 20000,
|
||||
attestation: "none",
|
||||
authenticatorSelection: {
|
||||
// see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why
|
||||
// default value of preferred is not necesarrily what we want, it limits webauthn to only devices that support
|
||||
// user verification, which usually requires entering a PIN
|
||||
userVerification: "discouraged"
|
||||
}
|
||||
};
|
||||
|
||||
navigator.credentials
|
||||
.create({
|
||||
publicKey: publicKeyCredentialCreationOptions
|
||||
})
|
||||
.then(
|
||||
credential => {
|
||||
let serverData = {
|
||||
id: credential.id,
|
||||
rawId: bufferToBase64(credential.rawId),
|
||||
type: credential.type,
|
||||
attestation: bufferToBase64(
|
||||
credential.response.attestationObject
|
||||
),
|
||||
clientData: bufferToBase64(credential.response.clientDataJSON),
|
||||
name: this.securityKeyName
|
||||
};
|
||||
|
||||
this.model
|
||||
.registerSecurityKey(serverData)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
this.set("errorMessage", null);
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch(error => this.onError(error))
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
err => {
|
||||
if (err.name === "InvalidStateError") {
|
||||
return this.set(
|
||||
"errorMessage",
|
||||
I18n.t("user.second_factor.security_key.already_added_error")
|
||||
);
|
||||
}
|
||||
if (err.name === "NotAllowedError") {
|
||||
return this.set(
|
||||
"errorMessage",
|
||||
I18n.t("user.second_factor.security_key.not_allowed_error")
|
||||
);
|
||||
}
|
||||
this.set("errorMessage", err.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -11,6 +11,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
secondFactorKey: null,
|
||||
secondFactorName: null,
|
||||
secondFactorToken: null,
|
||||
showSecondFactorKey: false,
|
||||
secondFactorImage: null,
|
||||
@ -47,10 +48,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
this.set("loading", true);
|
||||
|
||||
this.model
|
||||
.enableSecondFactorTotp(
|
||||
this.secondFactorToken,
|
||||
I18n.t("user.second_factor.totp.default_name")
|
||||
)
|
||||
.enableSecondFactorTotp(this.secondFactorToken, this.secondFactorName)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
actions: {
|
||||
disableSecurityKey() {
|
||||
this.user
|
||||
.updateSecurityKey(this.model.id, this.model.name, true)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
})
|
||||
.catch(error => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
this.send("closeModal");
|
||||
});
|
||||
},
|
||||
|
||||
editSecurityKey() {
|
||||
this.user
|
||||
.updateSecurityKey(this.model.id, this.model.name, false)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
})
|
||||
.catch(error => {
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
this.send("closeModal");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -471,6 +471,8 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
const quoteState = this.quoteState;
|
||||
const postStream = this.get("model.postStream");
|
||||
|
||||
this.appEvents.trigger("page:compose-reply", topic);
|
||||
|
||||
if (!postStream || !topic || !topic.get("details.can_create_post")) {
|
||||
return;
|
||||
}
|
||||
@ -705,6 +707,12 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
});
|
||||
},
|
||||
|
||||
jumpEnd() {
|
||||
DiscourseURL.routeTo(this.get("model.lastPostUrl"), {
|
||||
jumpEnd: true
|
||||
});
|
||||
},
|
||||
|
||||
jumpUnread() {
|
||||
this._jumpToPostId(this.get("model.last_read_post_id"));
|
||||
},
|
||||
@ -937,46 +945,6 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
}
|
||||
},
|
||||
|
||||
joinGroup() {
|
||||
const groupId = this.get("model.group.id");
|
||||
if (groupId) {
|
||||
if (this.get("model.group.allow_membership_requests")) {
|
||||
const groupName = this.get("model.group.name");
|
||||
return ajax(`/groups/${groupName}/request_membership`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
topic_id: this.get("model.id")
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
bootbox.alert(
|
||||
I18n.t("topic.group_request_sent", {
|
||||
group_name: this.get("model.group.full_name")
|
||||
}),
|
||||
() =>
|
||||
this.previousURL
|
||||
? DiscourseURL.routeTo(this.previousURL)
|
||||
: DiscourseURL.routeTo("/")
|
||||
);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
} else {
|
||||
const topic = this.model;
|
||||
return ajax(`/groups/${groupId}/members`, {
|
||||
type: "PUT",
|
||||
data: { user_id: this.get("currentUser.id") }
|
||||
})
|
||||
.then(() =>
|
||||
topic.reload().then(() => {
|
||||
topic.set("view_hidden", false);
|
||||
topic.postStream.refresh();
|
||||
})
|
||||
)
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
replyAsNewTopic(post, quotedText) {
|
||||
const composerController = this.composer;
|
||||
|
||||
@ -1174,7 +1142,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
}
|
||||
},
|
||||
|
||||
hasError: Ember.computed.or("model.notFoundHtml", "model.message"),
|
||||
hasError: Ember.computed.or("model.errorHtml", "model.errorMessage"),
|
||||
noErrorYet: Ember.computed.not("hasError"),
|
||||
|
||||
categories: Ember.computed.alias("site.categoriesList"),
|
||||
|
||||
@ -4,11 +4,7 @@ export default {
|
||||
initialize(container) {
|
||||
let lastAuthResult;
|
||||
|
||||
if (window.location.search.indexOf("authComplete=true") !== -1) {
|
||||
// Happens when a popup social login loses connection to the parent window
|
||||
lastAuthResult = localStorage.getItem("lastAuthResult");
|
||||
localStorage.removeItem("lastAuthResult");
|
||||
} else if (document.getElementById("data-authentication")) {
|
||||
if (document.getElementById("data-authentication")) {
|
||||
// Happens for full screen logins
|
||||
lastAuthResult = document.getElementById("data-authentication").dataset
|
||||
.authenticationData;
|
||||
|
||||
@ -10,7 +10,7 @@ export default {
|
||||
).selectable_avatars_enabled;
|
||||
|
||||
container
|
||||
.lookup("app-events:main")
|
||||
.lookup("service:app-events")
|
||||
.on("show-avatar-select", this, "_showAvatarSelect");
|
||||
},
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ export default {
|
||||
user.unread_notifications + user.unread_private_messages;
|
||||
|
||||
container
|
||||
.lookup("app-events:main")
|
||||
.lookup("service:app-events")
|
||||
.on("notifications:changed", this, "_updateBadge");
|
||||
},
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ export default {
|
||||
router.on("routeWillChange", viewTrackingRequired);
|
||||
router.on("routeDidChange", cleanDOM);
|
||||
|
||||
let appEvents = container.lookup("app-events:main");
|
||||
let appEvents = container.lookup("service:app-events");
|
||||
|
||||
startPageTracking(router, appEvents);
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ export default {
|
||||
initialize(container) {
|
||||
const user = container.lookup("current-user:main");
|
||||
const bus = container.lookup("message-bus:main");
|
||||
const appEvents = container.lookup("app-events:main");
|
||||
const appEvents = container.lookup("service:app-events");
|
||||
|
||||
if (user) {
|
||||
bus.subscribe("/reviewable_counts", data => {
|
||||
|
||||
@ -9,7 +9,7 @@ export default {
|
||||
this.container = container;
|
||||
|
||||
container
|
||||
.lookup("app-events:main")
|
||||
.lookup("service:app-events")
|
||||
.on("notifications:changed", this, "_updateTitle");
|
||||
},
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ function _clean() {
|
||||
}
|
||||
|
||||
// TODO: Avoid container lookup here
|
||||
const appEvents = Discourse.__container__.lookup("app-events:main");
|
||||
const appEvents = Discourse.__container__.lookup("service:app-events");
|
||||
appEvents.trigger("dom:clean");
|
||||
}
|
||||
|
||||
|
||||
@ -86,7 +86,7 @@ export default {
|
||||
this._stopCallback();
|
||||
|
||||
this.searchService = this.container.lookup("search-service:main");
|
||||
this.appEvents = this.container.lookup("app-events:main");
|
||||
this.appEvents = this.container.lookup("service:app-events");
|
||||
this.currentUser = this.container.lookup("current-user:main");
|
||||
let siteSettings = this.container.lookup("site-settings:main");
|
||||
|
||||
|
||||
@ -449,7 +449,7 @@ class PluginApi {
|
||||
```
|
||||
**/
|
||||
onAppEvent(name, fn) {
|
||||
const appEvents = this._lookupContainer("app-events:main");
|
||||
const appEvents = this._lookupContainer("service:app-events");
|
||||
appEvents && appEvents.on(name, fn);
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,8 @@ export function extraConnectorClass(name, obj) {
|
||||
const DefaultConnectorClass = {
|
||||
actions: {},
|
||||
shouldRender: () => true,
|
||||
setupComponent() {}
|
||||
setupComponent() {},
|
||||
teardownComponent() {}
|
||||
};
|
||||
|
||||
function findOutlets(collection, callback) {
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import debounce from "discourse/lib/debounce";
|
||||
import { safariHacksDisabled } from "discourse/lib/utilities";
|
||||
import {
|
||||
safariHacksDisabled,
|
||||
iOSWithVisualViewport
|
||||
} from "discourse/lib/utilities";
|
||||
|
||||
// TODO: remove calcHeight once iOS 13 adoption > 90%
|
||||
// In iOS 13 and up we use visualViewport API to calculate height
|
||||
@ -75,27 +78,21 @@ export function isWorkaroundActive() {
|
||||
function positioningWorkaround($fixedElement) {
|
||||
const caps = Discourse.__container__.lookup("capabilities:main");
|
||||
|
||||
if (!caps.isIOS || caps.isIpadOS || safariHacksDisabled()) {
|
||||
if (!caps.isIOS || safariHacksDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fixedElement = $fixedElement[0];
|
||||
const oldHeight = fixedElement.style.height;
|
||||
|
||||
var done = false;
|
||||
var originalScrollTop = 0;
|
||||
let lastTouchedElement = null;
|
||||
|
||||
positioningWorkaround.blur = function(evt) {
|
||||
if (workaroundActive) {
|
||||
done = true;
|
||||
$("body").removeClass("ios-safari-composer-hacks");
|
||||
|
||||
$("#main-outlet").show();
|
||||
$("header").show();
|
||||
|
||||
fixedElement.style.position = "";
|
||||
fixedElement.style.top = "";
|
||||
|
||||
if (window.visualViewport === undefined) {
|
||||
if (!iOSWithVisualViewport()) {
|
||||
fixedElement.style.height = oldHeight;
|
||||
Ember.run.later(
|
||||
() => $(fixedElement).removeClass("no-transition"),
|
||||
@ -113,14 +110,17 @@ function positioningWorkaround($fixedElement) {
|
||||
};
|
||||
|
||||
var blurredNow = function(evt) {
|
||||
// we cannot use evt.relatedTarget to get the last focused element in safari iOS
|
||||
// document.activeElement is also unreliable (iOS does not mark buttons as focused)
|
||||
// so instead, we store the last touched element and check against it
|
||||
|
||||
if (
|
||||
!done &&
|
||||
$(document.activeElement)
|
||||
.parents()
|
||||
.toArray()
|
||||
.indexOf(fixedElement) > -1
|
||||
lastTouchedElement &&
|
||||
($(lastTouchedElement).hasClass("select-kit-header") ||
|
||||
["span", "svg", "button"].includes(
|
||||
lastTouchedElement.nodeName.toLowerCase()
|
||||
))
|
||||
) {
|
||||
// something in focus so skip
|
||||
return;
|
||||
}
|
||||
|
||||
@ -130,60 +130,79 @@ function positioningWorkaround($fixedElement) {
|
||||
var blurred = debounce(blurredNow, 250);
|
||||
|
||||
var positioningHack = function(evt) {
|
||||
done = false;
|
||||
|
||||
// we need this, otherwise changing focus means we never clear
|
||||
this.addEventListener("blur", blurred);
|
||||
|
||||
if (fixedElement.style.top === "0px") {
|
||||
if (this !== document.activeElement) {
|
||||
evt.preventDefault();
|
||||
// resets focus out of select-kit elements
|
||||
// might become redundant after select-kit refactoring
|
||||
$fixedElement.find(".select-kit.is-expanded > button").trigger("click");
|
||||
$fixedElement
|
||||
.find(".select-kit > button.is-focused")
|
||||
.removeClass("is-focused");
|
||||
|
||||
// this tricks safari into assuming current input is at top of the viewport
|
||||
// via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input
|
||||
this.style.transform = "translateY(-200px)";
|
||||
this.focus();
|
||||
let _this = this;
|
||||
setTimeout(function() {
|
||||
_this.style.transform = "none";
|
||||
}, 50);
|
||||
if ($(window).scrollTop() > 0) {
|
||||
originalScrollTop = $(window).scrollTop();
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
if (iOSWithVisualViewport()) {
|
||||
// disable hacks when using a hardware keyboard
|
||||
// by default, a hardware keyboard will show the keyboard accessory bar
|
||||
// whose height is currently 55px (using 75 for a bit of a buffer)
|
||||
let heightDiff = window.innerHeight - window.visualViewport.height;
|
||||
if (heightDiff < 75) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// don't trigger keyboard on disabled element (happens when a category is required)
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
if (fixedElement.style.top === "0px") {
|
||||
if (this !== document.activeElement) {
|
||||
evt.preventDefault();
|
||||
|
||||
originalScrollTop = $(window).scrollTop();
|
||||
// this tricks safari into assuming current input is at top of the viewport
|
||||
// via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input
|
||||
this.style.transform = "translateY(-200px)";
|
||||
this.focus();
|
||||
let _this = this;
|
||||
setTimeout(function() {
|
||||
_this.style.transform = "none";
|
||||
}, 30);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// take care of body
|
||||
// don't trigger keyboard on disabled element (happens when a category is required)
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#main-outlet").hide();
|
||||
$("header").hide();
|
||||
|
||||
$(window).scrollTop(0);
|
||||
|
||||
let i = 20;
|
||||
let interval = setInterval(() => {
|
||||
$("body").addClass("ios-safari-composer-hacks");
|
||||
$(window).scrollTop(0);
|
||||
if (i-- === 0) {
|
||||
clearInterval(interval);
|
||||
|
||||
let i = 20;
|
||||
let interval = setInterval(() => {
|
||||
$(window).scrollTop(0);
|
||||
if (i-- === 0) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
if (!iOSWithVisualViewport()) {
|
||||
const height = calcHeight();
|
||||
fixedElement.style.height = height + "px";
|
||||
$(fixedElement).addClass("no-transition");
|
||||
}
|
||||
}, 10);
|
||||
|
||||
fixedElement.style.top = "0px";
|
||||
evt.preventDefault();
|
||||
this.focus();
|
||||
workaroundActive = true;
|
||||
}, 350);
|
||||
};
|
||||
|
||||
if (window.visualViewport === undefined) {
|
||||
const height = calcHeight();
|
||||
fixedElement.style.height = height + "px";
|
||||
$(fixedElement).addClass("no-transition");
|
||||
var lastTouched = function(evt) {
|
||||
if (evt && evt.target) {
|
||||
lastTouchedElement = evt.target;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
this.focus();
|
||||
workaroundActive = true;
|
||||
};
|
||||
|
||||
function attachTouchStart(elem, fn) {
|
||||
@ -194,30 +213,8 @@ function positioningWorkaround($fixedElement) {
|
||||
}
|
||||
|
||||
const checkForInputs = debounce(function() {
|
||||
$fixedElement
|
||||
.find(
|
||||
"button:not(.hide-preview),a:not(.mobile-file-upload):not(.toggle-toolbar)"
|
||||
)
|
||||
.each(function(idx, elem) {
|
||||
if ($(elem).parents(".emoji-picker").length > 0) {
|
||||
return;
|
||||
}
|
||||
attachTouchStart(fixedElement, lastTouched);
|
||||
|
||||
if ($(elem).parents(".autocomplete").length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($(elem).parents(".d-editor-button-bar").length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
attachTouchStart(this, function(evt) {
|
||||
done = true;
|
||||
$(document.activeElement).blur();
|
||||
evt.preventDefault();
|
||||
$(this).click();
|
||||
});
|
||||
});
|
||||
$fixedElement.find("input[type=text],textarea").each(function() {
|
||||
attachTouchStart(this, positioningHack);
|
||||
});
|
||||
|
||||
@ -93,6 +93,12 @@ const DiscourseURL = Ember.Object.extend({
|
||||
let elementId;
|
||||
let holder;
|
||||
|
||||
if (opts.jumpEnd) {
|
||||
$(window).scrollTop($(document).height() - $(window).height());
|
||||
_transitioning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (postNumber === 1 && !opts.anchor) {
|
||||
$(window).scrollTop(0);
|
||||
_transitioning = false;
|
||||
@ -347,7 +353,8 @@ const DiscourseURL = Ember.Object.extend({
|
||||
|
||||
this.appEvents.trigger("post:highlight", closest);
|
||||
const jumpOpts = {
|
||||
skipIfOnScreen: routeOpts.skipIfOnScreen
|
||||
skipIfOnScreen: routeOpts.skipIfOnScreen,
|
||||
jumpEnd: routeOpts.jumpEnd
|
||||
};
|
||||
|
||||
const m = /#.+$/.exec(path);
|
||||
@ -398,6 +405,9 @@ const DiscourseURL = Ember.Object.extend({
|
||||
);
|
||||
},
|
||||
|
||||
// TODO: These container calls can be replaced eventually if we migrate this to a service
|
||||
// object.
|
||||
|
||||
/**
|
||||
@private
|
||||
|
||||
@ -410,6 +420,10 @@ const DiscourseURL = Ember.Object.extend({
|
||||
return Discourse.__container__.lookup("router:main");
|
||||
},
|
||||
|
||||
get appEvents() {
|
||||
return Discourse.__container__.lookup("service:app-events");
|
||||
},
|
||||
|
||||
// Get a controller. Note that currently it uses `__container__` which is not
|
||||
// advised but there is no other way to access the router.
|
||||
controllerFor(name) {
|
||||
|
||||
@ -544,6 +544,10 @@ export function isAppleDevice() {
|
||||
|
||||
let iPadDetected = undefined;
|
||||
|
||||
export function iOSWithVisualViewport() {
|
||||
return isAppleDevice() && window.visualViewport !== undefined;
|
||||
}
|
||||
|
||||
export function isiPad() {
|
||||
if (iPadDetected === undefined) {
|
||||
iPadDetected =
|
||||
@ -554,6 +558,8 @@ export function isiPad() {
|
||||
}
|
||||
|
||||
export function safariHacksDisabled() {
|
||||
if (iOSWithVisualViewport()) return false;
|
||||
|
||||
let pref = localStorage.getItem("safari-hacks-disabled");
|
||||
let result = false;
|
||||
if (pref !== null) {
|
||||
|
||||
78
app/assets/javascripts/discourse/lib/webauthn.js.es6
Normal file
78
app/assets/javascripts/discourse/lib/webauthn.js.es6
Normal file
@ -0,0 +1,78 @@
|
||||
export function stringToBuffer(str) {
|
||||
let buffer = new ArrayBuffer(str.length);
|
||||
let byteView = new Uint8Array(buffer);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
byteView[i] = str.charCodeAt(i);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function bufferToBase64(buffer) {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||
}
|
||||
|
||||
export function isWebauthnSupported() {
|
||||
return typeof PublicKeyCredential !== "undefined";
|
||||
}
|
||||
|
||||
export function getWebauthnCredential(
|
||||
challenge,
|
||||
allowedCredentialIds,
|
||||
successCallback,
|
||||
errorCallback
|
||||
) {
|
||||
if (!isWebauthnSupported()) {
|
||||
return errorCallback(I18n.t("login.security_key_support_missing_error"));
|
||||
}
|
||||
|
||||
let challengeBuffer = stringToBuffer(challenge);
|
||||
let allowCredentials = allowedCredentialIds.map(credentialId => {
|
||||
return {
|
||||
id: stringToBuffer(atob(credentialId)),
|
||||
type: "public-key"
|
||||
};
|
||||
});
|
||||
|
||||
navigator.credentials
|
||||
.get({
|
||||
publicKey: {
|
||||
challenge: challengeBuffer,
|
||||
allowCredentials: allowCredentials,
|
||||
timeout: 60000,
|
||||
|
||||
// see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why
|
||||
// default value of preferred is not necesarrily what we want, it limits webauthn to only devices that support
|
||||
// user verification, which usually requires entering a PIN
|
||||
userVerification: "discouraged"
|
||||
}
|
||||
})
|
||||
.then(credential => {
|
||||
// 1. if there is a credential, check if the raw ID base64 matches
|
||||
// any of the allowed credential ids
|
||||
if (
|
||||
!allowedCredentialIds.some(
|
||||
credentialId => bufferToBase64(credential.rawId) === credentialId
|
||||
)
|
||||
) {
|
||||
return errorCallback(
|
||||
I18n.t("login.security_key_no_matching_credential_error")
|
||||
);
|
||||
}
|
||||
|
||||
const credentialData = {
|
||||
signature: bufferToBase64(credential.response.signature),
|
||||
clientData: bufferToBase64(credential.response.clientDataJSON),
|
||||
authenticatorData: bufferToBase64(
|
||||
credential.response.authenticatorData
|
||||
),
|
||||
credentialId: bufferToBase64(credential.rawId)
|
||||
};
|
||||
successCallback(credentialData);
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name === "NotAllowedError") {
|
||||
return errorCallback(I18n.t("login.security_key_not_allowed_error"));
|
||||
}
|
||||
errorCallback(err);
|
||||
});
|
||||
}
|
||||
@ -941,6 +941,11 @@ const Composer = RestModel.extend({
|
||||
|
||||
composer.clearState();
|
||||
composer.set("createdPost", createdPost);
|
||||
if (composer.replyingToTopic) {
|
||||
this.appEvents.trigger("post:created", createdPost);
|
||||
} else {
|
||||
this.appEvents.trigger("topic:created", createdPost, composer);
|
||||
}
|
||||
|
||||
if (addedToStream) {
|
||||
composer.set("composeState", CLOSED);
|
||||
|
||||
@ -17,60 +17,24 @@ const LoginMethod = Ember.Object.extend({
|
||||
return this.message_override || I18n.t(`login.${this.name}.message`);
|
||||
},
|
||||
|
||||
doLogin({ reconnect = false, fullScreenLogin = true } = {}) {
|
||||
const name = this.name;
|
||||
const customLogin = this.customLogin;
|
||||
|
||||
if (customLogin) {
|
||||
customLogin();
|
||||
} else {
|
||||
if (this.custom_url) {
|
||||
window.location = this.custom_url;
|
||||
return;
|
||||
}
|
||||
let authUrl = Discourse.getURL(`/auth/${name}`);
|
||||
|
||||
if (reconnect) {
|
||||
authUrl += "?reconnect=true";
|
||||
}
|
||||
|
||||
if (reconnect || fullScreenLogin || this.full_screen_login) {
|
||||
LoginMethod.buildPostForm(authUrl).then(form => {
|
||||
document.cookie = "fsl=true";
|
||||
form.submit();
|
||||
});
|
||||
} else {
|
||||
this.set("authenticate", name);
|
||||
const left = this.lastX - 400;
|
||||
const top = this.lastY - 200;
|
||||
|
||||
const height = this.frame_height || 400;
|
||||
const width = this.frame_width || 800;
|
||||
|
||||
if (name === "facebook") {
|
||||
authUrl += authUrl.includes("?") ? "&" : "?";
|
||||
authUrl += "display=popup";
|
||||
}
|
||||
LoginMethod.buildPostForm(authUrl).then(form => {
|
||||
const windowState = window.open(
|
||||
"about:blank",
|
||||
"auth_popup",
|
||||
`menubar=no,status=no,height=${height},width=${width},left=${left},top=${top}`
|
||||
);
|
||||
|
||||
form.target = "auth_popup";
|
||||
form.submit();
|
||||
|
||||
const timer = setInterval(() => {
|
||||
// If the process is aborted, reset state in this window
|
||||
if (!windowState || windowState.closed) {
|
||||
clearInterval(timer);
|
||||
this.set("authenticate", null);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
doLogin({ reconnect = false } = {}) {
|
||||
if (this.customLogin) {
|
||||
this.customLogin();
|
||||
return Ember.RSVP.resolve();
|
||||
}
|
||||
|
||||
if (this.custom_url) {
|
||||
window.location = this.custom_url;
|
||||
return Ember.RSVP.resolve();
|
||||
}
|
||||
|
||||
let authUrl = Discourse.getURL(`/auth/${this.name}`);
|
||||
|
||||
if (reconnect) {
|
||||
authUrl += "?reconnect=true";
|
||||
}
|
||||
|
||||
return LoginMethod.buildPostForm(authUrl).then(form => form.submit());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -1067,31 +1067,16 @@ export default RestModel.extend({
|
||||
// Handles an error loading a topic based on a HTTP status code. Updates
|
||||
// the text to the correct values.
|
||||
errorLoading(result) {
|
||||
const status = result.jqXHR.status;
|
||||
|
||||
const topic = this.topic;
|
||||
this.set("loadingFilter", false);
|
||||
topic.set("errorLoading", true);
|
||||
|
||||
// If the result was 404 the post is not found
|
||||
// If it was 410 the post is deleted and the user should not see it
|
||||
if (status === 404 || status === 410) {
|
||||
topic.set("notFoundHtml", result.jqXHR.responseText);
|
||||
return;
|
||||
const json = result.jqXHR.responseJSON;
|
||||
if (json && json.extras && json.extras.html) {
|
||||
topic.set("errorHtml", json.extras.html);
|
||||
} else {
|
||||
topic.set("errorMessage", I18n.t("topic.server_error.description"));
|
||||
topic.set("noRetry", result.jqXHR.status === 403);
|
||||
}
|
||||
|
||||
// If the result is 403 it means invalid access
|
||||
if (status === 403) {
|
||||
topic.set("noRetry", true);
|
||||
if (Discourse.User.current()) {
|
||||
topic.set("message", I18n.t("topic.invalid_access.description"));
|
||||
} else {
|
||||
topic.set("message", I18n.t("topic.invalid_access.login_required"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise supply a generic error message
|
||||
topic.set("message", I18n.t("topic.server_error.description"));
|
||||
}
|
||||
});
|
||||
|
||||
@ -316,7 +316,10 @@ const Post = RestModel.extend({
|
||||
// need to wait to hear back from server (stuff may not be loaded)
|
||||
|
||||
return Discourse.Post.updateBookmark(this.id, this.bookmarked)
|
||||
.then(result => this.set("topic.bookmarked", result.topic_bookmarked))
|
||||
.then(result => {
|
||||
this.set("topic.bookmarked", result.topic_bookmarked);
|
||||
this.appEvents.trigger("page:bookmark-post-toggled", this);
|
||||
})
|
||||
.catch(error => {
|
||||
this.toggleProperty("bookmarked");
|
||||
if (bookmarkedTopic) {
|
||||
|
||||
@ -17,16 +17,15 @@ import {
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export function loadTopicView(topic, args) {
|
||||
const topicId = topic.get("id");
|
||||
const data = _.merge({}, args);
|
||||
const url = `${Discourse.getURL("/t/")}${topicId}`;
|
||||
const url = `${Discourse.getURL("/t/")}${topic.id}`;
|
||||
const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json";
|
||||
|
||||
delete data.nearPost;
|
||||
delete data.__type;
|
||||
delete data.store;
|
||||
|
||||
return PreloadStore.getAndRemove(`topic_${topicId}`, () =>
|
||||
return PreloadStore.getAndRemove(`topic_${topic.id}`, () =>
|
||||
ajax(jsonUrl, { data })
|
||||
).then(json => {
|
||||
topic.updateFromJson(json);
|
||||
|
||||
@ -21,7 +21,11 @@ import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import Category from "discourse/models/category";
|
||||
|
||||
export const SECOND_FACTOR_METHODS = { TOTP: 1, BACKUP_CODE: 2 };
|
||||
export const SECOND_FACTOR_METHODS = {
|
||||
TOTP: 1,
|
||||
BACKUP_CODE: 2,
|
||||
SECURITY_KEY: 3
|
||||
};
|
||||
|
||||
const isForever = dt => moment().diff(dt, "years") < -500;
|
||||
|
||||
@ -375,6 +379,19 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
requestSecurityKeyChallenge() {
|
||||
return ajax("/u/create_second_factor_security_key.json", {
|
||||
type: "POST"
|
||||
});
|
||||
},
|
||||
|
||||
registerSecurityKey(credential) {
|
||||
return ajax("/u/register_second_factor_security_key.json", {
|
||||
data: credential,
|
||||
type: "POST"
|
||||
});
|
||||
},
|
||||
|
||||
createSecondFactorTotp() {
|
||||
return ajax("/u/create_second_factor_totp.json", {
|
||||
type: "POST"
|
||||
@ -409,6 +426,17 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
updateSecurityKey(id, name, disable) {
|
||||
return ajax("/u/security_key.json", {
|
||||
data: {
|
||||
name,
|
||||
disable,
|
||||
id
|
||||
},
|
||||
type: "PUT"
|
||||
});
|
||||
},
|
||||
|
||||
toggleSecondFactor(authToken, authMethod, targetMethod, enable) {
|
||||
return ajax("/u/second_factor.json", {
|
||||
data: {
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import Session from "discourse/models/session";
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import AppEvents from "discourse/lib/app-events";
|
||||
import Store from "discourse/models/store";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import DiscourseLocation from "discourse/lib/discourse-location";
|
||||
import SearchService from "discourse/services/search";
|
||||
import {
|
||||
@ -17,10 +15,7 @@ export default {
|
||||
name: "inject-discourse-objects",
|
||||
|
||||
initialize(container, app) {
|
||||
const appEvents = AppEvents.create();
|
||||
app.register("app-events:main", appEvents, { instantiate: false });
|
||||
ALL_TARGETS.forEach(t => app.inject(t, "appEvents", "app-events:main"));
|
||||
DiscourseURL.appEvents = appEvents;
|
||||
ALL_TARGETS.forEach(t => app.inject(t, "appEvents", "service:app-events"));
|
||||
|
||||
// backwards compatibility: remove when plugins have updated
|
||||
app.register("store:main", Store);
|
||||
|
||||
@ -265,9 +265,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||
const methods = findAll();
|
||||
|
||||
if (!this.siteSettings.enable_local_logins && methods.length === 1) {
|
||||
this.controllerFor("login").send("externalLogin", methods[0], {
|
||||
fullScreenLogin: true
|
||||
});
|
||||
this.controllerFor("login").send("externalLogin", methods[0]);
|
||||
} else {
|
||||
showModal(modal);
|
||||
this.controllerFor("modal").set("modalClass", modalClass);
|
||||
|
||||
@ -198,8 +198,24 @@ export default (filterArg, params) => {
|
||||
},
|
||||
|
||||
actions: {
|
||||
error(err) {
|
||||
const json = err.jqXHR.responseJSON;
|
||||
if (json && json.extras && json.extras.html) {
|
||||
this.controllerFor("discovery").set(
|
||||
"errorHtml",
|
||||
err.jqXHR.responseJSON.extras.html
|
||||
);
|
||||
} else {
|
||||
this.replaceWith("exception");
|
||||
}
|
||||
},
|
||||
|
||||
setNotification(notification_level) {
|
||||
this.currentModel.setNotification(notification_level);
|
||||
},
|
||||
|
||||
triggerRefresh() {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -14,6 +14,7 @@ export default RestrictedUserRoute.extend({
|
||||
setupController(controller, model) {
|
||||
controller.setProperties({ model, newUsername: model.get("username") });
|
||||
controller.set("loading", true);
|
||||
|
||||
model
|
||||
.loadSecondFactorCodes("")
|
||||
.then(response => {
|
||||
@ -24,7 +25,8 @@ export default RestrictedUserRoute.extend({
|
||||
errorMessage: null,
|
||||
loaded: !response.password_required,
|
||||
dirty: !!response.password_required,
|
||||
totps: response.totps
|
||||
totps: response.totps,
|
||||
security_keys: response.security_keys
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@ -52,6 +52,7 @@ export default Discourse.Route.extend({
|
||||
enteredAt: new Date().getTime().toString()
|
||||
});
|
||||
|
||||
this.appEvents.trigger("page:topic-loaded", topic);
|
||||
topicController.subscribe();
|
||||
|
||||
// Highlight our post after the next render
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
|
||||
export default Ember.Object.extend(Ember.Evented, {
|
||||
export default Ember.Service.extend(Ember.Evented, {
|
||||
_events: {},
|
||||
|
||||
on() {
|
||||
@ -28,9 +28,7 @@
|
||||
<section class='about admins'>
|
||||
<h3>{{d-icon "users"}} {{i18n 'about.our_admins'}}</h3>
|
||||
|
||||
{{#each model.admins as |a|}}
|
||||
{{user-info user=a}}
|
||||
{{/each}}
|
||||
{{about-page-users users=model.admins}}
|
||||
<div class='clearfix'></div>
|
||||
|
||||
</section>
|
||||
@ -45,9 +43,7 @@
|
||||
<h3>{{d-icon "users"}} {{i18n 'about.our_moderators'}}</h3>
|
||||
|
||||
<div class='users'>
|
||||
{{#each model.moderators as |m|}}
|
||||
{{user-info user=m}}
|
||||
{{/each}}
|
||||
{{about-page-users users=model.moderators}}
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
</section>
|
||||
@ -62,9 +58,7 @@
|
||||
<section class='about category-moderators moderators-{{cm.category.slug}}'>
|
||||
<h3>{{category-link cm.category}}{{i18n "about.moderators"}}</h3>
|
||||
<div class='users'>
|
||||
{{#each cm.moderators as |m|}}
|
||||
{{user-info user=m}}
|
||||
{{/each}}
|
||||
{{about-page-users users=cm.moderators}}
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
</section>
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
{{#each usersTemplates as |userTemplate|}}
|
||||
<div data-username="{{userTemplate.username}}" class="user-info small">
|
||||
<div class="user-image">
|
||||
<div class="user-image-inner">
|
||||
<a href="{{userTemplate.userPath}}" data-user-card="{{userTemplate.username}}">
|
||||
{{{userTemplate.avatar}}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-detail">
|
||||
<div class="name-line">
|
||||
<span class="username">
|
||||
<a href="{{userTemplate.userPath}}" data-user-card="{{userTemplate.username}}">
|
||||
{{userTemplate.username}}
|
||||
</a>
|
||||
</span>
|
||||
<span class="name">{{userTemplate.name}}</span>
|
||||
</div>
|
||||
<div class="title">{{userTemplate.title}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
@ -1,3 +1,3 @@
|
||||
{{#link-to route args}}
|
||||
{{#link-to route args class="cancel"}}
|
||||
{{i18n 'cancel'}}
|
||||
{{/link-to}}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
{{#unless hidden}}
|
||||
{{yield}}
|
||||
|
||||
<div class="controls">
|
||||
{{d-button class="btn-primary" label="composer.modal_ok" action=(action "ok")}}
|
||||
{{d-button class="btn-danger" label="composer.modal_cancel" action=(action "cancel")}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
@ -1,13 +1,3 @@
|
||||
<div class='d-editor-overlay hidden'></div>
|
||||
|
||||
<div class='d-editor-modals'>
|
||||
{{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction=(action "insertLink")}}
|
||||
<h3>{{i18n "composer.link_dialog_title"}}</h3>
|
||||
{{text-field value=linkUrl placeholderKey="composer.link_url_placeholder" class="link-url"}}
|
||||
{{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}}
|
||||
{{/d-editor-modal}}
|
||||
</div>
|
||||
|
||||
<div class='d-editor-container'>
|
||||
<div class="d-editor-textarea-wrapper {{if disabled "disabled"}}">
|
||||
<div class='d-editor-button-bar'>
|
||||
|
||||
@ -8,14 +8,9 @@
|
||||
<div class="location-box">
|
||||
<a class="close pull-right" {{action "hide"}}>{{d-icon "times"}}</a>
|
||||
{{#if copied}}
|
||||
<a class="btn btn-default btn-hover pull-right">
|
||||
{{d-icon "copy"}}
|
||||
{{i18n "ip_lookup.copied"}}
|
||||
</a>
|
||||
{{d-button class="btn-hover pull-right" icon="copy" label="ip_lookup.copied"}}
|
||||
{{else}}
|
||||
<a class="btn btn-default pull-right no-text" {{action "copy"}}>
|
||||
{{d-icon "copy"}}
|
||||
</a>
|
||||
{{d-button action=(action "copy") class="pull-right no-text" icon="copy"}}
|
||||
{{/if}}
|
||||
<h4>{{i18n "ip_lookup.title"}}</h4>
|
||||
<p class='powered-by'>{{{i18n "ip_lookup.powered_by"}}}</p>
|
||||
|
||||
@ -5,12 +5,9 @@
|
||||
{{/if}}
|
||||
<p>{{secondFactorDescription}}</p>
|
||||
{{yield}}
|
||||
{{#if backupEnabled}}
|
||||
{{#if showToggleMethodLink}}
|
||||
<p>
|
||||
{{discourse-linked-text
|
||||
class="toggle-second-factor-method"
|
||||
action=(action "toggleSecondFactorMethod")
|
||||
text=linkText}}
|
||||
<a href="" class="toggle-second-factor-method" {{action "toggleSecondFactorMethod"}}>{{ i18n linkText }}</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -6,4 +6,5 @@
|
||||
id=inputId
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
autofocus="autofocus"}}
|
||||
autofocus="autofocus"
|
||||
placeholder=placeholder}}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
<div id="security-key">
|
||||
{{d-button
|
||||
action=action
|
||||
icon="key"
|
||||
id="security-key-authenticate-button"
|
||||
label="login.security_key_authenticate"
|
||||
type="button"
|
||||
class='btn btn-large btn-primary'}}
|
||||
<p>
|
||||
{{#if otherMethodAllowed}}
|
||||
<a href="" class="toggle-second-factor-method" {{action "useAnotherMethod"}}>{{ i18n 'login.security_key_alternative' }}</a>
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
@ -1,32 +1,36 @@
|
||||
<div class="container">
|
||||
{{discourse-banner user=currentUser banner=site.banner}}
|
||||
</div>
|
||||
|
||||
<div class="list-controls">
|
||||
{{#if errorHtml}}
|
||||
{{{errorHtml}}}
|
||||
{{else}}
|
||||
<div class="container">
|
||||
{{outlet "navigation-bar"}}
|
||||
{{discourse-banner user=currentUser banner=site.banner}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
<div class="list-controls">
|
||||
<div class="container">
|
||||
{{outlet "navigation-bar"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container list-container {{if loading "hidden"}}">
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
<div id="header-list-area">
|
||||
{{outlet "header-list-container"}}
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
|
||||
<div class="container list-container {{if loading "hidden"}}">
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
<div id="header-list-area">
|
||||
{{outlet "header-list-container"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
<div id="list-area">
|
||||
{{plugin-outlet name="discovery-list-container-top"
|
||||
args=(hash category=category listLoading=loading)}}
|
||||
{{outlet "list-container"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
<div id="list-area">
|
||||
{{plugin-outlet name="discovery-list-container-top"
|
||||
args=(hash category=category listLoading=loading)}}
|
||||
{{outlet "list-container"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="discovery-below"}}
|
||||
{{plugin-outlet name="discovery-below"}}
|
||||
{{/if}}
|
||||
|
||||
@ -12,20 +12,34 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if model.can_login}}
|
||||
{{#if model.second_factor_required}}
|
||||
{{#second-factor-form
|
||||
secondFactorMethod=secondFactorMethod
|
||||
secondFactorToken=secondFactorToken
|
||||
backupEnabled=model.backup_codes_enabled
|
||||
isLogin=true}}
|
||||
{{second-factor-input value=secondFactorToken secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
|
||||
{{/second-factor-form}}
|
||||
{{#if secondFactorRequired }}
|
||||
{{#if model.security_key_required }}
|
||||
{{#security-key-form
|
||||
allowedCredentialIds=model.allowed_credential_ids
|
||||
challenge=model.security_key_challenge
|
||||
showSecurityKey=model.security_key_required
|
||||
showSecondFactor=false
|
||||
secondFactorMethod=secondFactorMethod
|
||||
otherMethodAllowed=secondFactorRequired
|
||||
action=(action "authenticateSecurityKey")}}
|
||||
{{/security-key-form}}
|
||||
{{else}}
|
||||
{{#second-factor-form
|
||||
secondFactorMethod=secondFactorMethod
|
||||
secondFactorToken=secondFactorToken
|
||||
backupEnabled=model.backup_codes_enabled
|
||||
isLogin=true}}
|
||||
{{second-factor-input value=secondFactorToken secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
|
||||
{{/second-factor-form}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<h2>{{i18n "email_login.confirm_title" site_name=siteSettings.title}}</h2>
|
||||
<p>{{i18n "email_login.logging_in_as" email=model.token_email}}</p>
|
||||
{{/if}}
|
||||
|
||||
{{d-button label="email_login.confirm_button" action=(action "finishLogin") class="btn-primary"}}
|
||||
{{#unless model.security_key_required }}
|
||||
{{d-button label="email_login.confirm_button" action=(action "finishLogin") class="btn-primary"}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -40,8 +40,21 @@
|
||||
secondFactorMethod=secondFactorMethod
|
||||
secondFactorToken=secondFactorToken
|
||||
class=secondFactorClass
|
||||
backupEnabled=backupEnabled
|
||||
isLogin=true}}
|
||||
{{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
|
||||
{{#if showSecurityKey}}
|
||||
{{#security-key-form
|
||||
allowedCredentialIds=securityKeyAllowedCredentialIds
|
||||
challenge=securityKeyChallenge
|
||||
showSecurityKey=showSecurityKey
|
||||
showSecondFactor=showSecondFactor
|
||||
secondFactorMethod=secondFactorMethod
|
||||
otherMethodAllowed=secondFactorRequired
|
||||
action=(action "authenticateSecurityKey")}}
|
||||
{{/security-key-form}}
|
||||
{{else}}
|
||||
{{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
|
||||
{{/if}}
|
||||
{{/second-factor-form}}
|
||||
</form>
|
||||
{{/if}}
|
||||
@ -49,24 +62,22 @@
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#if authenticate}}
|
||||
{{i18n "login.authenticating"}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canLoginLocal}}
|
||||
{{d-button action=(action "login")
|
||||
icon="unlock"
|
||||
label=loginButtonLabel
|
||||
disabled=loginDisabled
|
||||
class="btn btn-large btn-primary"}}
|
||||
{{#unless showSecurityKey }}
|
||||
{{d-button action=(action "login")
|
||||
icon="unlock"
|
||||
label=loginButtonLabel
|
||||
disabled=loginDisabled
|
||||
class="btn-large btn-primary"}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if showSignupLink}}
|
||||
{{#d-button class="btn btn-large" id="new-account-link" action=(route-action "showCreateAccount")}}
|
||||
{{#d-button class="btn-large" id="new-account-link" action=(route-action "showCreateAccount")}}
|
||||
{{i18n "create_account.title"}}
|
||||
{{/d-button}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="auth-message">{{authMessage}}</div>
|
||||
|
||||
<div id='login-alert' class={{alertClass}}>{{alert}}</div>
|
||||
{{/login-modal}}
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
{{#unless helpSeen}}
|
||||
{{d-button class="btn-large"
|
||||
label="forgot_password.button_help"
|
||||
icon="question-circle"
|
||||
action=(action "help")}}
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
{{#d-modal-body title="composer.link_dialog_title" class="insert-link"}}
|
||||
<div class="inputs">
|
||||
{{text-field value=linkUrl placeholderKey="composer.link_url_placeholder" class="link-url"}}
|
||||
</div>
|
||||
<div class="inputs">
|
||||
{{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}}
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button class="btn-primary" label="composer.modal_ok" action=(action "ok")}}
|
||||
{{d-button class="btn-danger" label="composer.modal_cancel" action=(action "cancel")}}
|
||||
</div>
|
||||
@ -8,11 +8,11 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for='login-account-name'>{{i18n 'login.username'}}</label></td>
|
||||
<td>{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus" disabled=showSecondFactor}}</td>
|
||||
<td>{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus" disabled=disableLoginFields}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for='login-account-password'>{{i18n 'login.password'}}</label></td>
|
||||
<td>{{password-field value=loginPassword type="password" id="login-account-password" maxlength="200" capsLockOn=capsLockOn disabled=showSecondFactor}}</td>
|
||||
<td>{{password-field value=loginPassword type="password" id="login-account-password" maxlength="200" capsLockOn=capsLockOn disabled=disableLoginFields}}</td>
|
||||
<td><a id="forgot-password-link" {{action "forgotPassword"}}>{{i18n 'forgot_password.action'}}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -28,7 +28,19 @@
|
||||
class=secondFactorClass
|
||||
backupEnabled=backupEnabled
|
||||
isLogin=true}}
|
||||
{{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
|
||||
{{#if showSecurityKey}}
|
||||
{{#security-key-form
|
||||
allowedCredentialIds=securityKeyAllowedCredentialIds
|
||||
challenge=securityKeyChallenge
|
||||
showSecurityKey=showSecurityKey
|
||||
showSecondFactor=showSecondFactor
|
||||
secondFactorMethod=secondFactorMethod
|
||||
otherMethodAllowed=secondFactorRequired
|
||||
action=(action "authenticateSecurityKey")}}
|
||||
{{/security-key-form}}
|
||||
{{else}}
|
||||
{{second-factor-input value=secondFactorToken inputId='login-second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
|
||||
{{/if}}
|
||||
{{/second-factor-form}}
|
||||
</form>
|
||||
{{/if}}
|
||||
@ -43,13 +55,16 @@
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#if canLoginLocal}}
|
||||
{{d-button
|
||||
action=(action "login")
|
||||
form="login-form"
|
||||
icon="unlock"
|
||||
label=loginButtonLabel
|
||||
disabled=loginDisabled
|
||||
class='btn btn-large btn-primary'}}
|
||||
{{#unless showSecurityKey }}
|
||||
{{d-button
|
||||
action=(action "login")
|
||||
id="login-button"
|
||||
form="login-form"
|
||||
icon="unlock"
|
||||
label=loginButtonLabel
|
||||
disabled=loginDisabled
|
||||
class='btn btn-large btn-primary'}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if showSignupLink}}
|
||||
<button class="btn btn-large" id="new-account-link" {{action "createAccount"}}>
|
||||
@ -58,13 +73,8 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if authenticate}}
|
||||
{{i18n 'login.authenticating'}}
|
||||
{{/if}}
|
||||
|
||||
{{conditional-loading-spinner condition=showSpinner size="small"}}
|
||||
</div>
|
||||
|
||||
<div class="auth-message">{{authMessage}}</div>
|
||||
<div id='login-alert' class={{alertClass}}>{{alert}}</div>
|
||||
{{/login-modal}}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
{{#d-modal-body}}
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
{{#if errorMessage}}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<div class='alert alert-error'>{{errorMessage}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{{i18n 'user.second_factor.enable_security_key_description'}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{input value=securityKeyName id='test' placeholder='security key name'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{#unless webauthnUnsupported}}
|
||||
{{d-button action=(action "registerSecurityKey")
|
||||
class="btn btn-primary add-totp"
|
||||
label="user.second_factor.security_key.register"}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
{{/conditional-loading-spinner}}
|
||||
{{/d-modal-body}}
|
||||
@ -33,10 +33,14 @@
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label input-prepend">{{i18n 'user.second_factor.label'}}</label>
|
||||
|
||||
<label class="control-label input-prepend">{{i18n 'user.second_factor.name'}}</label>
|
||||
<div class="controls">
|
||||
{{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token'}}
|
||||
{{second-factor-input value=secondFactorName inputId='second-factor-name' placeholder=(i18n 'user.second_factor.totp.default_name')}}
|
||||
</div>
|
||||
|
||||
<label class="control-label input-prepend">{{i18n 'user.second_factor.label'}}</label>
|
||||
<div class="controls">
|
||||
{{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token' placeholder='123456'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
{{#d-modal-body}}
|
||||
<div class="form-horizontal">
|
||||
{{input type="text" value=model.name}}
|
||||
</div>
|
||||
<div class='second-factor instructions'>
|
||||
{{i18n 'user.second_factor.security_key.edit_description'}}
|
||||
</div>
|
||||
{{d-button action=(action "editSecurityKey")
|
||||
class="btn-primary"
|
||||
label="user.second_factor.security_key.edit"}}
|
||||
|
||||
{{d-button action=(action "disableSecurityKey")
|
||||
class="btn-danger"
|
||||
label="user.second_factor.security_key.delete"}}
|
||||
{{/d-modal-body}}
|
||||
@ -16,20 +16,33 @@
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<form>
|
||||
{{#if secondFactorRequired}}
|
||||
{{#if securityKeyOrSecondFactorRequired }}
|
||||
{{#if errorMessage}}
|
||||
<div class='alert alert-error'>{{errorMessage}}</div>
|
||||
<br/>
|
||||
{{/if}}
|
||||
|
||||
{{#second-factor-form
|
||||
secondFactorMethod=secondFactorMethod
|
||||
secondFactorToken=secondFactorToken
|
||||
backupEnabled=backupEnabled
|
||||
isLogin=false}}
|
||||
{{second-factor-input value=secondFactorToken inputId='second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
|
||||
{{/second-factor-form}}
|
||||
{{d-button action=(action "submit") class='btn-primary' label='submit'}}
|
||||
{{#if securityKeyRequired }}
|
||||
{{#security-key-form
|
||||
allowedCredentialIds=model.allowed_credential_ids
|
||||
challenge=model.security_key_challenge
|
||||
showSecurityKey=model.security_key_required
|
||||
showSecondFactor=false
|
||||
secondFactorMethod=secondFactorMethod
|
||||
otherMethodAllowed=secondFactorRequired
|
||||
action=(action "authenticateSecurityKey")}}
|
||||
{{/security-key-form}}
|
||||
{{else}}
|
||||
{{#second-factor-form
|
||||
secondFactorMethod=secondFactorMethod
|
||||
secondFactorToken=secondFactorToken
|
||||
backupEnabled=backupEnabled
|
||||
isLogin=false}}
|
||||
{{second-factor-input value=secondFactorToken inputId='second-factor' secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
|
||||
{{/second-factor-form}}
|
||||
{{/if}}
|
||||
{{#unless securityKeyRequired }}
|
||||
{{d-button action=(action "submit") class='btn-primary' label='submit'}}
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
<h2>{{i18n 'user.change_password.choose'}}</h2>
|
||||
|
||||
|
||||
@ -54,6 +54,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<h2>{{i18n "user.second_factor.security_key.title"}}</h2>
|
||||
{{d-button action=(action "createSecurityKey")
|
||||
class="btn-primary new-security-key"
|
||||
disabled=loading
|
||||
label="user.second_factor.security_key.add"}}
|
||||
{{#each security_keys as |security_key|}}
|
||||
<div class="second-factor-item">
|
||||
{{#if security_key.name}}
|
||||
{{security_key.name}}
|
||||
{{else}}
|
||||
{{i18n "user.second_factor.security_key.default_name"}}
|
||||
{{/if}}
|
||||
|
||||
{{#if isCurrentUser}}
|
||||
{{d-button action=(action "editSecurityKey" security_key)
|
||||
class="btn-default btn-small btn-icon pad-left no-text edit"
|
||||
disabled=loading
|
||||
icon="pencil-alt"
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls pref-second-factor-backup">
|
||||
<h2>{{i18n "user.second_factor_backup.title"}}</h2>
|
||||
@ -99,7 +126,7 @@
|
||||
{{text-field value=password
|
||||
id="password"
|
||||
type="password"
|
||||
classNames="input-xxlarge"
|
||||
classNames="input-large"
|
||||
autofocus="autofocus"}}
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
@ -115,16 +142,14 @@
|
||||
disabled=loading
|
||||
label="continue"}}
|
||||
|
||||
{{d-button action=(action "resetPassword")
|
||||
class="btn"
|
||||
disabled=resetPasswordLoading
|
||||
icon="envelope"
|
||||
label='user.change_password.action'}}
|
||||
|
||||
{{resetPasswordProgress}}
|
||||
|
||||
{{#unless showEnforcedNotice}}
|
||||
{{cancel-link route="preferences.account" args= model.username}}
|
||||
{{cancel-link route="preferences.account" args=model.username}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
<div class="controls" style="margin-top: 5px">
|
||||
{{resetPasswordProgress}}
|
||||
{{#unless resetPasswordLoading}}
|
||||
<a href class="instructions" {{action "resetPassword"}}>{{ i18n 'user.second_factor.forgot_password' }}</a>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,105 +1,154 @@
|
||||
{{#discourse-topic multiSelect=multiSelect enteredAt=enteredAt topic=model hasScrolled=hasScrolled}}
|
||||
{{#if model.view_hidden}}
|
||||
{{topic-join-group-notice model=model action=(action "joinGroup")}}
|
||||
{{else}}
|
||||
{{#if model}}
|
||||
{{add-category-tag-classes category=model.category tags=model.tags}}
|
||||
<div class="container">
|
||||
{{discourse-banner user=currentUser banner=site.banner overlay=hasScrolled hide=model.errorLoading}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if model}}
|
||||
{{add-category-tag-classes category=model.category tags=model.tags}}
|
||||
<div class="container">
|
||||
{{discourse-banner user=currentUser banner=site.banner overlay=hasScrolled hide=model.errorLoading}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showSharedDraftControls}}
|
||||
{{shared-draft-controls topic=model}}
|
||||
{{/if}}
|
||||
{{#if showSharedDraftControls}}
|
||||
{{shared-draft-controls topic=model}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}}
|
||||
{{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}}
|
||||
|
||||
{{#if model.postStream.loaded}}
|
||||
{{#if model.postStream.firstPostPresent}}
|
||||
{{#topic-title cancelled=(action "cancelEditingTopic") save=(action "finishedEditingTopic") model=model}}
|
||||
{{#if editingTopic}}
|
||||
<div class="edit-topic-title">
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
{{#if model.postStream.loaded}}
|
||||
{{#if model.postStream.firstPostPresent}}
|
||||
{{#topic-title cancelled=(action "cancelEditingTopic") save=(action "finishedEditingTopic") model=model}}
|
||||
{{#if editingTopic}}
|
||||
<div class="edit-topic-title">
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
|
||||
|
||||
{{#if showCategoryChooser}}
|
||||
{{category-chooser
|
||||
class="small"
|
||||
value=(unbound buffered.category_id)
|
||||
onSelectAny=(action "topicCategoryChanged")}}
|
||||
{{/if}}
|
||||
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
|
||||
|
||||
{{#if showCategoryChooser}}
|
||||
{{category-chooser
|
||||
class="small"
|
||||
value=(unbound buffered.category_id)
|
||||
onSelectAny=(action "topicCategoryChanged")}}
|
||||
{{/if}}
|
||||
{{#if canEditTags}}
|
||||
{{mini-tag-chooser
|
||||
filterable=true
|
||||
tags=(unbound buffered.tags)
|
||||
categoryId=(unbound buffered.category_id)
|
||||
onChangeTags=(action "topicTagsChanged")}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canEditTags}}
|
||||
{{mini-tag-chooser
|
||||
filterable=true
|
||||
tags=(unbound buffered.tags)
|
||||
categoryId=(unbound buffered.category_id)
|
||||
onChangeTags=(action "topicTagsChanged")}}
|
||||
{{/if}}
|
||||
{{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
|
||||
|
||||
{{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
|
||||
<div class="edit-controls">
|
||||
{{d-button action=(action "finishedEditingTopic") class="btn-primary submit-edit" icon="check"}}
|
||||
{{d-button action=(action "cancelEditingTopic") class="btn-default cancel-edit" icon="times"}}
|
||||
|
||||
<div class="edit-controls">
|
||||
{{d-button action=(action "finishedEditingTopic") class="btn-primary submit-edit" icon="check"}}
|
||||
{{d-button action=(action "cancelEditingTopic") class="btn-default cancel-edit" icon="times"}}
|
||||
|
||||
{{#if canRemoveTopicFeaturedLink}}
|
||||
<a href {{action "removeFeaturedLink"}} class="remove-featured-link" title="{{i18n "composer.remove_featured_link"}}">
|
||||
{{d-icon "times-circle"}}
|
||||
{{featuredLinkDomain}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
<h1 data-topic-id="{{unbound model.id}}">
|
||||
{{#unless model.is_warning}}
|
||||
{{#if siteSettings.enable_personal_messages}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<a href={{pmPath}} title="{{i18n 'topic_statuses.personal_message.title'}}" aria-label={{i18n 'user.messages.inbox'}}>
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
</a>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if model.details.loaded}}
|
||||
{{topic-status topic=model}}
|
||||
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
|
||||
{{{model.fancyTitle}}}
|
||||
{{#if canRemoveTopicFeaturedLink}}
|
||||
<a href {{action "removeFeaturedLink"}} class="remove-featured-link" title="{{i18n "composer.remove_featured_link"}}">
|
||||
{{d-icon "times-circle"}}
|
||||
{{featuredLinkDomain}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if model.details.can_edit}}
|
||||
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{d-icon "pencil-alt"}}</a>
|
||||
{{else}}
|
||||
<h1 data-topic-id="{{unbound model.id}}">
|
||||
{{#unless model.is_warning}}
|
||||
{{#if siteSettings.enable_personal_messages}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<a href={{pmPath}} title="{{i18n 'topic_statuses.personal_message.title'}}" aria-label={{i18n 'user.messages.inbox'}}>
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
</a>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</h1>
|
||||
{{/unless}}
|
||||
|
||||
{{topic-category topic=model class="topic-category"}}
|
||||
{{#if model.details.loaded}}
|
||||
{{topic-status topic=model}}
|
||||
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
|
||||
{{{model.fancyTitle}}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.details.can_edit}}
|
||||
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{d-icon "pencil-alt"}}</a>
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
{{topic-category topic=model class="topic-category"}}
|
||||
{{/if}}
|
||||
{{/topic-title}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
<div class="container posts">
|
||||
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
|
||||
{{partial "selected-posts"}}
|
||||
</div>
|
||||
|
||||
{{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}}
|
||||
{{#if info.renderTimeline}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
fixed="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{/if}}
|
||||
{{/topic-title}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
<div class="container posts">
|
||||
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
|
||||
{{partial "selected-posts"}}
|
||||
</div>
|
||||
|
||||
{{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}}
|
||||
{{#if info.renderTimeline}}
|
||||
{{topic-timeline
|
||||
topic=model
|
||||
notificationLevel=model.details.notification_level
|
||||
prevEvent=info.prevEvent
|
||||
fullscreen=info.topicProgressExpanded
|
||||
enteredIndex=enteredIndex
|
||||
loading=model.postStream.loading
|
||||
jumpToPost=(action "jumpToPost")
|
||||
jumpTop=(action "jumpTop")
|
||||
jumpBottom=(action "jumpBottom")
|
||||
jumpEnd=(action "jumpEnd")
|
||||
jumpToPostPrompt=(action "jumpToPostPrompt")
|
||||
jumpToIndex=(action "jumpToIndex")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{else}}
|
||||
{{#topic-progress
|
||||
prevEvent=info.prevEvent
|
||||
topic=model
|
||||
expanded=info.topicProgressExpanded
|
||||
jumpToPost=(action "jumpToPost")}}
|
||||
{{plugin-outlet name="before-topic-progress" args=(hash model=model jumpToPost=(action "jumpToPost"))}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
fixed="true"
|
||||
openUpwards="true"
|
||||
rightSide="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
@ -113,264 +162,212 @@
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{/if}}
|
||||
{{/topic-progress}}
|
||||
{{/if}}
|
||||
{{/topic-navigation}}
|
||||
|
||||
{{topic-timeline
|
||||
topic=model
|
||||
notificationLevel=model.details.notification_level
|
||||
prevEvent=info.prevEvent
|
||||
fullscreen=info.topicProgressExpanded
|
||||
enteredIndex=enteredIndex
|
||||
loading=model.postStream.loading
|
||||
jumpToPost=(action "jumpToPost")
|
||||
jumpTop=(action "jumpTop")
|
||||
jumpBottom=(action "jumpBottom")
|
||||
jumpToPostPrompt=(action "jumpToPostPrompt")
|
||||
jumpToIndex=(action "jumpToIndex")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{else}}
|
||||
{{#topic-progress
|
||||
prevEvent=info.prevEvent
|
||||
topic=model
|
||||
expanded=info.topicProgressExpanded
|
||||
jumpToPost=(action "jumpToPost")}}
|
||||
{{plugin-outlet name="before-topic-progress" args=(hash model=model jumpToPost=(action "jumpToPost"))}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
openUpwards="true"
|
||||
rightSide="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{/if}}
|
||||
{{/topic-progress}}
|
||||
{{/if}}
|
||||
{{/topic-navigation}}
|
||||
<div class="row">
|
||||
<section class="topic-area" id="topic" data-topic-id="{{unbound model.id}}">
|
||||
|
||||
<div class="row">
|
||||
<section class="topic-area" id="topic" data-topic-id="{{unbound model.id}}">
|
||||
<div class="posts-wrapper">
|
||||
{{conditional-loading-spinner condition=model.postStream.loadingAbove}}
|
||||
|
||||
<div class="posts-wrapper">
|
||||
{{conditional-loading-spinner condition=model.postStream.loadingAbove}}
|
||||
{{plugin-outlet name="topic-above-posts" args=(hash model=model)}}
|
||||
|
||||
{{plugin-outlet name="topic-above-posts" args=(hash model=model)}}
|
||||
{{#unless model.postStream.loadingFilter}}
|
||||
{{scrolling-post-stream
|
||||
posts=postsToRender
|
||||
canCreatePost=model.details.can_create_post
|
||||
multiSelect=multiSelect
|
||||
selectedPostsCount=selectedPostsCount
|
||||
selectedQuery=selectedQuery
|
||||
gaps=model.postStream.gaps
|
||||
showReadIndicator=model.show_read_indicator
|
||||
showFlags=(action "showPostFlags")
|
||||
editPost=(action "editPost")
|
||||
showHistory=(route-action "showHistory")
|
||||
showLogin=(route-action "showLogin")
|
||||
showRawEmail=(route-action "showRawEmail")
|
||||
deletePost=(action "deletePost")
|
||||
recoverPost=(action "recoverPost")
|
||||
expandHidden=(action "expandHidden")
|
||||
newTopicAction=(action "replyAsNewTopic")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
togglePostType=(action "togglePostType")
|
||||
rebakePost=(action "rebakePost")
|
||||
changePostOwner=(action "changePostOwner")
|
||||
grantBadge=(action "grantBadge")
|
||||
addNotice=(action "addNotice")
|
||||
removeNotice=(action "removeNotice")
|
||||
lockPost=(action "lockPost")
|
||||
unlockPost=(action "unlockPost")
|
||||
unhidePost=(action "unhidePost")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleWiki=(action "toggleWiki")
|
||||
toggleSummary=(action "toggleSummary")
|
||||
removeAllowedUser=(action "removeAllowedUser")
|
||||
removeAllowedGroup=(action "removeAllowedGroup")
|
||||
topVisibleChanged=(action "topVisibleChanged")
|
||||
currentPostChanged=(action "currentPostChanged")
|
||||
currentPostScrolled=(action "currentPostScrolled")
|
||||
bottomVisibleChanged=(action "bottomVisibleChanged")
|
||||
togglePostSelection=(action "togglePostSelection")
|
||||
selectReplies=(action "selectReplies")
|
||||
selectBelow=(action "selectBelow")
|
||||
fillGapBefore=(action "fillGapBefore")
|
||||
fillGapAfter=(action "fillGapAfter")
|
||||
showInvite=(route-action "showInvite")}}
|
||||
{{/unless}}
|
||||
|
||||
{{#unless model.postStream.loadingFilter}}
|
||||
{{scrolling-post-stream
|
||||
posts=postsToRender
|
||||
canCreatePost=model.details.can_create_post
|
||||
multiSelect=multiSelect
|
||||
selectedPostsCount=selectedPostsCount
|
||||
selectedQuery=selectedQuery
|
||||
gaps=model.postStream.gaps
|
||||
showReadIndicator=model.show_read_indicator
|
||||
showFlags=(action "showPostFlags")
|
||||
editPost=(action "editPost")
|
||||
showHistory=(route-action "showHistory")
|
||||
showLogin=(route-action "showLogin")
|
||||
showRawEmail=(route-action "showRawEmail")
|
||||
deletePost=(action "deletePost")
|
||||
recoverPost=(action "recoverPost")
|
||||
expandHidden=(action "expandHidden")
|
||||
newTopicAction=(action "replyAsNewTopic")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
togglePostType=(action "togglePostType")
|
||||
rebakePost=(action "rebakePost")
|
||||
changePostOwner=(action "changePostOwner")
|
||||
grantBadge=(action "grantBadge")
|
||||
addNotice=(action "addNotice")
|
||||
removeNotice=(action "removeNotice")
|
||||
lockPost=(action "lockPost")
|
||||
unlockPost=(action "unlockPost")
|
||||
unhidePost=(action "unhidePost")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleWiki=(action "toggleWiki")
|
||||
toggleSummary=(action "toggleSummary")
|
||||
removeAllowedUser=(action "removeAllowedUser")
|
||||
removeAllowedGroup=(action "removeAllowedGroup")
|
||||
topVisibleChanged=(action "topVisibleChanged")
|
||||
currentPostChanged=(action "currentPostChanged")
|
||||
currentPostScrolled=(action "currentPostScrolled")
|
||||
bottomVisibleChanged=(action "bottomVisibleChanged")
|
||||
togglePostSelection=(action "togglePostSelection")
|
||||
selectReplies=(action "selectReplies")
|
||||
selectBelow=(action "selectBelow")
|
||||
fillGapBefore=(action "fillGapBefore")
|
||||
fillGapAfter=(action "fillGapAfter")
|
||||
showInvite=(route-action "showInvite")}}
|
||||
{{/unless}}
|
||||
{{conditional-loading-spinner condition=model.postStream.loadingBelow}}
|
||||
</div>
|
||||
<div id="topic-bottom"></div>
|
||||
|
||||
{{conditional-loading-spinner condition=model.postStream.loadingBelow}}
|
||||
</div>
|
||||
<div id="topic-bottom"></div>
|
||||
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
|
||||
{{#if loadedAllPosts}}
|
||||
|
||||
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
|
||||
{{#if loadedAllPosts}}
|
||||
|
||||
{{#if model.pending_posts}}
|
||||
<div class='pending-posts'>
|
||||
{{#each model.pending_posts as |pending|}}
|
||||
<div class='reviewable-item'>
|
||||
<div class='reviewable-meta-data'>
|
||||
<span class='reviewable-type'>
|
||||
{{i18n "review.awaiting_approval"}}
|
||||
</span>
|
||||
<span class='created-at'>
|
||||
{{age-with-tooltip pending.created_at}}
|
||||
</span>
|
||||
</div>
|
||||
<div class='post-contents-wrapper'>
|
||||
{{reviewable-created-by user=currentUser tagName=''}}
|
||||
<div class='post-contents'>
|
||||
{{reviewable-created-by-name user=currentUser tagName=''}}
|
||||
<div class='post-body'>{{cook-text pending.raw}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='reviewable-actions'>
|
||||
{{d-button
|
||||
class="btn-danger"
|
||||
label="review.delete"
|
||||
icon="trash-alt"
|
||||
action=(action "deletePending" pending) }}
|
||||
{{#if model.pending_posts}}
|
||||
<div class='pending-posts'>
|
||||
{{#each model.pending_posts as |pending|}}
|
||||
<div class='reviewable-item'>
|
||||
<div class='reviewable-meta-data'>
|
||||
<span class='reviewable-type'>
|
||||
{{i18n "review.awaiting_approval"}}
|
||||
</span>
|
||||
<span class='created-at'>
|
||||
{{age-with-tooltip pending.created_at}}
|
||||
</span>
|
||||
</div>
|
||||
<div class='post-contents-wrapper'>
|
||||
{{reviewable-created-by user=currentUser tagName=''}}
|
||||
<div class='post-contents'>
|
||||
{{reviewable-created-by-name user=currentUser tagName=''}}
|
||||
<div class='post-body'>{{cook-text pending.raw}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.queued_posts_count}}
|
||||
<div class="has-pending-posts">
|
||||
<div>
|
||||
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
|
||||
<div class='reviewable-actions'>
|
||||
{{d-button
|
||||
class="btn-danger"
|
||||
label="review.delete"
|
||||
icon="trash-alt"
|
||||
action=(action "deletePending" pending) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
|
||||
{{i18n "review.view_pending"}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.private_topic_timer.execute_at}}
|
||||
{{topic-timer-info
|
||||
topicClosed=model.closed
|
||||
statusType=model.private_topic_timer.status_type
|
||||
executeAt=model.private_topic_timer.execute_at
|
||||
duration=model.private_topic_timer.duration
|
||||
removeTopicTimer=(action "removeTopicTimer" model.private_topic_timer.status_type "private_topic_timer")}}
|
||||
{{/if}}
|
||||
|
||||
{{topic-timer-info
|
||||
topicClosed=model.closed
|
||||
statusType=model.topic_timer.status_type
|
||||
executeAt=model.topic_timer.execute_at
|
||||
basedOnLastPost=model.topic_timer.based_on_last_post
|
||||
duration=model.topic_timer.duration
|
||||
categoryId=model.topic_timer.category_id
|
||||
removeTopicTimer=(action "removeTopicTimer" model.topic_timer.status_type "topic_timer")}}
|
||||
|
||||
{{#if session.showSignupCta}}
|
||||
{{! replace "Log In to Reply" with the infobox }}
|
||||
{{signup-cta}}
|
||||
{{else}}
|
||||
{{#if currentUser}}
|
||||
{{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}}
|
||||
|
||||
{{topic-footer-buttons
|
||||
topic=model
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
showFlagTopic=(route-action "showFlagTopic")
|
||||
toggleArchiveMessage=(action "toggleArchiveMessage")
|
||||
editFirstPost=(action "editFirstPost")
|
||||
deferTopic=(action "deferTopic")
|
||||
replyToPost=(action "replyToPost")}}
|
||||
{{else}}
|
||||
<div id="topic-footer-buttons">
|
||||
{{d-button icon="reply" class="btn-primary pull-right" action=(route-action "showLogin") label="topic.reply.title"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showSelectedPostsAtBottom}}
|
||||
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
|
||||
{{partial "selected-posts"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="topic-above-suggested" args=(hash model=model)}}
|
||||
<div class="{{if model.relatedMessages.length 'related-messages-wrapper'}} {{if model.suggestedTopics.length 'suggested-topics-wrapper'}}">
|
||||
{{#if model.relatedMessages.length}}
|
||||
{{related-messages topic=model}}
|
||||
{{/if}}
|
||||
{{#if model.suggestedTopics.length}}
|
||||
{{suggested-topics topic=model}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
</section>
|
||||
</div>
|
||||
{{#if model.queued_posts_count}}
|
||||
<div class="has-pending-posts">
|
||||
<div>
|
||||
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="container">
|
||||
{{#conditional-loading-spinner condition=noErrorYet}}
|
||||
{{#if model.notFoundHtml}}
|
||||
<div class="not-found">{{{model.notFoundHtml}}}</div>
|
||||
{{else}}
|
||||
<div class="topic-error">
|
||||
<div>{{model.message}}</div>
|
||||
{{#if model.noRetry}}
|
||||
{{#unless currentUser}}
|
||||
{{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}}
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
{{d-button action=(action "retryLoading") class="btn-primary topic-retry" icon="sync" label="errors.buttons.again"}}
|
||||
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
|
||||
{{i18n "review.view_pending"}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{conditional-loading-spinner condition=retrying}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
{{#if model.private_topic_timer.execute_at}}
|
||||
{{topic-timer-info
|
||||
topicClosed=model.closed
|
||||
statusType=model.private_topic_timer.status_type
|
||||
executeAt=model.private_topic_timer.execute_at
|
||||
duration=model.private_topic_timer.duration
|
||||
removeTopicTimer=(action "removeTopicTimer" model.private_topic_timer.status_type "private_topic_timer")}}
|
||||
{{/if}}
|
||||
|
||||
{{topic-timer-info
|
||||
topicClosed=model.closed
|
||||
statusType=model.topic_timer.status_type
|
||||
executeAt=model.topic_timer.execute_at
|
||||
basedOnLastPost=model.topic_timer.based_on_last_post
|
||||
duration=model.topic_timer.duration
|
||||
categoryId=model.topic_timer.category_id
|
||||
removeTopicTimer=(action "removeTopicTimer" model.topic_timer.status_type "topic_timer")}}
|
||||
|
||||
{{#if session.showSignupCta}}
|
||||
{{! replace "Log In to Reply" with the infobox }}
|
||||
{{signup-cta}}
|
||||
{{else}}
|
||||
{{#if currentUser}}
|
||||
{{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}}
|
||||
|
||||
{{topic-footer-buttons
|
||||
topic=model
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
showFlagTopic=(route-action "showFlagTopic")
|
||||
toggleArchiveMessage=(action "toggleArchiveMessage")
|
||||
editFirstPost=(action "editFirstPost")
|
||||
deferTopic=(action "deferTopic")
|
||||
replyToPost=(action "replyToPost")}}
|
||||
{{else}}
|
||||
<div id="topic-footer-buttons">
|
||||
{{d-button icon="reply" class="btn-primary pull-right" action=(route-action "showLogin") label="topic.reply.title"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showSelectedPostsAtBottom}}
|
||||
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
|
||||
{{partial "selected-posts"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="topic-above-suggested" args=(hash model=model)}}
|
||||
<div class="{{if model.relatedMessages.length 'related-messages-wrapper'}} {{if model.suggestedTopics.length 'suggested-topics-wrapper'}}">
|
||||
{{#if model.relatedMessages.length}}
|
||||
{{related-messages topic=model}}
|
||||
{{/if}}
|
||||
{{#if model.suggestedTopics.length}}
|
||||
{{suggested-topics topic=model}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
</section>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="container">
|
||||
{{#conditional-loading-spinner condition=noErrorYet}}
|
||||
{{#if model.errorHtml}}
|
||||
<div class="not-found">{{{model.errorHtml}}}</div>
|
||||
{{else}}
|
||||
<div class="topic-error">
|
||||
<div>{{model.errorMessage}}</div>
|
||||
{{#if model.noRetry}}
|
||||
{{#unless currentUser}}
|
||||
{{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}}
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
{{d-button action=(action "retryLoading") class="btn-primary topic-retry" icon="sync" label="errors.buttons.again"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{conditional-loading-spinner condition=retrying}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if embedQuoteButton}}
|
||||
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
|
||||
{{/if}}
|
||||
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
|
||||
|
||||
{{#if embedQuoteButton}}
|
||||
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
|
||||
{{/if}}
|
||||
{{/discourse-topic}}
|
||||
|
||||
@ -396,8 +396,7 @@ export default createWidget("post-menu", {
|
||||
},
|
||||
|
||||
menuItems() {
|
||||
let result = this.siteSettings.post_menu.split("|");
|
||||
return result;
|
||||
return this.siteSettings.post_menu.split("|").filter(Boolean);
|
||||
},
|
||||
|
||||
html(attrs, state) {
|
||||
@ -526,6 +525,19 @@ export default createWidget("post-menu", {
|
||||
)
|
||||
];
|
||||
|
||||
if (state.readers.length) {
|
||||
const remaining = state.totalReaders - state.readers.length;
|
||||
contents.push(
|
||||
this.attach("small-user-list", {
|
||||
users: state.readers,
|
||||
addSelf: false,
|
||||
listClassName: "who-read",
|
||||
description: "post.actions.people.read",
|
||||
count: remaining
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (state.likedUsers.length) {
|
||||
const remaining = state.total - state.likedUsers.length;
|
||||
contents.push(
|
||||
@ -542,19 +554,6 @@ export default createWidget("post-menu", {
|
||||
);
|
||||
}
|
||||
|
||||
if (state.readers.length) {
|
||||
const remaining = state.totalReaders - state.readers.length;
|
||||
contents.push(
|
||||
this.attach("small-user-list", {
|
||||
users: state.readers,
|
||||
addSelf: false,
|
||||
listClassName: "who-read",
|
||||
description: "post.actions.people.read",
|
||||
count: remaining
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return contents;
|
||||
},
|
||||
|
||||
|
||||
@ -692,9 +692,10 @@ export default createWidget("post", {
|
||||
const likeAction = post.get("likeAction");
|
||||
|
||||
if (likeAction && likeAction.get("canToggle")) {
|
||||
return likeAction
|
||||
.togglePromise(post)
|
||||
.then(result => this._warnIfClose(result));
|
||||
return likeAction.togglePromise(post).then(result => {
|
||||
this.appEvents.trigger("page:like-toggled", post, likeAction);
|
||||
return this._warnIfClose(result);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -45,6 +45,11 @@ createWidgetFrom(QuickAccessPanel, "quick-access-profile", {
|
||||
href: `${this.attrs.path}/messages`,
|
||||
content: I18n.t("user.private_messages")
|
||||
},
|
||||
{
|
||||
icon: "pencil",
|
||||
href: `${this.attrs.path}/activity/drafts`,
|
||||
content: I18n.t("user_action_groups.15")
|
||||
},
|
||||
{
|
||||
icon: "cog",
|
||||
href: `${this.attrs.path}/preferences`,
|
||||
|
||||
@ -148,7 +148,8 @@ createWidget("timeline-scrollarea", {
|
||||
const postStream = topic.get("postStream");
|
||||
const total = postStream.get("filteredPostsCount");
|
||||
|
||||
const current = clamp(Math.floor(total * percentage) + 1, 1, total);
|
||||
const scrollPosition = clamp(Math.floor(total * percentage), 0, total) + 1;
|
||||
const current = clamp(scrollPosition, 1, total);
|
||||
|
||||
const daysAgo = postStream.closestDaysAgoFor(current);
|
||||
let date;
|
||||
@ -168,6 +169,7 @@ createWidget("timeline-scrollarea", {
|
||||
|
||||
const result = {
|
||||
current,
|
||||
scrollPosition,
|
||||
total,
|
||||
date,
|
||||
lastRead: null,
|
||||
@ -183,9 +185,13 @@ createWidget("timeline-scrollarea", {
|
||||
result.lastReadPercentage = this._percentFor(topic, idx);
|
||||
}
|
||||
|
||||
if (this.state.position !== result.current) {
|
||||
this.state.position = result.current;
|
||||
this.sendWidgetAction("updatePosition", result.current);
|
||||
if (this.state.position !== result.scrollPosition) {
|
||||
this.state.position = result.scrollPosition;
|
||||
this.sendWidgetAction(
|
||||
"updatePosition",
|
||||
result.position,
|
||||
result.scrollPosition
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -259,7 +265,11 @@ createWidget("timeline-scrollarea", {
|
||||
const position = this.position();
|
||||
this.state.scrolledPost = position.current;
|
||||
|
||||
this.sendWidgetAction("jumpToIndex", position.current);
|
||||
if (position.current === position.scrollPosition) {
|
||||
this.sendWidgetAction("jumpToIndex", position.current);
|
||||
} else {
|
||||
this.sendWidgetAction("jumpEnd");
|
||||
}
|
||||
},
|
||||
|
||||
topicCurrentPostScrolled(event) {
|
||||
@ -380,25 +390,25 @@ export default createWidget("topic-timeline", {
|
||||
return { position: null, excerpt: null };
|
||||
},
|
||||
|
||||
updatePosition(pos) {
|
||||
updatePosition(postIdx, scrollPosition) {
|
||||
if (!this.attrs.fullScreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.position = pos;
|
||||
this.state.position = scrollPosition;
|
||||
this.state.excerpt = "";
|
||||
const stream = this.attrs.topic.get("postStream");
|
||||
|
||||
// a little debounce to avoid flashing
|
||||
Ember.run.later(() => {
|
||||
if (!this.state.position === pos) {
|
||||
if (!this.state.position === scrollPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we have an off by one, stream is zero based,
|
||||
// pos is 1 based
|
||||
stream.excerpt(pos - 1).then(info => {
|
||||
if (info && this.state.position === pos) {
|
||||
// postIdx is 1 based
|
||||
stream.excerpt(postIdx - 1).then(info => {
|
||||
if (info && this.state.position === scrollPosition) {
|
||||
let excerpt = "";
|
||||
|
||||
if (info.username) {
|
||||
|
||||
@ -111,7 +111,7 @@ export default class Widget {
|
||||
this.currentUser = register.lookup("current-user:main");
|
||||
this.capabilities = register.lookup("capabilities:main");
|
||||
this.store = register.lookup("service:store");
|
||||
this.appEvents = register.lookup("app-events:main");
|
||||
this.appEvents = register.lookup("service:app-events");
|
||||
this.keyValueStore = register.lookup("key-value-store:main");
|
||||
|
||||
// Helps debug widgets
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
(function() {
|
||||
const { authResult, baseUrl } = document.getElementById(
|
||||
"data-auth-result"
|
||||
).dataset;
|
||||
const parsedAuthResult = JSON.parse(authResult);
|
||||
|
||||
if (
|
||||
!window.opener ||
|
||||
!window.opener.Discourse ||
|
||||
!window.opener.Discourse.authenticationComplete
|
||||
) {
|
||||
localStorage.setItem("lastAuthResult", authResult);
|
||||
window.location.href = `${baseUrl}?authComplete=true`;
|
||||
} else {
|
||||
window.opener.Discourse.authenticationComplete(parsedAuthResult);
|
||||
window.close();
|
||||
}
|
||||
})();
|
||||
@ -7,7 +7,7 @@ workbox.setConfig({
|
||||
debug: false
|
||||
});
|
||||
|
||||
const cacheVersion = "1";
|
||||
var cacheVersion = "1";
|
||||
|
||||
// Cache all GET requests, so Discourse can be used while offline
|
||||
workbox.routing.registerRoute(
|
||||
@ -24,7 +24,7 @@ workbox.routing.registerRoute(
|
||||
})
|
||||
);
|
||||
|
||||
const idleThresholdTime = 1000 * 10; // 10 seconds
|
||||
var idleThresholdTime = 1000 * 10; // 10 seconds
|
||||
var lastAction = -1;
|
||||
|
||||
function isIdle() {
|
||||
|
||||
@ -100,10 +100,10 @@ $mobile-breakpoint: 700px;
|
||||
padding: 8px;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: darken($secondary, 2.5%);
|
||||
background-color: $primary-very-low;
|
||||
}
|
||||
tr.selected {
|
||||
background-color: lighten($primary, 80%);
|
||||
background-color: $primary-low;
|
||||
}
|
||||
.filters input {
|
||||
margin-bottom: 0;
|
||||
@ -341,8 +341,8 @@ $mobile-breakpoint: 700px;
|
||||
}
|
||||
|
||||
.admin-users .users-list {
|
||||
.username .fa {
|
||||
color: dark-light-choose($primary-medium, $secondary-medium);
|
||||
.username .d-icon {
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
|
||||
@ -566,12 +566,12 @@ $mobile-breakpoint: 700px;
|
||||
float: left;
|
||||
padding: 5px 10px;
|
||||
margin-right: 15px;
|
||||
border: 1px solid lighten($primary, 40%);
|
||||
border: 1px solid $primary-medium;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: $primary;
|
||||
&:hover {
|
||||
background-color: lighten($primary, 60%);
|
||||
background-color: $primary-low-mid;
|
||||
}
|
||||
@media (max-width: $mobile-breakpoint) {
|
||||
display: inline-block;
|
||||
@ -659,7 +659,7 @@ $mobile-breakpoint: 700px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: lighten($primary, 40);
|
||||
color: $primary-medium;
|
||||
}
|
||||
|
||||
.admin-nav {
|
||||
|
||||
@ -167,12 +167,12 @@ table.api-keys {
|
||||
|
||||
> p {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: darken($secondary, 10%) 1px solid;
|
||||
border-bottom: $primary-low 1px solid;
|
||||
}
|
||||
.filters {
|
||||
margin: 5px 0;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: darken($secondary, 5%) 1px solid;
|
||||
border-bottom: $primary-low 1px solid;
|
||||
.filter {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@ -162,30 +162,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mobile specific styles - mostly commmon style overrides
|
||||
// TODO move to mobile sheet once mobile view has a seprate template.
|
||||
.mobile-view {
|
||||
.admin-badges {
|
||||
.badges {
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
.content-list {
|
||||
flex: 0 0 100%;
|
||||
.admin-badge-list {
|
||||
max-height: 40vh;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.badge-intro {
|
||||
flex: 0 1 75%;
|
||||
}
|
||||
.current-badge {
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,9 +99,9 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
.color-schemes li {
|
||||
.fa {
|
||||
margin-right: 6px;
|
||||
color: dark-light-choose($primary-medium, $secondary-medium);
|
||||
.d-icon {
|
||||
margin-right: 0.25em;
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
.show-current-style {
|
||||
@ -492,8 +492,14 @@
|
||||
.hex {
|
||||
text-align: center;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.description {
|
||||
color: dark-light-choose($primary-medium, $secondary-medium);
|
||||
margin: 0.15em 0 0;
|
||||
color: $primary-high;
|
||||
font-size: $font-down-1;
|
||||
line-height: $line-height-medium;
|
||||
}
|
||||
|
||||
.invalid .hex input {
|
||||
@ -594,7 +600,7 @@
|
||||
|
||||
.permalink-search {
|
||||
text-align: left;
|
||||
@media screen and (min-width: map-get($breakpoints, tablet)) {
|
||||
@include breakpoint(tablet, min-width) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@ -604,7 +610,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
@media screen and (min-width: map-get($breakpoints, tablet)) {
|
||||
@include breakpoint(tablet, min-width) {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
@ -614,7 +620,7 @@
|
||||
}
|
||||
input {
|
||||
margin: 5px 0;
|
||||
@media screen and (min-width: map-get($breakpoints, tablet)) {
|
||||
@include breakpoint(tablet, min-width) {
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
@ -625,7 +631,7 @@
|
||||
}
|
||||
|
||||
.permalink-description {
|
||||
color: dark-light-choose($primary-medium, $secondary-medium);
|
||||
color: $primary-medium;
|
||||
}
|
||||
|
||||
// embedding
|
||||
@ -705,7 +711,7 @@
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
p.description {
|
||||
color: dark-light-choose($primary-medium, $secondary-medium);
|
||||
color: $primary-medium;
|
||||
margin-bottom: 1em;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
.reply-key {
|
||||
display: block;
|
||||
font-size: $font-down-1;
|
||||
color: dark-light-choose($primary-medium, $secondary-high);
|
||||
color: $primary-medium;
|
||||
}
|
||||
.username div {
|
||||
max-width: 180px;
|
||||
@ -39,7 +39,7 @@
|
||||
margin: 5px 10px;
|
||||
}
|
||||
.error-description {
|
||||
color: #919191;
|
||||
color: $primary-medium;
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
hr {
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
.admin-list-item {
|
||||
width: 100%;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
border-top: 1px solid $primary-low;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
position: relative;
|
||||
line-height: $line-height-small;
|
||||
cursor: default;
|
||||
border: 1px dashed #aaa;
|
||||
border: 1px dashed $primary-low-mid;
|
||||
border-radius: 3px;
|
||||
background-clip: padding-box;
|
||||
-moz-user-select: none;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Styles for /admin/logs
|
||||
|
||||
.web-hook-events {
|
||||
border-bottom: dotted 1px dark-light-choose($primary-low-mid, $secondary);
|
||||
border-bottom: dotted 1px $primary-low-mid;
|
||||
.heading-container {
|
||||
width: 100%;
|
||||
background-color: $primary-low;
|
||||
@ -399,10 +399,10 @@ table.screened-ip-addresses {
|
||||
cursor: pointer;
|
||||
.d-icon {
|
||||
margin-right: 0.25em;
|
||||
color: dark-light-diff($primary, $secondary, 50%, -50%);
|
||||
color: $primary-medium;
|
||||
}
|
||||
&:hover .d-icon {
|
||||
color: $primary;
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -94,8 +94,8 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.users-list {
|
||||
.username .fa {
|
||||
color: dark-light-choose($primary-medium, $secondary-medium);
|
||||
.username .d-icon {
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user