Version bump

This commit is contained in:
Neil Lalonde 2019-10-10 11:46:54 -04:00
commit 2672410743
1962 changed files with 8720 additions and 5035 deletions

View File

@ -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"
}

View File

@ -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

View File

@ -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

View 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;
}
};
}

View File

@ -0,0 +1 @@
require("admin-login/admin-login").default();

View File

@ -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")

View File

@ -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) {

View File

@ -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>&nbsp;</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"

View File

@ -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}}

View File

@ -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">

View File

@ -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>

View File

@ -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

View File

@ -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);
}
}
}
});

View File

@ -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/`;

View File

@ -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);
},

View File

@ -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)
};
});
})
});

View File

@ -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);
}
},

View File

@ -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);
}
}
});

View File

@ -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);
}
}
}
}
});

View File

@ -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;

View File

@ -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();
}
},

View File

@ -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 || {};

View File

@ -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;

View File

@ -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);
}
}
});

View File

@ -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) {

View File

@ -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(),

View File

@ -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();
}

View File

@ -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);
}
);
}
}
});

View File

@ -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");
}
}
});

View File

@ -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);
});
};

View File

@ -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 || "/");

View File

@ -252,7 +252,7 @@ export default Ember.Controller.extend(
},
connectAccount(method) {
method.doLogin({ reconnect: true, fullScreenLogin: false });
method.doLogin({ reconnect: true });
}
}
}

View File

@ -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()

View File

@ -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,

View File

@ -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);
}
);
}
}
});

View File

@ -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);

View File

@ -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");
});
}
}
});

View File

@ -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"),

View File

@ -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;

View File

@ -10,7 +10,7 @@ export default {
).selectable_avatars_enabled;
container
.lookup("app-events:main")
.lookup("service:app-events")
.on("show-avatar-select", this, "_showAvatarSelect");
},

View File

@ -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");
},

View File

@ -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);

View File

@ -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 => {

View File

@ -9,7 +9,7 @@ export default {
this.container = container;
container
.lookup("app-events:main")
.lookup("service:app-events")
.on("notifications:changed", this, "_updateTitle");
},

View File

@ -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");
}

View File

@ -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");

View File

@ -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);
}

View File

@ -17,7 +17,8 @@ export function extraConnectorClass(name, obj) {
const DefaultConnectorClass = {
actions: {},
shouldRender: () => true,
setupComponent() {}
setupComponent() {},
teardownComponent() {}
};
function findOutlets(collection, callback) {

View File

@ -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);
});

View File

@ -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) {

View File

@ -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) {

View 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);
});
}

View File

@ -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);

View File

@ -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());
}
});

View File

@ -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"));
}
});

View File

@ -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) {

View File

@ -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);

View File

@ -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: {

View File

@ -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);

View File

@ -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);

View File

@ -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();
}
}
});

View File

@ -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
});
}
})

View File

@ -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

View File

@ -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() {

View File

@ -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>

View File

@ -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}}

View File

@ -1,3 +1,3 @@
{{#link-to route args}}
{{#link-to route args class="cancel"}}
{{i18n 'cancel'}}
{{/link-to}}

View File

@ -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}}

View File

@ -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'>

View File

@ -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>

View File

@ -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>

View File

@ -6,4 +6,5 @@
id=inputId
autocorrect="off"
autocapitalize="off"
autofocus="autofocus"}}
autofocus="autofocus"
placeholder=placeholder}}

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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}}

View File

@ -20,6 +20,7 @@
{{#unless helpSeen}}
{{d-button class="btn-large"
label="forgot_password.button_help"
icon="question-circle"
action=(action "help")}}
{{/unless}}
{{/unless}}

View File

@ -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>

View File

@ -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}}
&nbsp;{{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}}

View File

@ -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}}

View File

@ -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>

View File

@ -0,0 +1,15 @@
{{#d-modal-body}}
<div class="form-horizontal">
{{input type="text" value=model.name}}
</div>
<div class='second-factor instructions'>
{{i18n 'user.second_factor.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}}

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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;
},

View File

@ -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);
});
}
},

View File

@ -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`,

View File

@ -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) {

View File

@ -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

View File

@ -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();
}
})();

View File

@ -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() {

View File

@ -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 {

View File

@ -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;
}

View File

@ -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%;
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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