Version bump
This commit is contained in:
commit
f5416a987f
19
Gemfile
19
Gemfile
@ -14,13 +14,13 @@ if rails_master?
|
||||
else
|
||||
# until rubygems gives us optional dependencies we are stuck with this
|
||||
# bundle update actionmailer actionpack actionview activemodel activerecord activesupport railties
|
||||
gem 'actionmailer', '5.2.2.1'
|
||||
gem 'actionpack', '5.2.2.1'
|
||||
gem 'actionview', '5.2.2.1'
|
||||
gem 'activemodel', '5.2.2.1'
|
||||
gem 'activerecord', '5.2.2.1'
|
||||
gem 'activesupport', '5.2.2.1'
|
||||
gem 'railties', '5.2.2.1'
|
||||
gem 'actionmailer', '5.2.3'
|
||||
gem 'actionpack', '5.2.3'
|
||||
gem 'actionview', '5.2.3'
|
||||
gem 'activemodel', '5.2.3'
|
||||
gem 'activerecord', '5.2.3'
|
||||
gem 'activesupport', '5.2.3'
|
||||
gem 'railties', '5.2.3'
|
||||
gem 'sprockets-rails'
|
||||
end
|
||||
|
||||
@ -44,7 +44,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.83'
|
||||
gem 'onebox', '1.8.86'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -122,6 +122,7 @@ group :test do
|
||||
gem 'minitest', require: false
|
||||
gem 'danger'
|
||||
gem 'simplecov', require: false
|
||||
gem "test-prof"
|
||||
end
|
||||
|
||||
group :test, :development do
|
||||
@ -135,7 +136,7 @@ group :test, :development do
|
||||
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
|
||||
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
|
||||
gem 'rspec-rails', require: false
|
||||
gem 'shoulda', require: false
|
||||
gem 'shoulda-matchers', '~> 3.1', '>= 3.1.3', require: false
|
||||
gem 'rspec-html-matchers'
|
||||
gem 'pry-nav'
|
||||
gem 'byebug', require: ENV['RM_INFO'].nil?
|
||||
|
||||
78
Gemfile.lock
78
Gemfile.lock
@ -1,37 +1,37 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (5.2.2.1)
|
||||
actionpack (= 5.2.2.1)
|
||||
actionview (= 5.2.2.1)
|
||||
activejob (= 5.2.2.1)
|
||||
actionmailer (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activejob (= 5.2.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.2.1)
|
||||
actionview (= 5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
actionpack (5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
rack (~> 2.0)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
actionview (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
activejob (5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
activejob (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
activerecord (5.2.2.1)
|
||||
activemodel (= 5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
activemodel (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
activerecord (5.2.3)
|
||||
activemodel (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
arel (>= 9.0)
|
||||
activesupport (5.2.2.1)
|
||||
activesupport (5.2.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
@ -86,7 +86,7 @@ GEM
|
||||
open4 (~> 1.3)
|
||||
coderay (1.1.2)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.1.4)
|
||||
concurrent-ruby (1.1.5)
|
||||
connection_pool (2.2.2)
|
||||
cork (0.3.0)
|
||||
colored2 (~> 3.1)
|
||||
@ -161,7 +161,7 @@ GEM
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http_accept_language (2.0.5)
|
||||
i18n (1.5.3)
|
||||
i18n (1.6.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_size (1.5.0)
|
||||
in_threads (1.5.0)
|
||||
@ -214,7 +214,7 @@ GEM
|
||||
mocha (1.5.0)
|
||||
metaclass (~> 0.0.1)
|
||||
mock_redis (0.18.0)
|
||||
moneta (1.1.0)
|
||||
moneta (1.1.1)
|
||||
msgpack (1.2.6)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
@ -222,7 +222,7 @@ GEM
|
||||
mustache (1.1.0)
|
||||
nap (1.1.0)
|
||||
no_proxy_fix (0.1.2)
|
||||
nokogiri (1.10.1)
|
||||
nokogiri (1.10.3)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (2.0.1)
|
||||
nokogiri (~> 1.8, >= 1.8.4)
|
||||
@ -263,7 +263,7 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.83)
|
||||
onebox (1.8.86)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -312,9 +312,9 @@ GEM
|
||||
rails_multisite (2.0.6)
|
||||
activerecord (> 4.2, < 6)
|
||||
railties (> 4.2, < 6)
|
||||
railties (5.2.2.1)
|
||||
actionpack (= 5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
railties (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
@ -400,12 +400,8 @@ GEM
|
||||
seed-fu (2.3.9)
|
||||
activerecord (>= 3.1)
|
||||
activesupport (>= 3.1)
|
||||
shoulda (3.5.0)
|
||||
shoulda-context (~> 1.0, >= 1.0.1)
|
||||
shoulda-matchers (>= 1.4.1, < 3.0)
|
||||
shoulda-context (1.2.2)
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
shoulda-matchers (3.1.3)
|
||||
activesupport (>= 4.0.0)
|
||||
sidekiq (5.2.5)
|
||||
connection_pool (~> 2.2, >= 2.2.2)
|
||||
rack (>= 1.5.0)
|
||||
@ -428,6 +424,7 @@ GEM
|
||||
stackprof (0.2.12)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
test-prof (0.8.0)
|
||||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.8)
|
||||
@ -455,13 +452,13 @@ PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
actionmailer (= 5.2.2.1)
|
||||
actionpack (= 5.2.2.1)
|
||||
actionview (= 5.2.2.1)
|
||||
actionmailer (= 5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
active_model_serializers (~> 0.8.3)
|
||||
activemodel (= 5.2.2.1)
|
||||
activerecord (= 5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
activemodel (= 5.2.3)
|
||||
activerecord (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
annotate
|
||||
aws-sdk-s3
|
||||
aws-sdk-sns
|
||||
@ -524,7 +521,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.83)
|
||||
onebox (= 1.8.86)
|
||||
openid-redis-store
|
||||
parallel_tests
|
||||
pg
|
||||
@ -535,7 +532,7 @@ DEPENDENCIES
|
||||
rack-mini-profiler
|
||||
rack-protection
|
||||
rails_multisite
|
||||
railties (= 5.2.2.1)
|
||||
railties (= 5.2.3)
|
||||
rake
|
||||
rb-fsevent
|
||||
rb-inotify (~> 0.9)
|
||||
@ -557,12 +554,13 @@ DEPENDENCIES
|
||||
sassc
|
||||
sassc-rails
|
||||
seed-fu
|
||||
shoulda
|
||||
shoulda-matchers (~> 3.1, >= 3.1.3)
|
||||
sidekiq
|
||||
simplecov
|
||||
sprockets-rails
|
||||
sshkey
|
||||
stackprof
|
||||
test-prof
|
||||
thor
|
||||
tilt
|
||||
uglifier
|
||||
|
||||
@ -27,6 +27,7 @@ export default Ember.Component.extend({
|
||||
@computed("currentTargetName", "fieldName")
|
||||
activeSectionMode(targetName, fieldName) {
|
||||
if (["settings", "translations"].includes(targetName)) return "yaml";
|
||||
if (["extra_scss"].includes(targetName)) return "scss";
|
||||
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
|
||||
},
|
||||
|
||||
@ -73,7 +74,7 @@ export default Ember.Component.extend({
|
||||
|
||||
addField(name) {
|
||||
if (!name) return;
|
||||
name = name.replace(/\W/g, "");
|
||||
name = name.replace(/[^a-zA-Z0-9-_/]/g, "");
|
||||
this.get("theme").setField(this.get("currentTargetName"), name, "");
|
||||
this.setProperties({ newFieldName: "", addingField: false });
|
||||
this.fieldAdded(this.get("currentTargetName"), name);
|
||||
|
||||
@ -6,6 +6,8 @@ import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
|
||||
export default Ember.Component.extend({
|
||||
tagName: "li",
|
||||
expandDetails: null,
|
||||
expandDetailsRequestKey: "request",
|
||||
expandDetailsResponseKey: "response",
|
||||
|
||||
@computed("model.status")
|
||||
statusColorClasses(status) {
|
||||
@ -29,6 +31,20 @@ export default Ember.Component.extend({
|
||||
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
|
||||
},
|
||||
|
||||
@computed("expandDetails")
|
||||
expandRequestIcon(expandDetails) {
|
||||
return expandDetails === this.get("expandDetailsRequestKey")
|
||||
? "ellipsis-h"
|
||||
: "ellipsis-v";
|
||||
},
|
||||
|
||||
@computed("expandDetails")
|
||||
expandResponseIcon(expandDetails) {
|
||||
return expandDetails === this.get("expandDetailsResponseKey")
|
||||
? "ellipsis-h"
|
||||
: "ellipsis-v";
|
||||
},
|
||||
|
||||
actions: {
|
||||
redeliver() {
|
||||
return bootbox.confirm(
|
||||
@ -53,7 +69,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
toggleRequest() {
|
||||
const expandDetailsKey = "request";
|
||||
const expandDetailsKey = this.get("expandDetailsRequestKey");
|
||||
|
||||
if (this.get("expandDetails") !== expandDetailsKey) {
|
||||
let headers = _.extend(
|
||||
@ -75,7 +91,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
toggleResponse() {
|
||||
const expandDetailsKey = "response";
|
||||
const expandDetailsKey = this.get("expandDetailsResponseKey");
|
||||
|
||||
if (this.get("expandDetails") !== expandDetailsKey) {
|
||||
this.setProperties({
|
||||
|
||||
@ -13,11 +13,9 @@ export default Ember.Controller.extend({
|
||||
const filterActionId = this.get("filterActionId");
|
||||
if (filterActionId) {
|
||||
this._changeFilters({
|
||||
action_name: this.get("userHistoryActions").findBy(
|
||||
"id",
|
||||
parseInt(filterActionId, 10)
|
||||
).name_raw,
|
||||
action_id: filterActionId
|
||||
action_name: filterActionId,
|
||||
action_id: this.get("userHistoryActions").findBy("id", filterActionId)
|
||||
.action_id
|
||||
});
|
||||
}
|
||||
}.observes("filterActionId"),
|
||||
@ -54,11 +52,12 @@ export default Ember.Controller.extend({
|
||||
.then(result => {
|
||||
this.set("model", result.staff_action_logs);
|
||||
if (this.get("userHistoryActions").length === 0) {
|
||||
let actionTypes = result.user_history_actions.map(pair => {
|
||||
let actionTypes = result.user_history_actions.map(action => {
|
||||
return {
|
||||
id: pair.id,
|
||||
name: I18n.t("admin.logs.staff_actions.actions." + pair.name),
|
||||
name_raw: pair.name
|
||||
id: action.id,
|
||||
action_id: action.action_id,
|
||||
name: I18n.t("admin.logs.staff_actions.actions." + action.id),
|
||||
name_raw: action.id
|
||||
};
|
||||
});
|
||||
actionTypes = _.sortBy(actionTypes, row => row.name);
|
||||
|
||||
@ -34,7 +34,6 @@ const SCSS_VARIABLE_NAMES = [
|
||||
"facebook",
|
||||
"cas",
|
||||
"twitter",
|
||||
"yahoo",
|
||||
"github",
|
||||
"base-font-size",
|
||||
"base-line-height",
|
||||
|
||||
@ -27,6 +27,13 @@ const Theme = RestModel.extend({
|
||||
icon: "globe",
|
||||
advanced: true,
|
||||
customNames: true
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "extra_scss",
|
||||
icon: "paint-brush",
|
||||
advanced: true,
|
||||
customNames: true
|
||||
}
|
||||
].map(target => {
|
||||
target["edited"] = this.hasEdited(target.name);
|
||||
@ -46,6 +53,14 @@ const Theme = RestModel.extend({
|
||||
"footer"
|
||||
];
|
||||
|
||||
const scss_fields = (this.get("theme_fields") || [])
|
||||
.filter(f => f.target === "extra_scss" && f.name !== "")
|
||||
.map(f => f.name);
|
||||
|
||||
if (scss_fields.length < 1) {
|
||||
scss_fields.push("importable_scss");
|
||||
}
|
||||
|
||||
return {
|
||||
common: [...common, "embedded_scss"],
|
||||
desktop: common,
|
||||
@ -56,7 +71,8 @@ const Theme = RestModel.extend({
|
||||
...(this.get("theme_fields") || [])
|
||||
.filter(f => f.target === "translations" && f.name !== "en")
|
||||
.map(f => f.name)
|
||||
]
|
||||
],
|
||||
extra_scss: scss_fields
|
||||
};
|
||||
},
|
||||
|
||||
@ -71,7 +87,7 @@ const Theme = RestModel.extend({
|
||||
error: this.hasError(target, fieldName)
|
||||
};
|
||||
|
||||
if (target === "translations") {
|
||||
if (target === "translations" || target === "extra_scss") {
|
||||
field.translatedName = fieldName;
|
||||
} else {
|
||||
field.translatedName = I18n.t(
|
||||
|
||||
@ -16,6 +16,7 @@ export default Ember.Route.extend({
|
||||
this._super(...arguments);
|
||||
|
||||
const parentController = this.controllerFor("adminCustomizeThemes");
|
||||
|
||||
parentController.setProperties({
|
||||
editingTheme: false,
|
||||
currentTab: model.get("component") ? COMPONENTS : THEMES
|
||||
@ -26,7 +27,8 @@ export default Ember.Route.extend({
|
||||
parentController: parentController,
|
||||
allThemes: parentController.get("model"),
|
||||
colorSchemeId: model.get("color_scheme_id"),
|
||||
colorSchemes: parentController.get("model.extras.color_schemes")
|
||||
colorSchemes: parentController.get("model.extras.color_schemes"),
|
||||
editingName: false
|
||||
});
|
||||
|
||||
this.handleHighlight(model);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{#d-section class="current-badges"}}
|
||||
<div class="badge-intro">
|
||||
<div class="badge-intro admin-intro">
|
||||
<img src={{badgeIntroEmoji}} class="badge-intro-emoji">
|
||||
<div class="content-wrapper">
|
||||
<h1>{{i18n 'admin.badges.badge_intro.title'}}</h1>
|
||||
@ -13,4 +13,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/d-section}}
|
||||
{{/d-section}}
|
||||
|
||||
@ -40,11 +40,11 @@
|
||||
{{else}}
|
||||
{{number model.currentTotal noTitle="true"}}{{#if model.percent}}%{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
{{#if model.trendIcon}}
|
||||
{{d-icon model.trendIcon class="icon"}}
|
||||
{{/if}}
|
||||
{{#if model.trendIcon}}
|
||||
{{d-icon model.trendIcon class="icon"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
<div class="col timestamp">{{createdAt}}</div>
|
||||
<div class="col completion">{{completion}}</div>
|
||||
<div class="col actions">
|
||||
{{d-button icon="ellipsis-v" action=(action "toggleRequest") label="admin.web_hooks.events.request"}}
|
||||
{{d-button icon="ellipsis-v" action=(action "toggleResponse") label="admin.web_hooks.events.response"}}
|
||||
{{d-button icon=expandRequestIcon action=(action "toggleRequest") label="admin.web_hooks.events.request"}}
|
||||
{{d-button icon=expandResponseIcon action=(action "toggleResponse") label="admin.web_hooks.events.response"}}
|
||||
{{d-button icon="sync" action=(action "redeliver") label="admin.web_hooks.events.redeliver"}}
|
||||
</div>
|
||||
{{#if expandDetails}}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="themes-intro">
|
||||
<div class="themes-intro admin-intro">
|
||||
<img src={{womanArtistEmojiURL}}>
|
||||
<div class="content-wrapper">
|
||||
<h1>{{I18n "admin.customize.theme.themes_intro"}}</h1>
|
||||
|
||||
@ -122,10 +122,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-column">
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
//= require ./deprecated
|
||||
|
||||
// Stuff we need to load first
|
||||
//= require ./discourse/helpers/parse-html
|
||||
//= require ./discourse/lib/to-markdown
|
||||
//= require ./discourse/lib/utilities
|
||||
//= require ./discourse/lib/page-visible
|
||||
|
||||
@ -34,7 +34,8 @@ const REPLACEMENTS = {
|
||||
"notification.granted_badge": "certificate",
|
||||
"notification.topic_reminder": "far-clock",
|
||||
"notification.watching_first_post": "far-dot-circle",
|
||||
"notification.group_message_summary": "group"
|
||||
"notification.group_message_summary": "group",
|
||||
"notification.post_approved": "check"
|
||||
};
|
||||
|
||||
// TODO: use lib/svg_sprite/fa4-renames.json here
|
||||
|
||||
@ -51,10 +51,7 @@ const Discourse = Ember.Application.extend({
|
||||
$("title").text(title);
|
||||
}
|
||||
|
||||
var displayCount = Discourse.User.current()
|
||||
? this.get("notificationCount")
|
||||
: this.get("contextCount");
|
||||
|
||||
var displayCount = this.get("displayCount");
|
||||
if (displayCount > 0 && !Discourse.User.currentProp("dynamic_favicon")) {
|
||||
title = `(${displayCount}) ${title}`;
|
||||
}
|
||||
@ -62,6 +59,14 @@ const Discourse = Ember.Application.extend({
|
||||
document.title = title;
|
||||
},
|
||||
|
||||
@computed("contextCount", "notificationCount")
|
||||
displayCount() {
|
||||
return Discourse.User.current() &&
|
||||
Discourse.User.currentProp("title_count_mode") === "notifications"
|
||||
? this.get("notificationCount")
|
||||
: this.get("contextCount");
|
||||
},
|
||||
|
||||
@observes("contextCount", "notificationCount")
|
||||
faviconChanged() {
|
||||
if (Discourse.User.currentProp("dynamic_favicon")) {
|
||||
@ -74,9 +79,7 @@ const Discourse = Ember.Application.extend({
|
||||
url = Discourse.getURL("/favicon/proxied?" + encodeURIComponent(url));
|
||||
}
|
||||
|
||||
var displayCount = Discourse.User.current()
|
||||
? this.get("notificationCount")
|
||||
: this.get("contextCount");
|
||||
var displayCount = this.get("displayCount");
|
||||
|
||||
new window.Favcount(url).set(displayCount);
|
||||
}
|
||||
|
||||
@ -963,7 +963,11 @@ export default Ember.Component.extend({
|
||||
unshift: true
|
||||
});
|
||||
|
||||
if (this.get("allowUpload") && this.get("uploadIcon")) {
|
||||
if (
|
||||
this.get("allowUpload") &&
|
||||
this.get("uploadIcon") &&
|
||||
!this.site.mobileView
|
||||
) {
|
||||
toolbar.addButton({
|
||||
id: "upload",
|
||||
group: "insertions",
|
||||
|
||||
@ -22,6 +22,7 @@ export default Ember.Component.extend({
|
||||
this._super(...arguments);
|
||||
this.appEvents.off("modal-body:flash", this, "_flash");
|
||||
this.appEvents.off("modal-body:clearFlash", this, "_clearFlash");
|
||||
this.appEvents.trigger("modal:body-dismissed");
|
||||
},
|
||||
|
||||
_afterFirstRender() {
|
||||
|
||||
@ -73,7 +73,7 @@ export default Ember.Component.extend({
|
||||
$("html").off("keydown.discourse-modal");
|
||||
},
|
||||
|
||||
click(e) {
|
||||
mouseDown(e) {
|
||||
if (!this.get("dismissable")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2,12 +2,10 @@ import DiscourseURL from "discourse/lib/url";
|
||||
import AddArchetypeClass from "discourse/mixins/add-archetype-class";
|
||||
import ClickTrack from "discourse/lib/click-track";
|
||||
import Scrolling from "discourse/mixins/scrolling";
|
||||
import { selectedText } from "discourse/lib/utilities";
|
||||
import MobileScrollDirection from "discourse/mixins/mobile-scroll-direction";
|
||||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 300;
|
||||
// Small buffer so that very tiny scrolls don't trigger mobile header switch
|
||||
const MOBILE_SCROLL_TOLERANCE = 5;
|
||||
|
||||
function highlight(postNumber) {
|
||||
const $contents = $(`#post_${postNumber} .topic-body`);
|
||||
@ -16,222 +14,179 @@ function highlight(postNumber) {
|
||||
$contents.on("animationend", () => $contents.removeClass("highlighted"));
|
||||
}
|
||||
|
||||
export default Ember.Component.extend(AddArchetypeClass, Scrolling, {
|
||||
userFilters: Ember.computed.alias("topic.userFilters"),
|
||||
classNameBindings: [
|
||||
"multiSelect",
|
||||
"topic.archetype",
|
||||
"topic.is_warning",
|
||||
"topic.category.read_restricted:read_restricted",
|
||||
"topic.deleted:deleted-topic",
|
||||
"topic.categoryClass",
|
||||
"topic.tagClasses"
|
||||
],
|
||||
menuVisible: true,
|
||||
SHORT_POST: 1200,
|
||||
export default Ember.Component.extend(
|
||||
AddArchetypeClass,
|
||||
Scrolling,
|
||||
MobileScrollDirection,
|
||||
{
|
||||
userFilters: Ember.computed.alias("topic.userFilters"),
|
||||
classNameBindings: [
|
||||
"multiSelect",
|
||||
"topic.archetype",
|
||||
"topic.is_warning",
|
||||
"topic.category.read_restricted:read_restricted",
|
||||
"topic.deleted:deleted-topic",
|
||||
"topic.categoryClass",
|
||||
"topic.tagClasses"
|
||||
],
|
||||
menuVisible: true,
|
||||
SHORT_POST: 1200,
|
||||
|
||||
postStream: Ember.computed.alias("topic.postStream"),
|
||||
archetype: Ember.computed.alias("topic.archetype"),
|
||||
dockAt: 0,
|
||||
postStream: Ember.computed.alias("topic.postStream"),
|
||||
archetype: Ember.computed.alias("topic.archetype"),
|
||||
dockAt: 0,
|
||||
|
||||
_lastShowTopic: null,
|
||||
_lastShowTopic: null,
|
||||
|
||||
mobileScrollDirection: null,
|
||||
_mobileLastScroll: null,
|
||||
mobileScrollDirection: null,
|
||||
|
||||
@observes("enteredAt")
|
||||
_enteredTopic() {
|
||||
// Ember is supposed to only call observers when values change but something
|
||||
// in our view set up is firing this observer with the same value. This check
|
||||
// prevents scrolled from being called twice.
|
||||
const enteredAt = this.get("enteredAt");
|
||||
if (enteredAt && this.get("lastEnteredAt") !== enteredAt) {
|
||||
this._lastShowTopic = null;
|
||||
Ember.run.schedule("afterRender", () => this.scrolled());
|
||||
this.set("lastEnteredAt", enteredAt);
|
||||
}
|
||||
},
|
||||
|
||||
_highlightPost(postNumber) {
|
||||
Ember.run.scheduleOnce("afterRender", null, highlight, postNumber);
|
||||
},
|
||||
|
||||
_updateTopic(topic) {
|
||||
if (topic === null) {
|
||||
this._lastShowTopic = false;
|
||||
this.appEvents.trigger("header:hide-topic");
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = window.pageYOffset || $("html").scrollTop();
|
||||
this._lastShowTopic = this.showTopicInHeader(topic, offset);
|
||||
|
||||
if (this._lastShowTopic) {
|
||||
this.appEvents.trigger("header:show-topic", topic);
|
||||
} else {
|
||||
this.appEvents.trigger("header:hide-topic");
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this.bindScrolling({ name: "topic-view" });
|
||||
|
||||
$(window).on("resize.discourse-on-scroll", () => this.scrolled());
|
||||
|
||||
this.$().on(
|
||||
"mouseup.discourse-redirect",
|
||||
".cooked a, a.track-link",
|
||||
function(e) {
|
||||
// bypass if we are selecting stuff
|
||||
const selection = window.getSelection && window.getSelection();
|
||||
if (selection.type === "Range" || selection.rangeCount > 0) {
|
||||
if (selectedText() !== "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const $target = $(e.target);
|
||||
if (
|
||||
$target.hasClass("mention") ||
|
||||
$target.parents(".expanded-embed").length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ClickTrack.trackClick(e);
|
||||
@observes("enteredAt")
|
||||
_enteredTopic() {
|
||||
// Ember is supposed to only call observers when values change but something
|
||||
// in our view set up is firing this observer with the same value. This check
|
||||
// prevents scrolled from being called twice.
|
||||
const enteredAt = this.get("enteredAt");
|
||||
if (enteredAt && this.get("lastEnteredAt") !== enteredAt) {
|
||||
this._lastShowTopic = null;
|
||||
Ember.run.schedule("afterRender", () => this.scrolled());
|
||||
this.set("lastEnteredAt", enteredAt);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
this.appEvents.on("post:highlight", this, "_highlightPost");
|
||||
_highlightPost(postNumber) {
|
||||
Ember.run.scheduleOnce("afterRender", null, highlight, postNumber);
|
||||
},
|
||||
|
||||
this.appEvents.on("header:update-topic", this, "_updateTopic");
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.unbindScrolling("topic-view");
|
||||
$(window).unbind("resize.discourse-on-scroll");
|
||||
|
||||
// Unbind link tracking
|
||||
this.$().off("mouseup.discourse-redirect", ".cooked a, a.track-link");
|
||||
|
||||
this.resetExamineDockCache();
|
||||
|
||||
// this happens after route exit, stuff could have trickled in
|
||||
this.appEvents.trigger("header:hide-topic");
|
||||
this.appEvents.off("post:highlight", this, "_highlightPost");
|
||||
this.appEvents.off("header:update-topic", this, "_updateTopic");
|
||||
},
|
||||
|
||||
@observes("Discourse.hasFocus")
|
||||
gotFocus() {
|
||||
if (Discourse.get("hasFocus")) {
|
||||
this.scrolled();
|
||||
}
|
||||
},
|
||||
|
||||
resetExamineDockCache() {
|
||||
this.set("dockAt", 0);
|
||||
},
|
||||
|
||||
showTopicInHeader(topic, offset) {
|
||||
// On mobile, we show the header topic if the user has scrolled past the topic
|
||||
// title and the current scroll direction is down
|
||||
// On desktop the user only needs to scroll past the topic title.
|
||||
return (
|
||||
offset > this.dockAt &&
|
||||
(!this.site.mobileView || this.mobileScrollDirection === "down")
|
||||
);
|
||||
},
|
||||
// The user has scrolled the window, or it is finished rendering and ready for processing.
|
||||
scrolled() {
|
||||
if (this.isDestroyed || this.isDestroying || this._state !== "inDOM") {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = window.pageYOffset || $("html").scrollTop();
|
||||
if (this.get("dockAt") === 0) {
|
||||
const title = $("#topic-title");
|
||||
if (title && title.length === 1) {
|
||||
this.set("dockAt", title.offset().top);
|
||||
_updateTopic(topic) {
|
||||
if (topic === null) {
|
||||
this._lastShowTopic = false;
|
||||
this.appEvents.trigger("header:hide-topic");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.set("hasScrolled", offset > 0);
|
||||
const offset = window.pageYOffset || $("html").scrollTop();
|
||||
this._lastShowTopic = this.showTopicInHeader(topic, offset);
|
||||
|
||||
const topic = this.get("topic");
|
||||
const showTopic = this.showTopicInHeader(topic, offset);
|
||||
if (showTopic !== this._lastShowTopic) {
|
||||
if (showTopic) {
|
||||
if (this._lastShowTopic) {
|
||||
this.appEvents.trigger("header:show-topic", topic);
|
||||
this._lastShowTopic = true;
|
||||
} else {
|
||||
if (!DiscourseURL.isJumpScheduled()) {
|
||||
const loadingNear = topic.get("postStream.loadingNearPost") || 1;
|
||||
if (loadingNear === 1) {
|
||||
this.appEvents.trigger("header:hide-topic");
|
||||
this._lastShowTopic = false;
|
||||
this.appEvents.trigger("header:hide-topic");
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this.bindScrolling({ name: "topic-view" });
|
||||
|
||||
$(window).on("resize.discourse-on-scroll", () => this.scrolled());
|
||||
|
||||
this.$().on(
|
||||
"click.discourse-redirect",
|
||||
".cooked a, a.track-link",
|
||||
function(e) {
|
||||
return ClickTrack.trackClick(e);
|
||||
}
|
||||
);
|
||||
|
||||
this.appEvents.on("post:highlight", this, "_highlightPost");
|
||||
|
||||
this.appEvents.on("header:update-topic", this, "_updateTopic");
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.unbindScrolling("topic-view");
|
||||
$(window).unbind("resize.discourse-on-scroll");
|
||||
|
||||
// Unbind link tracking
|
||||
this.$().off("click.discourse-redirect", ".cooked a, a.track-link");
|
||||
|
||||
this.resetExamineDockCache();
|
||||
|
||||
// this happens after route exit, stuff could have trickled in
|
||||
this.appEvents.trigger("header:hide-topic");
|
||||
this.appEvents.off("post:highlight", this, "_highlightPost");
|
||||
this.appEvents.off("header:update-topic", this, "_updateTopic");
|
||||
},
|
||||
|
||||
@observes("Discourse.hasFocus")
|
||||
gotFocus() {
|
||||
if (Discourse.get("hasFocus")) {
|
||||
this.scrolled();
|
||||
}
|
||||
},
|
||||
|
||||
resetExamineDockCache() {
|
||||
this.set("dockAt", 0);
|
||||
},
|
||||
|
||||
showTopicInHeader(topic, offset) {
|
||||
// On mobile, we show the header topic if the user has scrolled past the topic
|
||||
// title and the current scroll direction is down
|
||||
// On desktop the user only needs to scroll past the topic title.
|
||||
return (
|
||||
offset > this.dockAt &&
|
||||
(!this.site.mobileView || this.mobileScrollDirection === "down")
|
||||
);
|
||||
},
|
||||
// The user has scrolled the window, or it is finished rendering and ready for processing.
|
||||
scrolled() {
|
||||
if (this.isDestroyed || this.isDestroying || this._state !== "inDOM") {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = window.pageYOffset || $("html").scrollTop();
|
||||
if (this.get("dockAt") === 0) {
|
||||
const title = $("#topic-title");
|
||||
if (title && title.length === 1) {
|
||||
this.set("dockAt", title.offset().top);
|
||||
}
|
||||
}
|
||||
|
||||
this.set("hasScrolled", offset > 0);
|
||||
|
||||
const topic = this.get("topic");
|
||||
const showTopic = this.showTopicInHeader(topic, offset);
|
||||
if (showTopic !== this._lastShowTopic) {
|
||||
if (showTopic) {
|
||||
this.appEvents.trigger("header:show-topic", topic);
|
||||
this._lastShowTopic = true;
|
||||
} else {
|
||||
if (!DiscourseURL.isJumpScheduled()) {
|
||||
const loadingNear = topic.get("postStream.loadingNearPost") || 1;
|
||||
if (loadingNear === 1) {
|
||||
this.appEvents.trigger("header:hide-topic");
|
||||
this._lastShowTopic = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since the user has scrolled, we need to check the scroll direction on mobile.
|
||||
// We use throttle instead of debounce because we want the switch to occur
|
||||
// at the start of the scroll. This feels a lot more snappy compared to waiting
|
||||
// for the scroll to end if we debounce.
|
||||
if (this.site.mobileView && this.hasScrolled) {
|
||||
Ember.run.throttle(
|
||||
this,
|
||||
this._mobileScrollDirectionCheck,
|
||||
offset,
|
||||
MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE
|
||||
// Since the user has scrolled, we need to check the scroll direction on mobile.
|
||||
// We use throttle instead of debounce because we want the switch to occur
|
||||
// at the start of the scroll. This feels a lot more snappy compared to waiting
|
||||
// for the scroll to end if we debounce.
|
||||
if (this.site.mobileView && this.hasScrolled) {
|
||||
Ember.run.throttle(
|
||||
this,
|
||||
this.calculateDirection,
|
||||
offset,
|
||||
MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger a scrolled event
|
||||
this.appEvents.trigger("topic:scrolled", offset);
|
||||
},
|
||||
|
||||
// We observe the scroll direction on mobile and if it's down, we show the topic
|
||||
// in the header, otherwise, we hide it.
|
||||
@observes("mobileScrollDirection")
|
||||
toggleMobileHeaderTopic() {
|
||||
return this.appEvents.trigger(
|
||||
"header:update-topic",
|
||||
this.mobileScrollDirection === "down" ? this.get("topic") : null
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger a scrolled event
|
||||
this.appEvents.trigger("topic:scrolled", offset);
|
||||
},
|
||||
|
||||
_mobileScrollDirectionCheck(offset) {
|
||||
// Difference between this scroll and the one before it.
|
||||
const delta = Math.floor(offset - this._mobileLastScroll);
|
||||
|
||||
// This is a tiny scroll, so we ignore it.
|
||||
if (delta <= MOBILE_SCROLL_TOLERANCE && delta >= -MOBILE_SCROLL_TOLERANCE)
|
||||
return;
|
||||
|
||||
const prevDirection = this.mobileScrollDirection;
|
||||
const currDirection = delta > 0 ? "down" : "up";
|
||||
|
||||
if (currDirection !== prevDirection) {
|
||||
this.set("mobileScrollDirection", currDirection);
|
||||
}
|
||||
|
||||
// We store this to compare against it the next time the user scrolls
|
||||
this._mobileLastScroll = Math.floor(offset);
|
||||
|
||||
// If the user reaches the very bottom of the topic, we want to reset the
|
||||
// scroll direction in order for the header to switch back.
|
||||
const distanceToTopicBottom = Math.floor(
|
||||
$("body").height() - offset - $(window).height()
|
||||
);
|
||||
|
||||
// Not at the bottom yet
|
||||
if (distanceToTopicBottom > 0) return;
|
||||
|
||||
// We're at the bottom now, so we reset the direction.
|
||||
this.set("mobileScrollDirection", null);
|
||||
},
|
||||
|
||||
// We observe the scroll direction on mobile and if it's down, we show the topic
|
||||
// in the header, otherwise, we hide it.
|
||||
@observes("mobileScrollDirection")
|
||||
toggleMobileHeaderTopic() {
|
||||
return this.appEvents.trigger(
|
||||
"header:update-topic",
|
||||
this.mobileScrollDirection === "down" ? this.get("topic") : null
|
||||
);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
@ -360,12 +360,16 @@ export default Ember.Component.extend({
|
||||
.off("touchstart")
|
||||
.on("touchstart", "button.emoji", touchStartEvent => {
|
||||
const $this = $(touchStartEvent.currentTarget);
|
||||
|
||||
$this.on("touchend", touchEndEvent => {
|
||||
touchEndEvent.preventDefault();
|
||||
touchEndEvent.stopPropagation();
|
||||
|
||||
handler.bind(self)(touchEndEvent);
|
||||
$this.off("touchend");
|
||||
});
|
||||
|
||||
$this.on("touchmove", () => $this.off("touchend"));
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
$emojisContainer
|
||||
|
||||
161
app/assets/javascripts/discourse/components/footer-nav.js.es6
Normal file
161
app/assets/javascripts/discourse/components/footer-nav.js.es6
Normal file
@ -0,0 +1,161 @@
|
||||
import MountWidget from "discourse/components/mount-widget";
|
||||
import MobileScrollDirection from "discourse/mixins/mobile-scroll-direction";
|
||||
import Scrolling from "discourse/mixins/scrolling";
|
||||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
import { isiPad } from "discourse/lib/utilities";
|
||||
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
|
||||
|
||||
const MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE = 150;
|
||||
|
||||
const FooterNavComponent = MountWidget.extend(
|
||||
Scrolling,
|
||||
MobileScrollDirection,
|
||||
{
|
||||
widget: "footer-nav",
|
||||
mobileScrollDirection: null,
|
||||
scrollEventDisabled: false,
|
||||
classNames: ["footer-nav", "visible"],
|
||||
routeHistory: [],
|
||||
currentRouteIndex: 0,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
backForwardClicked: null,
|
||||
|
||||
buildArgs() {
|
||||
return {
|
||||
canGoBack: this.canGoBack,
|
||||
canGoForward: this.canGoForward
|
||||
};
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this.appEvents.on("page:changed", this, "_routeChanged");
|
||||
|
||||
if (isAppWebview()) {
|
||||
this.appEvents.on("modal:body-shown", this, "_modalOn");
|
||||
this.appEvents.on("modal:body-dismissed", this, "_modalOff");
|
||||
}
|
||||
|
||||
if (isiPad()) {
|
||||
$("body").addClass("footer-nav-ipad");
|
||||
} else {
|
||||
this.bindScrolling({ name: "footer-nav" });
|
||||
$(window).on("resize.footer-nav-on-scroll", () => this.scrolled());
|
||||
this.appEvents.on("composer:opened", this, "_composerOpened");
|
||||
this.appEvents.on("composer:closed", this, "_composerClosed");
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.appEvents.off("page:changed", this, "_routeChanged");
|
||||
|
||||
if (isAppWebview()) {
|
||||
this.appEvents.off("modal:body-shown", this, "_modalOn");
|
||||
this.appEvents.off("modal:body-removed", this, "_modalOff");
|
||||
}
|
||||
|
||||
if (isiPad()) {
|
||||
$("body").removeClass("footer-nav-ipad");
|
||||
} else {
|
||||
this.unbindScrolling("footer-nav");
|
||||
$(window).unbind("resize.footer-nav-on-scroll");
|
||||
this.appEvents.off("composer:opened", this, "_composerOpened");
|
||||
this.appEvents.off("composer:closed", this, "_composerClosed");
|
||||
}
|
||||
},
|
||||
|
||||
// The user has scrolled the window, or it is finished rendering and ready for processing.
|
||||
scrolled() {
|
||||
if (
|
||||
this.isDestroyed ||
|
||||
this.isDestroying ||
|
||||
this._state !== "inDOM" ||
|
||||
this.scrollEventDisabled
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = window.pageYOffset || $("html").scrollTop();
|
||||
|
||||
Ember.run.throttle(
|
||||
this,
|
||||
this.calculateDirection,
|
||||
offset,
|
||||
MOBILE_SCROLL_DIRECTION_CHECK_THROTTLE
|
||||
);
|
||||
},
|
||||
|
||||
// We observe the scroll direction on mobile and if it's down, we show the topic
|
||||
// in the header, otherwise, we hide it.
|
||||
@observes("mobileScrollDirection")
|
||||
toggleMobileFooter() {
|
||||
this.$().toggleClass(
|
||||
"visible",
|
||||
this.mobileScrollDirection === null ? true : false
|
||||
);
|
||||
// body class used to adjust positioning of #topic-progress-wrapper
|
||||
$("body").toggleClass(
|
||||
"footer-nav-visible",
|
||||
this.mobileScrollDirection === null ? true : false
|
||||
);
|
||||
},
|
||||
|
||||
_routeChanged(route) {
|
||||
// only update route history if not using back/forward nav
|
||||
if (this.backForwardClicked) {
|
||||
this.backForwardClicked = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.routeHistory.push(route.url);
|
||||
this.set("currentRouteIndex", this.routeHistory.length);
|
||||
|
||||
this.queueRerender();
|
||||
},
|
||||
|
||||
_composerOpened() {
|
||||
this.set("mobileScrollDirection", "down");
|
||||
this.set("scrollEventDisabled", true);
|
||||
},
|
||||
|
||||
_composerClosed() {
|
||||
this.set("mobileScrollDirection", null);
|
||||
this.set("scrollEventDisabled", false);
|
||||
},
|
||||
|
||||
_modalOn() {
|
||||
postRNWebviewMessage(
|
||||
"headerBg",
|
||||
$(".modal-backdrop").css("background-color")
|
||||
);
|
||||
},
|
||||
|
||||
_modalOff() {
|
||||
postRNWebviewMessage("headerBg", $(".d-header").css("background-color"));
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.set("currentRouteIndex", this.get("currentRouteIndex") - 1);
|
||||
this.backForwardClicked = true;
|
||||
window.history.back();
|
||||
},
|
||||
|
||||
goForward() {
|
||||
this.set("currentRouteIndex", this.get("currentRouteIndex") + 1);
|
||||
this.backForwardClicked = true;
|
||||
window.history.forward();
|
||||
},
|
||||
|
||||
@observes("currentRouteIndex")
|
||||
setBackForward() {
|
||||
let index = this.get("currentRouteIndex");
|
||||
|
||||
this.set("canGoBack", index > 1 || document.referrer ? true : false);
|
||||
this.set("canGoForward", index < this.routeHistory.length ? true : false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default FooterNavComponent;
|
||||
@ -21,6 +21,8 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
canEdit: Ember.computed.not("model.automatic"),
|
||||
|
||||
@computed("basicNameValidation", "uniqueNameValidation")
|
||||
nameValidation(basicNameValidation, uniqueNameValidation) {
|
||||
return uniqueNameValidation ? uniqueNameValidation : basicNameValidation;
|
||||
|
||||
@ -14,7 +14,6 @@ export default Ember.Component.extend({
|
||||
// page which is wrong.
|
||||
emailOrUsername: null,
|
||||
hasCustomMessage: false,
|
||||
hasCustomMessage: false,
|
||||
customMessage: null,
|
||||
inviteIcon: "envelope",
|
||||
invitingExistingUserToTopic: false,
|
||||
|
||||
@ -1,33 +1,16 @@
|
||||
import ClickTrack from "discourse/lib/click-track";
|
||||
import { selectedText } from "discourse/lib/utilities";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.$().on("mouseup.discourse-redirect", "#revisions a", function(e) {
|
||||
// bypass if we are selecting stuff
|
||||
const selection = window.getSelection && window.getSelection();
|
||||
if (selection.type === "Range" || selection.rangeCount > 0) {
|
||||
if (selectedText() !== "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const $target = $(e.target);
|
||||
if (
|
||||
$target.hasClass("mention") ||
|
||||
$target.parents(".expanded-embed").length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.$().on("click.discourse-redirect", "#revisions a", function(e) {
|
||||
return ClickTrack.trackClick(e);
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.$().off("mouseup.discourse-redirect", "#revisions a");
|
||||
this.$().off("click.discourse-redirect", "#revisions a");
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
showUsername: Ember.computed.gte("index", 1)
|
||||
});
|
||||
@ -236,15 +236,21 @@ export default MountWidget.extend({
|
||||
}
|
||||
|
||||
const onscreenPostNumbers = [];
|
||||
const readPostNumbers = [];
|
||||
|
||||
const prev = this._previouslyNearby;
|
||||
const newPrev = {};
|
||||
nearby.forEach(idx => {
|
||||
const post = posts.objectAt(idx);
|
||||
const postNumber = post.post_number;
|
||||
|
||||
delete prev[postNumber];
|
||||
|
||||
if (onscreen.indexOf(idx) !== -1) {
|
||||
onscreenPostNumbers.push(postNumber);
|
||||
if (post.read) {
|
||||
readPostNumbers.push(postNumber);
|
||||
}
|
||||
}
|
||||
newPrev[postNumber] = post;
|
||||
uncloak(post, this);
|
||||
@ -253,7 +259,7 @@ export default MountWidget.extend({
|
||||
Object.values(prev).forEach(node => cloak(node, this));
|
||||
|
||||
this._previouslyNearby = newPrev;
|
||||
this.screenTrack.setOnscreen(onscreenPostNumbers);
|
||||
this.screenTrack.setOnscreen(onscreenPostNumbers, readPostNumbers);
|
||||
},
|
||||
|
||||
_scrollTriggered() {
|
||||
@ -261,10 +267,8 @@ export default MountWidget.extend({
|
||||
},
|
||||
|
||||
_posted(staged) {
|
||||
const disableJumpReply = this.currentUser.get("disable_jump_reply");
|
||||
|
||||
this.queueRerender(() => {
|
||||
if (staged && !disableJumpReply) {
|
||||
if (staged) {
|
||||
const postNumber = staged.get("post_number");
|
||||
DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true });
|
||||
}
|
||||
|
||||
@ -20,6 +20,6 @@ export default TextField.extend({
|
||||
// iOS is crazy, without this we will not be
|
||||
// at the top of the page
|
||||
$(window).scrollTop(0);
|
||||
$searchInput.focus();
|
||||
$searchInput.trigger("touchstart").focus();
|
||||
}
|
||||
});
|
||||
|
||||
@ -7,11 +7,6 @@ import PanEvents, {
|
||||
SWIPE_VELOCITY_THRESHOLD
|
||||
} from "discourse/mixins/pan-events";
|
||||
|
||||
const _flagProperties = [];
|
||||
function addFlagProperty(prop) {
|
||||
_flagProperties.pushObject(prop);
|
||||
}
|
||||
|
||||
const PANEL_BODY_MARGIN = 30;
|
||||
|
||||
//android supports pulling in from the screen edges
|
||||
@ -32,7 +27,8 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
|
||||
|
||||
@observes(
|
||||
"currentUser.unread_notifications",
|
||||
"currentUser.unread_private_messages"
|
||||
"currentUser.unread_private_messages",
|
||||
"currentUser.reviewable_count"
|
||||
)
|
||||
notificationsChanged() {
|
||||
this.queueRerender();
|
||||
@ -274,10 +270,6 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
|
||||
|
||||
buildArgs() {
|
||||
return {
|
||||
flagCount: _flagProperties.reduce(
|
||||
(prev, cur) => prev + (this.get(cur) || 0),
|
||||
0
|
||||
),
|
||||
topic: this._topic,
|
||||
canSignUp: this.get("canSignUp")
|
||||
};
|
||||
@ -410,23 +402,6 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
|
||||
|
||||
export default SiteHeaderComponent;
|
||||
|
||||
function applyFlaggedProperties() {
|
||||
const args = _flagProperties.slice();
|
||||
args.push(
|
||||
function() {
|
||||
this.queueRerender();
|
||||
}.on("init")
|
||||
);
|
||||
|
||||
SiteHeaderComponent.reopen({
|
||||
_flagsChanged: Ember.observer.apply(this, args)
|
||||
});
|
||||
}
|
||||
|
||||
addFlagProperty("currentUser.reviewable_count");
|
||||
|
||||
export { addFlagProperty, applyFlaggedProperties };
|
||||
|
||||
export function headerHeight() {
|
||||
const $header = $("header.d-header");
|
||||
const headerOffset = $header.offset();
|
||||
|
||||
@ -2,10 +2,12 @@ export default Ember.Component.extend({
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
Ember.run.next(null, () => {
|
||||
this.$()
|
||||
.find("hr")
|
||||
.remove();
|
||||
this.$().ellipsis();
|
||||
const $this = this.$();
|
||||
|
||||
if ($this) {
|
||||
$this.find("hr").remove();
|
||||
$this.ellipsis();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -150,6 +150,9 @@ export default Ember.Component.extend(
|
||||
},
|
||||
|
||||
_showCallback(username, $target) {
|
||||
this._positionCard($target);
|
||||
this.setProperties({ visible: true, loading: true });
|
||||
|
||||
const args = { stats: false };
|
||||
args.include_post_count_for = this.get("topic.id");
|
||||
User.findByUsername(username, args)
|
||||
@ -160,8 +163,7 @@ export default Ember.Component.extend(
|
||||
user.topic_post_count[args.include_post_count_for]
|
||||
);
|
||||
}
|
||||
this._positionCard($target);
|
||||
this.setProperties({ user, visible: true });
|
||||
this.setProperties({ user });
|
||||
})
|
||||
.catch(() => this._close())
|
||||
.finally(() => this.set("loading", null));
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
import ClickTrack from "discourse/lib/click-track";
|
||||
import { selectedText } from "discourse/lib/utilities";
|
||||
import Post from "discourse/models/post";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Draft from "discourse/models/draft";
|
||||
@ -8,6 +7,16 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
|
||||
export default Ember.Component.extend(LoadMore, {
|
||||
_initialize: function() {
|
||||
const filter = this.get("stream.filter");
|
||||
if (filter) {
|
||||
this.set("classNames", [
|
||||
"user-stream",
|
||||
"filter-" + filter.toString().replace(",", "-")
|
||||
]);
|
||||
}
|
||||
}.on("init"),
|
||||
|
||||
loading: false,
|
||||
eyelineSelector: ".user-stream .item",
|
||||
classNames: ["user-stream"],
|
||||
@ -22,23 +31,7 @@ export default Ember.Component.extend(LoadMore, {
|
||||
$(window).on("resize.discourse-on-scroll", () => this.scrolled());
|
||||
|
||||
this.$().on("click.details-disabled", "details.disabled", () => false);
|
||||
this.$().on("mouseup.discourse-redirect", ".excerpt a", function(e) {
|
||||
// bypass if we are selecting stuff
|
||||
const selection = window.getSelection && window.getSelection();
|
||||
if (selection.type === "Range" || selection.rangeCount > 0) {
|
||||
if (selectedText() !== "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const $target = $(e.target);
|
||||
if (
|
||||
$target.hasClass("mention") ||
|
||||
$target.parents(".expanded-embed").length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.$().on("click.discourse-redirect", ".excerpt a", function(e) {
|
||||
return ClickTrack.trackClick(e);
|
||||
});
|
||||
}.on("didInsertElement"),
|
||||
@ -50,7 +43,7 @@ export default Ember.Component.extend(LoadMore, {
|
||||
this.$().off("click.details-disabled", "details.disabled");
|
||||
|
||||
// Unbind link tracking
|
||||
this.$().off("mouseup.discourse-redirect", ".excerpt a");
|
||||
this.$().off("click.discourse-redirect", ".excerpt a");
|
||||
}.on("willDestroyElement"),
|
||||
|
||||
actions: {
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
post: null,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
|
||||
notice: null,
|
||||
saving: false,
|
||||
|
||||
@computed("saving", "notice")
|
||||
disabled(saving, notice) {
|
||||
return saving || Ember.isEmpty(notice);
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
notice: "",
|
||||
saving: false
|
||||
});
|
||||
},
|
||||
|
||||
onClose() {
|
||||
const reject = this.get("reject");
|
||||
if (reject) {
|
||||
reject();
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
setNotice() {
|
||||
this.set("saving", true);
|
||||
|
||||
const post = this.get("post");
|
||||
const resolve = this.get("resolve");
|
||||
const reject = this.get("reject");
|
||||
const notice = this.get("notice");
|
||||
|
||||
// Let `updatePostField` handle state.
|
||||
this.setProperties({ resolve: null, reject: null });
|
||||
|
||||
post
|
||||
.updatePostField("notice", notice)
|
||||
.then(() => {
|
||||
post.setProperties({
|
||||
notice_type: "custom",
|
||||
notice_args: notice
|
||||
});
|
||||
resolve();
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch(() => {
|
||||
reject();
|
||||
this.send("closeModal");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,4 +1,5 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { isAppWebview, isiOSPWA } from "discourse/lib/utilities";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
showTop: true,
|
||||
@ -16,5 +17,10 @@ export default Ember.Controller.extend({
|
||||
@computed
|
||||
loginRequired() {
|
||||
return Discourse.SiteSettings.login_required && !Discourse.User.current();
|
||||
},
|
||||
|
||||
@computed
|
||||
showFooterNav() {
|
||||
return isAppWebview() || isiOSPWA();
|
||||
}
|
||||
});
|
||||
|
||||
@ -679,6 +679,13 @@ export default Ember.Controller.extend({
|
||||
.then(result => {
|
||||
if (result.responseJson.action === "enqueued") {
|
||||
this.send("postWasEnqueued", result.responseJson);
|
||||
if (result.responseJson.pending_post) {
|
||||
let pendingPosts = this.get("topicController.model.pending_posts");
|
||||
if (pendingPosts) {
|
||||
pendingPosts.pushObject(result.responseJson.pending_post);
|
||||
}
|
||||
}
|
||||
|
||||
this.destroyDraft();
|
||||
this.close();
|
||||
this.appEvents.trigger("post-stream:refresh");
|
||||
@ -719,14 +726,9 @@ export default Ember.Controller.extend({
|
||||
currentUser.set("reply_count", currentUser.get("reply_count") + 1);
|
||||
}
|
||||
|
||||
const disableJumpReply = Discourse.User.currentProp(
|
||||
"disable_jump_reply"
|
||||
);
|
||||
if (!composer.get("replyingToTopic") || !disableJumpReply) {
|
||||
const post = result.target;
|
||||
if (post && !staged) {
|
||||
DiscourseURL.routeTo(post.get("url"));
|
||||
}
|
||||
const post = result.target;
|
||||
if (post && !staged) {
|
||||
DiscourseURL.routeTo(post.get("url"));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
@ -6,19 +6,16 @@ export default Ember.Controller.extend({
|
||||
@computed("model.automatic")
|
||||
tabs(automatic) {
|
||||
const defaultTabs = [
|
||||
{ route: "group.manage.profile", title: "groups.manage.profile.title" },
|
||||
{
|
||||
route: "group.manage.interaction",
|
||||
title: "groups.manage.interaction.title"
|
||||
},
|
||||
|
||||
{ route: "group.manage.logs", title: "groups.manage.logs.title" }
|
||||
];
|
||||
|
||||
if (!automatic) {
|
||||
defaultTabs.splice(0, 0, {
|
||||
route: "group.manage.profile",
|
||||
title: "groups.manage.profile.title"
|
||||
});
|
||||
|
||||
defaultTabs.splice(1, 0, {
|
||||
route: "group.manage.membership",
|
||||
title: "groups.manage.membership.title"
|
||||
|
||||
@ -55,11 +55,15 @@ export default Ember.Controller.extend(
|
||||
return availableTitles.length > 0;
|
||||
},
|
||||
|
||||
@computed()
|
||||
canChangePassword() {
|
||||
return (
|
||||
!this.siteSettings.enable_sso && this.siteSettings.enable_local_logins
|
||||
);
|
||||
@computed("model.is_anonymous")
|
||||
canChangePassword(isAnonymous) {
|
||||
if (isAnonymous) {
|
||||
return false;
|
||||
} else {
|
||||
return (
|
||||
!this.siteSettings.enable_sso && this.siteSettings.enable_local_logins
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@computed("model.associated_accounts")
|
||||
@ -92,9 +96,17 @@ export default Ember.Controller.extend(
|
||||
return userId !== this.get("currentUser.id");
|
||||
},
|
||||
|
||||
@computed("model.second_factor_enabled", "canCheckEmails")
|
||||
canUpdateAssociatedAccounts(secondFactorEnabled, canCheckEmails) {
|
||||
if (secondFactorEnabled || !canCheckEmails) {
|
||||
@computed(
|
||||
"model.second_factor_enabled",
|
||||
"canCheckEmails",
|
||||
"model.is_anonymous"
|
||||
)
|
||||
canUpdateAssociatedAccounts(
|
||||
secondFactorEnabled,
|
||||
canCheckEmails,
|
||||
isAnonymous
|
||||
) {
|
||||
if (secondFactorEnabled || !canCheckEmails || isAnonymous) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ const USER_HOMES = {
|
||||
};
|
||||
|
||||
const TEXT_SIZES = ["smaller", "normal", "larger", "largest"];
|
||||
const TITLE_COUNT_MODES = ["notifications", "contextual"];
|
||||
|
||||
export default Ember.Controller.extend(PreferencesTabController, {
|
||||
@computed("makeThemeDefault")
|
||||
@ -30,12 +31,12 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
"external_links_in_new_tab",
|
||||
"dynamic_favicon",
|
||||
"enable_quoting",
|
||||
"disable_jump_reply",
|
||||
"automatically_unpin_topics",
|
||||
"allow_private_messages",
|
||||
"homepage_id",
|
||||
"hide_profile_and_presence",
|
||||
"text_size"
|
||||
"text_size",
|
||||
"title_count_mode"
|
||||
];
|
||||
|
||||
if (makeDefault) {
|
||||
@ -69,6 +70,13 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
});
|
||||
},
|
||||
|
||||
@computed
|
||||
titleCountModes() {
|
||||
return TITLE_COUNT_MODES.map(value => {
|
||||
return { name: I18n.t(`user.title_count_mode.${value}`), value };
|
||||
});
|
||||
},
|
||||
|
||||
userSelectableThemes: function() {
|
||||
return listThemes(this.site);
|
||||
}.property(),
|
||||
|
||||
@ -6,6 +6,8 @@ import User from "discourse/models/user";
|
||||
export default Ember.Controller.extend(PreferencesTabController, {
|
||||
saveAttrNames: ["muted_usernames", "ignored_usernames"],
|
||||
ignoredUsernames: Ember.computed.alias("model.ignored_usernames"),
|
||||
userIsMemberOrAbove: Ember.computed.gte("model.trust_level", 2),
|
||||
ignoredEnabled: Ember.computed.or("userIsMemberOrAbove", "model.staff"),
|
||||
actions: {
|
||||
ignoredUsernamesChanged(previous, current) {
|
||||
if (current.length > previous.length) {
|
||||
|
||||
@ -36,10 +36,35 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
|
||||
|
||||
moveDir(cat, dir) {
|
||||
const cats = this.get("categoriesOrdered");
|
||||
const curIdx = cats.indexOf(cat);
|
||||
const desiredIdx = curIdx + dir;
|
||||
const curIdx = cat.get("position");
|
||||
let desiredIdx = curIdx + dir;
|
||||
if (desiredIdx >= 0 && desiredIdx < cats.get("length")) {
|
||||
const otherCat = cats.objectAt(desiredIdx);
|
||||
let otherCat = cats.objectAt(desiredIdx);
|
||||
|
||||
// Respect children
|
||||
const parentIdx = otherCat.get("parent_category_id");
|
||||
if (parentIdx && parentIdx !== cat.get("parent_category_id")) {
|
||||
if (parentIdx === cat.get("id")) {
|
||||
// We want to move down
|
||||
for (let i = curIdx + 1; i < cats.get("length"); i++) {
|
||||
let tmpCat = cats.objectAt(i);
|
||||
if (!tmpCat.get("parent_category_id")) {
|
||||
desiredIdx = cats.indexOf(tmpCat);
|
||||
otherCat = tmpCat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We want to move up
|
||||
cats.forEach(function(tmpCat) {
|
||||
if (tmpCat.get("id") === parentIdx) {
|
||||
desiredIdx = cats.indexOf(tmpCat);
|
||||
otherCat = tmpCat;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
otherCat.set("position", curIdx);
|
||||
cat.set("position", desiredIdx);
|
||||
this.send("commit");
|
||||
@ -89,7 +114,7 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
|
||||
Math.max(position, 0),
|
||||
this.get("categoriesOrdered").length - 1
|
||||
);
|
||||
this.moveDir(cat, amount - this.get("categoriesOrdered").indexOf(cat));
|
||||
this.moveDir(cat, amount - cat.get("position"));
|
||||
},
|
||||
|
||||
moveUp(cat) {
|
||||
|
||||
@ -202,6 +202,14 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
},
|
||||
|
||||
actions: {
|
||||
deletePending(pending) {
|
||||
return ajax(`/review/${pending.id}`, { type: "DELETE" })
|
||||
.then(() => {
|
||||
this.get("model.pending_posts").removeObject(pending);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
showPostFlags(post) {
|
||||
return this.send("showFlags", post);
|
||||
},
|
||||
@ -742,6 +750,22 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
this.send("showGrantBadgeModal");
|
||||
},
|
||||
|
||||
addNotice(post) {
|
||||
return new Ember.RSVP.Promise(function(resolve, reject) {
|
||||
const controller = showModal("add-post-notice");
|
||||
controller.setProperties({ post, resolve, reject });
|
||||
});
|
||||
},
|
||||
|
||||
removeNotice(post) {
|
||||
return post.updatePostField("notice", null).then(() =>
|
||||
post.setProperties({
|
||||
notice_type: null,
|
||||
notice_args: null
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
toggleParticipant(user) {
|
||||
this.get("model.postStream")
|
||||
.toggleParticipant(user.get("username"))
|
||||
@ -1277,7 +1301,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
|
||||
if ($post.length === 0 || isElementInViewport($post)) return;
|
||||
|
||||
$("body").animate({ scrollTop: $post.offset().top }, 1000);
|
||||
$("html, body").animate({ scrollTop: $post.offset().top }, 1000);
|
||||
}, 500),
|
||||
|
||||
unsubscribe() {
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
/* global Tautologistics */
|
||||
export default function parseHTML(rawHtml) {
|
||||
const builder = new Tautologistics.NodeHtmlParser.HtmlBuilder();
|
||||
const parser = new Tautologistics.NodeHtmlParser.Parser(builder);
|
||||
|
||||
parser.parseComplete(rawHtml);
|
||||
return builder.dom;
|
||||
}
|
||||
@ -9,23 +9,39 @@ import {
|
||||
DELETED
|
||||
} from "discourse/models/reviewable";
|
||||
|
||||
export function htmlStatus(status) {
|
||||
function dataFor(status) {
|
||||
switch (status) {
|
||||
case PENDING:
|
||||
return I18n.t("review.statuses.pending.title");
|
||||
return { name: "pending" };
|
||||
case APPROVED:
|
||||
return `${iconHTML("check")} ${I18n.t("review.statuses.approved.title")}`;
|
||||
return { icon: "check", name: "approved" };
|
||||
case REJECTED:
|
||||
return `${iconHTML("times")} ${I18n.t("review.statuses.rejected.title")}`;
|
||||
return { icon: "times", name: "rejected" };
|
||||
case IGNORED:
|
||||
return `${iconHTML("external-link-alt")} ${I18n.t(
|
||||
"review.statuses.ignored.title"
|
||||
)}`;
|
||||
return { icon: "external-link-alt", name: "ignored" };
|
||||
case DELETED:
|
||||
return `${iconHTML("trash")} ${I18n.t("review.statuses.deleted.title")}`;
|
||||
return { icon: "trash", name: "deleted" };
|
||||
}
|
||||
}
|
||||
|
||||
export function htmlStatus(status) {
|
||||
let data = dataFor(status);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
let icon = data.icon ? iconHTML(data.icon) : "";
|
||||
|
||||
return `
|
||||
<span class='status'>
|
||||
<span class="${data.name}">
|
||||
${icon}
|
||||
${I18n.t("review.statuses." + data.name + ".title")}
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
export default htmlHelper(status => {
|
||||
return htmlStatus(status);
|
||||
});
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import { applyFlaggedProperties } from "discourse/components/site-header";
|
||||
|
||||
export default {
|
||||
name: "apply-flagged-properties",
|
||||
initialize: applyFlaggedProperties
|
||||
};
|
||||
@ -5,7 +5,7 @@ export default {
|
||||
after: "message-bus",
|
||||
|
||||
initialize(container) {
|
||||
const banner = Ember.Object.create(PreloadStore.get("banner")),
|
||||
const banner = Ember.Object.create(PreloadStore.get("banner") || {}),
|
||||
site = container.lookup("site:main");
|
||||
|
||||
site.set("banner", banner);
|
||||
@ -16,7 +16,7 @@ export default {
|
||||
}
|
||||
|
||||
messageBus.subscribe("/site/banner", function(ban) {
|
||||
site.set("banner", Ember.Object.create(ban));
|
||||
site.set("banner", Ember.Object.create(ban || {}));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
// Prevents auto-zoom in Safari iOS inputs with font-size < 16px
|
||||
const originalMeta = $("meta[name=viewport]").attr("content");
|
||||
|
||||
export default {
|
||||
name: "ios-input-no-zoom",
|
||||
|
||||
initialize() {
|
||||
let iOS =
|
||||
!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
|
||||
|
||||
if (iOS) {
|
||||
$("body").on("touchstart", "input", () => {
|
||||
$("meta[name=viewport]").attr(
|
||||
"content",
|
||||
"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
);
|
||||
});
|
||||
|
||||
$("body").on("focusout", "input", e => {
|
||||
if (e.relatedTarget === null) {
|
||||
$("meta[name=viewport]").attr("content", originalMeta);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import Mobile from "discourse/lib/mobile";
|
||||
import { setResolverOption } from "discourse-common/resolver";
|
||||
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
|
||||
|
||||
// Initializes the `Mobile` helper object.
|
||||
export default {
|
||||
@ -14,5 +15,14 @@ export default {
|
||||
site.set("isMobileDevice", Mobile.isMobileDevice);
|
||||
|
||||
setResolverOption("mobileView", Mobile.mobileView);
|
||||
|
||||
if (isAppWebview()) {
|
||||
Ember.run.later(() => {
|
||||
postRNWebviewMessage(
|
||||
"headerBg",
|
||||
$(".d-header").css("background-color")
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -22,7 +22,6 @@ export default {
|
||||
if (keyValueStore.get("anon-cta-never")) return; // "never show again"
|
||||
if (!siteSettings.allow_new_registrations) return;
|
||||
if (siteSettings.invite_only) return;
|
||||
if (siteSettings.must_approve_users) return;
|
||||
if (siteSettings.login_required) return;
|
||||
if (!siteSettings.enable_signup_cta) return;
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { nativeShare } from "discourse/lib/pwa-utils";
|
||||
import { registerTopicFooterButton } from "discourse/lib/register-topic-footer-button";
|
||||
|
||||
export default {
|
||||
@ -13,44 +12,40 @@ export default {
|
||||
label: "topic.share.title",
|
||||
title: "topic.share.help",
|
||||
action() {
|
||||
const modal = () => {
|
||||
const panels = [
|
||||
{
|
||||
id: "share",
|
||||
title: "topic.share.extended_title",
|
||||
model: {
|
||||
topic: this.get("topic")
|
||||
}
|
||||
const panels = [
|
||||
{
|
||||
id: "share",
|
||||
title: "topic.share.extended_title",
|
||||
model: {
|
||||
topic: this.get("topic")
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
if (this.get("canInviteTo") && !this.get("inviteDisabled")) {
|
||||
let invitePanelTitle;
|
||||
if (this.get("canInviteTo") && !this.get("inviteDisabled")) {
|
||||
let invitePanelTitle;
|
||||
|
||||
if (this.get("isPM")) {
|
||||
invitePanelTitle = "topic.invite_private.title";
|
||||
} else if (this.get("invitingToTopic")) {
|
||||
invitePanelTitle = "topic.invite_reply.title";
|
||||
} else {
|
||||
invitePanelTitle = "user.invited.create";
|
||||
}
|
||||
|
||||
panels.push({
|
||||
id: "invite",
|
||||
title: invitePanelTitle,
|
||||
model: {
|
||||
inviteModel: this.get("topic")
|
||||
}
|
||||
});
|
||||
if (this.get("isPM")) {
|
||||
invitePanelTitle = "topic.invite_private.title";
|
||||
} else if (this.get("invitingToTopic")) {
|
||||
invitePanelTitle = "topic.invite_reply.title";
|
||||
} else {
|
||||
invitePanelTitle = "user.invited.create";
|
||||
}
|
||||
|
||||
showModal("share-and-invite", {
|
||||
modalClass: "share-and-invite",
|
||||
panels
|
||||
panels.push({
|
||||
id: "invite",
|
||||
title: invitePanelTitle,
|
||||
model: {
|
||||
inviteModel: this.get("topic")
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
nativeShare({ url: this.get("topic.shareUrl") }).then(null, modal);
|
||||
showModal("share-and-invite", {
|
||||
modalClass: "share-and-invite",
|
||||
panels
|
||||
});
|
||||
},
|
||||
dropdown() {
|
||||
return this.site.mobileView;
|
||||
|
||||
@ -4,9 +4,32 @@ import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { selectedText } from "discourse/lib/utilities";
|
||||
|
||||
export function isValidLink($link) {
|
||||
// Do not track:
|
||||
// - lightboxes
|
||||
// - group mentions
|
||||
// - links with disabled tracking
|
||||
// - category links
|
||||
// - quote back button
|
||||
if (
|
||||
$link.is(
|
||||
".lightbox, .mention, .mention-group, .no-track-link, .hashtag, .back"
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not track links in quotes or in elided part
|
||||
if ($link.parents("aside.quote, .elided").length !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($link.parents(".expanded-embed").length !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
$link.hasClass("track-link") ||
|
||||
$link.closest(".hashtag,.badge-category,.onebox-result,.onebox-body")
|
||||
$link.closest(".hashtag, .badge-category, .onebox-result, .onebox-body")
|
||||
.length === 0
|
||||
);
|
||||
}
|
||||
@ -18,34 +41,34 @@ export default {
|
||||
return true;
|
||||
}
|
||||
|
||||
// cancel click if triggered as part of selection.
|
||||
if (selectedText() !== "") {
|
||||
return false;
|
||||
// Cancel click if triggered as part of selection.
|
||||
const selection = window.getSelection();
|
||||
if (selection.type === "Range" || selection.rangeCount > 0) {
|
||||
if (selectedText() !== "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const $link = $(e.currentTarget);
|
||||
|
||||
// don't track
|
||||
// - lightboxes
|
||||
// - group mentions
|
||||
// - links with disabled tracking
|
||||
// - category links
|
||||
// - quote back button
|
||||
if (
|
||||
$link.is(".lightbox, .mention-group, .no-track-link, .hashtag, .back")
|
||||
) {
|
||||
if (!isValidLink($link)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// don't track links in quotes or in elided part
|
||||
let tracking = $link.parents("aside.quote, .elided").length === 0;
|
||||
if ($link.hasClass("attachment")) {
|
||||
// Warn the user if they cannot download the file.
|
||||
if (
|
||||
Discourse.SiteSettings.prevent_anons_from_downloading_files &&
|
||||
!Discourse.User.current()
|
||||
) {
|
||||
bootbox.alert(I18n.t("post.errors.attachment_download_requires_login"));
|
||||
return false;
|
||||
}
|
||||
|
||||
let href = $link.attr("href") || $link.data("href");
|
||||
|
||||
if (!href || href.trim().length === 0) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
if (href.indexOf("mailto:") === 0) {
|
||||
|
||||
let href = ($link.attr("href") || $link.data("href") || "").trim();
|
||||
if (!href || href.indexOf("mailto:") === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -57,119 +80,64 @@ export default {
|
||||
const userId = $link.data("user-id") || $article.data("user-id");
|
||||
const ownLink = userId && userId === Discourse.User.currentProp("id");
|
||||
|
||||
let destUrl = href;
|
||||
|
||||
if (tracking) {
|
||||
destUrl = Discourse.getURL(
|
||||
"/clicks/track?url=" + encodeURIComponent(href)
|
||||
);
|
||||
|
||||
if (postId && !$link.data("ignore-post-id")) {
|
||||
destUrl += "&post_id=" + encodeURI(postId);
|
||||
}
|
||||
if (topicId) {
|
||||
destUrl += "&topic_id=" + encodeURI(topicId);
|
||||
}
|
||||
|
||||
// Update badge clicks unless it's our own
|
||||
if (!ownLink) {
|
||||
const $badge = $("span.badge", $link);
|
||||
if ($badge.length === 1) {
|
||||
// don't update counts in category badge nor in oneboxes (except when we force it)
|
||||
if (isValidLink($link)) {
|
||||
const html = $badge.html();
|
||||
const key = `${new Date().toLocaleDateString()}-${postId}-${href}`;
|
||||
if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) {
|
||||
sessionStorage.setItem(key, true);
|
||||
$badge.html(parseInt(html, 10) + 1);
|
||||
}
|
||||
}
|
||||
// Update badge clicks unless it's our own.
|
||||
if (!ownLink) {
|
||||
const $badge = $("span.badge", $link);
|
||||
if ($badge.length === 1) {
|
||||
const html = $badge.html();
|
||||
const key = `${new Date().toLocaleDateString()}-${postId}-${href}`;
|
||||
if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) {
|
||||
sessionStorage.setItem(key, true);
|
||||
$badge.html(parseInt(html, 10) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if they want to open in a new tab, do an AJAX request
|
||||
if (tracking && wantsNewWindow(e)) {
|
||||
ajax("/clicks/track", {
|
||||
data: {
|
||||
url: href,
|
||||
post_id: postId,
|
||||
topic_id: topicId,
|
||||
redirect: false
|
||||
},
|
||||
dataType: "html"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Remove the href, put it as a data attribute
|
||||
if (!$link.data("href")) {
|
||||
$link.addClass("no-href");
|
||||
$link.data("href", $link.attr("href"));
|
||||
$link.attr("href", null);
|
||||
// Don't route to this URL
|
||||
$link.data("auto-route", true);
|
||||
}
|
||||
|
||||
// restore href
|
||||
Ember.run.later(() => {
|
||||
$link.removeClass("no-href");
|
||||
$link.attr("href", $link.data("href"));
|
||||
$link.data("href", null);
|
||||
}, 50);
|
||||
|
||||
// warn the user if they can't download the file
|
||||
if (
|
||||
Discourse.SiteSettings.prevent_anons_from_downloading_files &&
|
||||
$link.hasClass("attachment") &&
|
||||
!Discourse.User.current()
|
||||
) {
|
||||
bootbox.alert(I18n.t("post.errors.attachment_download_requires_login"));
|
||||
return false;
|
||||
}
|
||||
const trackPromise = ajax("/clicks/track", {
|
||||
data: {
|
||||
url: href,
|
||||
post_id: postId,
|
||||
topic_id: topicId
|
||||
}
|
||||
});
|
||||
|
||||
const isInternal = DiscourseURL.isInternal(href);
|
||||
|
||||
const modifierLeftClicked = (e.ctrlKey || e.metaKey) && e.which === 1;
|
||||
const middleClicked = e.which === 2;
|
||||
const openExternalInNewTab = Discourse.User.currentProp(
|
||||
"external_links_in_new_tab"
|
||||
);
|
||||
|
||||
const openWindow =
|
||||
modifierLeftClicked ||
|
||||
middleClicked ||
|
||||
(!isInternal && openExternalInNewTab);
|
||||
if (!wantsNewWindow(e)) {
|
||||
if (!isInternal && openExternalInNewTab) {
|
||||
window.open(href, "_blank").focus();
|
||||
|
||||
// If we're on the same site, use the router and track via AJAX
|
||||
if (isInternal && !$link.hasClass("attachment")) {
|
||||
if (tracking) {
|
||||
ajax("/clicks/track", {
|
||||
data: {
|
||||
url: href,
|
||||
post_id: postId,
|
||||
topic_id: topicId,
|
||||
redirect: false
|
||||
},
|
||||
dataType: "html"
|
||||
// Hack to prevent changing current window.location.
|
||||
// e.preventDefault() does not work.
|
||||
if (!$link.data("href")) {
|
||||
$link.addClass("no-href");
|
||||
$link.data("href", $link.attr("href"));
|
||||
$link.attr("href", null);
|
||||
$link.data("auto-route", true);
|
||||
|
||||
Ember.run.later(() => {
|
||||
$link.removeClass("no-href");
|
||||
$link.attr("href", $link.data("href"));
|
||||
$link.data("href", null);
|
||||
$link.data("auto-route", null);
|
||||
}, 50);
|
||||
}
|
||||
} else {
|
||||
trackPromise.finally(() => {
|
||||
if (isInternal) {
|
||||
DiscourseURL.routeTo(href);
|
||||
} else {
|
||||
DiscourseURL.redirectTo(href);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (openWindow) {
|
||||
window.open(destUrl, "_blank").focus();
|
||||
} else {
|
||||
DiscourseURL.routeTo(href);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (openWindow) {
|
||||
window.open(destUrl, "_blank").focus();
|
||||
} else {
|
||||
DiscourseURL.redirectTo(destUrl);
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -445,6 +445,9 @@ export default {
|
||||
$selected = $articles
|
||||
.toArray()
|
||||
.find(article => article.getBoundingClientRect().top > offset);
|
||||
if (!$selected) {
|
||||
$selected = $articles[$articles.length - 1];
|
||||
}
|
||||
direction = 0;
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { renderIcon } from "discourse-common/lib/icon-library";
|
||||
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
|
||||
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
||||
|
||||
export default function($elem) {
|
||||
if (!$elem) {
|
||||
return;
|
||||
}
|
||||
const originalMeta = $("meta[name=viewport]").attr("content");
|
||||
|
||||
loadScript("/javascripts/jquery.magnific-popup.min.js").then(function() {
|
||||
const spoilers = $elem.find(".spoiler a.lightbox, .spoiled a.lightbox");
|
||||
|
||||
$elem
|
||||
.find("a.lightbox")
|
||||
.not(spoilers)
|
||||
@ -17,17 +20,22 @@ export default function($elem) {
|
||||
closeOnContentClick: false,
|
||||
removalDelay: 300,
|
||||
mainClass: "mfp-zoom-in",
|
||||
tClose: I18n.t("lightbox.close"),
|
||||
tLoading: spinnerHTML,
|
||||
|
||||
gallery: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
tPrev: I18n.t("lightbox.previous"),
|
||||
tNext: I18n.t("lightbox.next"),
|
||||
tCounter: I18n.t("lightbox.counter")
|
||||
},
|
||||
|
||||
ajax: {
|
||||
tError: I18n.t("lightbox.content_load_error")
|
||||
},
|
||||
|
||||
callbacks: {
|
||||
open() {
|
||||
$("meta[name=viewport]").attr(
|
||||
"content",
|
||||
"width=device-width, initial-scale=1.0"
|
||||
);
|
||||
const wrap = this.wrap,
|
||||
img = this.currItem.img,
|
||||
maxHeight = img.css("max-height");
|
||||
@ -39,15 +47,28 @@ export default function($elem) {
|
||||
wrap.hasClass("mfp-force-scrollbars") ? "none" : maxHeight
|
||||
);
|
||||
});
|
||||
|
||||
if (isAppWebview()) {
|
||||
postRNWebviewMessage(
|
||||
"headerBg",
|
||||
$(".mfp-bg").css("background-color")
|
||||
);
|
||||
}
|
||||
},
|
||||
beforeClose() {
|
||||
$("meta[name=viewport]").attr("content", originalMeta);
|
||||
this.wrap.off("click.pinhandler");
|
||||
this.wrap.removeClass("mfp-force-scrollbars");
|
||||
if (isAppWebview()) {
|
||||
postRNWebviewMessage(
|
||||
"headerBg",
|
||||
$(".d-header").css("background-color")
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
image: {
|
||||
tError: I18n.t("lightbox.image_load_error"),
|
||||
titleSrc(item) {
|
||||
const href = item.el.data("download-href") || item.src;
|
||||
let src = [
|
||||
|
||||
@ -36,18 +36,6 @@ const Mobile = {
|
||||
// localStorage may be disabled, just skip this
|
||||
// you get security errors if it is disabled
|
||||
}
|
||||
|
||||
// Sam: I tried this to disable zooming on iOS 10 but it is not consistent
|
||||
// you can still sometimes trigger zoom and be stuck in a horrible state
|
||||
//
|
||||
// let iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
|
||||
// if (iOS) {
|
||||
// document.documentElement.addEventListener('touchstart', function (event) {
|
||||
// if (event.touches.length > 1) {
|
||||
// event.preventDefault();
|
||||
// }
|
||||
// }, false);
|
||||
// }
|
||||
},
|
||||
|
||||
toggleMobileView() {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { addDecorator } from "discourse/widgets/post-cooked";
|
||||
import ComposerEditor from "discourse/components/composer-editor";
|
||||
@ -15,7 +16,6 @@ import {
|
||||
} from "discourse/widgets/widget";
|
||||
import { preventCloak } from "discourse/widgets/post-stream";
|
||||
import { h } from "virtual-dom";
|
||||
import { addFlagProperty } from "discourse/components/site-header";
|
||||
import { addPopupMenuOptionsCallback } from "discourse/controllers/composer";
|
||||
import { extraConnectorClass } from "discourse/lib/plugin-connectors";
|
||||
import { addPostSmallActionIcon } from "discourse/widgets/post-small-action";
|
||||
@ -536,11 +536,10 @@ class PluginApi {
|
||||
return reopenWidget(name, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a property that can be summed for calculating the flag counter
|
||||
**/
|
||||
addFlagProperty(property) {
|
||||
return addFlagProperty(property);
|
||||
addFlagProperty() {
|
||||
deprecated(
|
||||
"addFlagProperty has been removed. Use the reviewable API instead."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -8,7 +8,7 @@ export function nativeShare(data) {
|
||||
.share(data)
|
||||
.then(resolve)
|
||||
.catch(e => {
|
||||
if (e.message === "Share canceled") {
|
||||
if (e.name === "AbortError") {
|
||||
// closing share panel do nothing
|
||||
} else {
|
||||
reject();
|
||||
|
||||
@ -54,8 +54,9 @@ export default class {
|
||||
}
|
||||
}
|
||||
|
||||
setOnscreen(onscreen) {
|
||||
setOnscreen(onscreen, readOnscreen) {
|
||||
this._onscreen = onscreen;
|
||||
this._readOnscreen = readOnscreen;
|
||||
}
|
||||
|
||||
// Reset our timers
|
||||
@ -68,6 +69,8 @@ export default class {
|
||||
this._totalTimings = {};
|
||||
this._topicTime = 0;
|
||||
this._onscreen = [];
|
||||
this._readOnscreen = [];
|
||||
this._readPosts = {};
|
||||
this._inProgress = false;
|
||||
}
|
||||
|
||||
@ -112,6 +115,7 @@ export default class {
|
||||
if (!$.isEmptyObject(newTimings)) {
|
||||
if (this.currentUser) {
|
||||
this._inProgress = true;
|
||||
|
||||
ajax("/topics/timings", {
|
||||
data: {
|
||||
timings: newTimings,
|
||||
@ -198,20 +202,27 @@ export default class {
|
||||
const nextFlush = this.siteSettings.flush_timings_secs * 1000;
|
||||
|
||||
const rush = Object.keys(timings).some(postNumber => {
|
||||
return timings[postNumber] > 0 && !totalTimings[postNumber];
|
||||
return (
|
||||
timings[postNumber] > 0 &&
|
||||
!totalTimings[postNumber] &&
|
||||
!this._readPosts[postNumber]
|
||||
);
|
||||
});
|
||||
|
||||
if (!this._inProgress && (this._lastFlush > nextFlush || rush)) {
|
||||
this.flush();
|
||||
}
|
||||
|
||||
// Don't track timings if we're not in focus
|
||||
if (!Discourse.get("hasFocus")) return;
|
||||
if (Discourse.get("hasFocus")) {
|
||||
this._topicTime += diff;
|
||||
|
||||
this._topicTime += diff;
|
||||
this._onscreen.forEach(
|
||||
postNumber => (timings[postNumber] = (timings[postNumber] || 0) + diff)
|
||||
);
|
||||
|
||||
this._onscreen.forEach(
|
||||
postNumber => (timings[postNumber] = (timings[postNumber] || 0) + diff)
|
||||
);
|
||||
this._readOnscreen.forEach(postNumber => {
|
||||
this._readPosts[postNumber] = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import parseHTML from "discourse/helpers/parse-html";
|
||||
|
||||
const trimLeft = text => text.replace(/^\s+/, "");
|
||||
const trimRight = text => text.replace(/\s+$/, "");
|
||||
const countPipes = text => (text.replace(/\\\|/, "").match(/\|/g) || []).length;
|
||||
@ -495,10 +493,9 @@ function tags() {
|
||||
class Element {
|
||||
constructor(element, parent, previous, next) {
|
||||
this.name = element.name;
|
||||
this.type = element.type;
|
||||
this.data = element.data;
|
||||
this.children = element.children;
|
||||
this.attributes = element.attributes || {};
|
||||
this.attributes = element.attributes;
|
||||
|
||||
if (parent) {
|
||||
this.parent = parent;
|
||||
@ -554,14 +551,7 @@ class Element {
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
switch (this.type) {
|
||||
case "text":
|
||||
return this.text();
|
||||
break;
|
||||
case "tag":
|
||||
return this.tag().toMarkdown();
|
||||
break;
|
||||
}
|
||||
return this.name === "#text" ? this.text() : this.tag().toMarkdown();
|
||||
}
|
||||
|
||||
filterParentNames(names) {
|
||||
@ -628,7 +618,42 @@ function putPlaceholders(html) {
|
||||
match = codeRegEx.exec(origHtml);
|
||||
}
|
||||
|
||||
const elements = parseHTML(trimUnwanted(html));
|
||||
const transformNode = node => {
|
||||
if (node.nodeName !== "#text" && node.length !== undefined) {
|
||||
const ret = [];
|
||||
for (let i = 0; i < node.length; ++i) {
|
||||
if (node[i].nodeName !== "#comment") {
|
||||
ret.push(transformNode(node[i]));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const ret = {
|
||||
name: node.nodeName.toLowerCase(),
|
||||
data: node.data,
|
||||
children: [],
|
||||
attributes: {}
|
||||
};
|
||||
|
||||
if (node.nodeName === "#text") {
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.childNodes.length; ++i) {
|
||||
if (node.childNodes[i].nodeName !== "#comment") {
|
||||
ret.children.push(transformNode(node.childNodes[i]));
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.attributes.length; ++i) {
|
||||
ret.attributes[node.attributes[i].name] = node.attributes[i].value;
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
const elements = transformNode($.parseHTML(trimUnwanted(html)));
|
||||
return { elements, placeholders };
|
||||
}
|
||||
|
||||
|
||||
@ -133,10 +133,12 @@ export default function transformPost(
|
||||
postAtts.topicUrl = topic.get("url");
|
||||
postAtts.isSaving = post.isSaving;
|
||||
|
||||
if (post.post_notice_type) {
|
||||
postAtts.postNoticeType = post.post_notice_type;
|
||||
if (postAtts.postNoticeType === "returning") {
|
||||
postAtts.postNoticeTime = new Date(post.post_notice_time);
|
||||
if (post.notice_type) {
|
||||
postAtts.noticeType = post.notice_type;
|
||||
if (postAtts.noticeType === "custom") {
|
||||
postAtts.noticeMessage = post.notice_args;
|
||||
} else if (postAtts.noticeType === "returning_user") {
|
||||
postAtts.noticeTime = new Date(post.notice_args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -643,5 +643,22 @@ export function areCookiesEnabled() {
|
||||
}
|
||||
}
|
||||
|
||||
export function isiOSPWA() {
|
||||
return (
|
||||
window.matchMedia("(display-mode: standalone)").matches &&
|
||||
navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
|
||||
);
|
||||
}
|
||||
|
||||
export function isAppWebview() {
|
||||
return window.ReactNativeWebView !== undefined;
|
||||
}
|
||||
|
||||
export function postRNWebviewMessage(prop, value) {
|
||||
if (window.ReactNativeWebView !== undefined) {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({ [prop]: value }));
|
||||
}
|
||||
}
|
||||
|
||||
// This prevents a mini racer crash
|
||||
export default {};
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
// Small buffer so that very tiny scrolls don't trigger mobile header switch
|
||||
const MOBILE_SCROLL_TOLERANCE = 5;
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
_lastScroll: null,
|
||||
_bottomHit: 0,
|
||||
|
||||
calculateDirection(offset) {
|
||||
// Difference between this scroll and the one before it.
|
||||
const delta = Math.floor(offset - this._lastScroll);
|
||||
|
||||
// This is a tiny scroll, so we ignore it.
|
||||
if (delta <= MOBILE_SCROLL_TOLERANCE && delta >= -MOBILE_SCROLL_TOLERANCE)
|
||||
return;
|
||||
|
||||
// don't calculate when resetting offset (i.e. going to /latest or to next topic in suggested list)
|
||||
if (offset === 0) return;
|
||||
|
||||
const prevDirection = this.mobileScrollDirection;
|
||||
const currDirection = delta > 0 ? "down" : null;
|
||||
|
||||
const distanceToBottom = Math.floor(
|
||||
$("body").height() - offset - $(window).height()
|
||||
);
|
||||
|
||||
// Handle Safari top overscroll first
|
||||
if (offset < 0) {
|
||||
this.set("mobileScrollDirection", null);
|
||||
} else if (currDirection !== prevDirection && distanceToBottom > 0) {
|
||||
this.set("mobileScrollDirection", currDirection);
|
||||
}
|
||||
|
||||
// We store this to compare against it the next time the user scrolls
|
||||
this._lastScroll = Math.floor(offset);
|
||||
|
||||
// Not at the bottom yet
|
||||
if (distanceToBottom > 0) {
|
||||
this._bottomHit = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user reaches the very bottom of the topic, we only want to reset
|
||||
// this scroll direction after a second scrolldown. This is a nicer event
|
||||
// similar to what Safari and Chrome do.
|
||||
Ember.run.debounce(() => {
|
||||
this._bottomHit = 1;
|
||||
}, 1000);
|
||||
|
||||
if (this._bottomHit === 1) {
|
||||
this.set("mobileScrollDirection", null);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -74,6 +74,8 @@ const Composer = RestModel.extend({
|
||||
_categoryId: null,
|
||||
unlistTopic: false,
|
||||
noBump: false,
|
||||
draftSaving: false,
|
||||
draftSaved: false,
|
||||
|
||||
archetypes: function() {
|
||||
return this.site.get("archetypes");
|
||||
@ -971,7 +973,8 @@ const Composer = RestModel.extend({
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
draftStatus: I18n.t("composer.saving_draft_tip"),
|
||||
draftSaved: false,
|
||||
draftSaving: true,
|
||||
draftConflictUser: null
|
||||
});
|
||||
|
||||
@ -1004,18 +1007,21 @@ const Composer = RestModel.extend({
|
||||
.then(result => {
|
||||
if (result.conflict_user) {
|
||||
this.setProperties({
|
||||
draftSaving: false,
|
||||
draftStatus: I18n.t("composer.edit_conflict"),
|
||||
draftConflictUser: result.conflict_user
|
||||
});
|
||||
} else {
|
||||
this.setProperties({
|
||||
draftStatus: I18n.t("composer.saved_draft_tip"),
|
||||
draftSaving: false,
|
||||
draftSaved: true,
|
||||
draftConflictUser: null
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.setProperties({
|
||||
draftSaving: false,
|
||||
draftStatus: I18n.t("composer.drafts_offline"),
|
||||
draftConflictUser: null
|
||||
});
|
||||
@ -1033,6 +1039,8 @@ const Composer = RestModel.extend({
|
||||
self.set("draftStatus", null);
|
||||
self.set("draftConflictUser", null);
|
||||
self._clearingStatus = null;
|
||||
self.set("draftSaving", false);
|
||||
self.set("draftSaved", false);
|
||||
},
|
||||
Ember.Test ? 0 : 1000
|
||||
);
|
||||
|
||||
@ -10,11 +10,6 @@ export const IGNORED = 3;
|
||||
export const DELETED = 4;
|
||||
|
||||
export default RestModel.extend({
|
||||
pending: Ember.computed.equal("status", PENDING),
|
||||
approved: Ember.computed.equal("status", APPROVED),
|
||||
rejected: Ember.computed.equal("status", REJECTED),
|
||||
ignored: Ember.computed.equal("status", IGNORED),
|
||||
|
||||
@computed("type")
|
||||
humanType(type) {
|
||||
return I18n.t(`review.types.${type.underscore()}.title`, {
|
||||
|
||||
@ -274,7 +274,6 @@ const User = RestModel.extend({
|
||||
"email_previous_replies",
|
||||
"dynamic_favicon",
|
||||
"enable_quoting",
|
||||
"disable_jump_reply",
|
||||
"automatically_unpin_topics",
|
||||
"digest_after_minutes",
|
||||
"new_topic_duration_minutes",
|
||||
@ -286,7 +285,8 @@ const User = RestModel.extend({
|
||||
"allow_private_messages",
|
||||
"homepage_id",
|
||||
"hide_profile_and_presence",
|
||||
"text_size"
|
||||
"text_size",
|
||||
"title_count_mode"
|
||||
];
|
||||
|
||||
if (fields) {
|
||||
|
||||
@ -3,11 +3,5 @@ export default Discourse.Route.extend({
|
||||
|
||||
titleToken() {
|
||||
return I18n.t("groups.manage.profile.title");
|
||||
},
|
||||
|
||||
afterModel(group) {
|
||||
if (group.get("automatic")) {
|
||||
this.replaceWith("group.manage.interaction", group);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -33,3 +33,7 @@
|
||||
{{outlet "modal"}}
|
||||
{{topic-entrance}}
|
||||
{{outlet "composer"}}
|
||||
|
||||
{{#if showFooterNav}}
|
||||
{{footer-nav}}
|
||||
{{/if}}
|
||||
|
||||
@ -1,209 +1,225 @@
|
||||
{{#if showPositionInput}}
|
||||
<section class='field position-fields'>
|
||||
<label for="category-position">
|
||||
{{i18n 'category.position'}}
|
||||
</label>
|
||||
{{text-field value=category.position id="category-position" class="position-input" type="number"}}
|
||||
<section>
|
||||
<h3>{{i18n 'category.settings_sections.general'}}</h3>
|
||||
|
||||
</section>
|
||||
{{/if}}
|
||||
{{#if showPositionInput}}
|
||||
<section class='field position-fields'>
|
||||
<label for="category-position">
|
||||
{{i18n 'category.position'}}
|
||||
</label>
|
||||
{{text-field value=category.position id="category-position" class="position-input" type="number"}}
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{#unless showPositionInput}}
|
||||
<section class='field'>
|
||||
{{i18n 'category.position_disabled'}}
|
||||
<a href="{{get-url '/admin/site_settings/category/basic'}}">{{i18n 'category.position_disabled_click'}}</a>
|
||||
</section>
|
||||
{{/unless}}
|
||||
|
||||
<section class='field'>
|
||||
<div class="control-group">
|
||||
<label for="topic-auto-close">
|
||||
{{i18n 'topic.auto_close.label'}}
|
||||
</label>
|
||||
{{text-field value=category.auto_close_hours id="topic-auto-close" type="number"}}
|
||||
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.auto_close_based_on_last_post}}
|
||||
{{i18n 'topic.auto_close.based_on_last_post'}}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.allow_badges}}
|
||||
{{i18n 'category.allow_badges_label'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.suppress_from_latest}}
|
||||
{{i18n "category.suppress_from_latest"}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label for="category-search-priority">
|
||||
{{i18n "category.search_priority.label"}}
|
||||
</label>
|
||||
|
||||
{{combo-box valueAttribute="value"
|
||||
id="category-search-priority"
|
||||
content=searchPrioritiesOptions
|
||||
value=category.search_priority}}
|
||||
</section>
|
||||
|
||||
{{#if isParentCategory}}
|
||||
<section class="field show-subcategory-list-field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.show_subcategory_list}}
|
||||
{{i18n "category.show_subcategory_list"}}
|
||||
</label>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{#if showSubcategoryListStyle}}
|
||||
<section class="field subcategory-list-style-field">
|
||||
<label for="subcategory-list-style">
|
||||
{{i18n "category.subcategory_list_style"}}
|
||||
</label>
|
||||
{{combo-box valueAttribute="value" id="subcategory-list-style" content=availableSubcategoryListStyles value=category.subcategory_list_style}}
|
||||
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class="field default-view-field">
|
||||
<label for="category-default-view">
|
||||
{{i18n "category.default_view"}}
|
||||
</label>
|
||||
<div class="controls">
|
||||
{{combo-box valueAttribute="value" id="category-default-view" content=availableViews value=category.default_view}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="field default-top-period-field">
|
||||
<label for="category-default-period">
|
||||
{{i18n "category.default_top_period"}}
|
||||
</label>
|
||||
<div class="controls">
|
||||
{{combo-box valueAttribute="value" id="category-default-period" content=availableTopPeriods value=category.default_top_period}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label for="category-sort-order">
|
||||
{{i18n "category.sort_order"}}
|
||||
</label>
|
||||
<div class="controls">
|
||||
{{combo-box valueAttribute="value" content=availableSorts value=category.sort_order none="category.sort_options.default"}}
|
||||
{{#unless isDefaultSortOrder}}
|
||||
{{combo-box castBoolean=true valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}}
|
||||
{{#unless showPositionInput}}
|
||||
<section class='field'>
|
||||
{{i18n 'category.position_disabled'}}
|
||||
<a href="{{get-url '/admin/site_settings/category/basic'}}">{{i18n 'category.position_disabled_click'}}</a>
|
||||
</section>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label for="category-number-featured-topics">
|
||||
{{#if category.parent_category_id}}
|
||||
{{i18n "category.subcategory_num_featured_topics"}}
|
||||
{{else}}
|
||||
{{i18n "category.num_featured_topics"}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{text-field value=category.num_featured_topics id="category-number-featured-topics" type="number"}}
|
||||
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label for="category-number-daily-bump">
|
||||
{{i18n "category.num_auto_bump_daily"}}
|
||||
</label>
|
||||
{{text-field value=category.custom_fields.num_auto_bump_daily id="category-number-daily-bump" type="number"}}
|
||||
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.all_topics_wiki}}
|
||||
{{i18n "category.all_topics_wiki"}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.navigate_to_first_post_after_read}}
|
||||
{{i18n "category.navigate_to_first_post_after_read"}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{{#if siteSettings.topic_featured_link_enabled}}
|
||||
<section class='field'>
|
||||
<div class="allowed-topic-featured-link-category">
|
||||
<label class="checkbox-label">
|
||||
{{input type="checkbox" checked=category.topic_featured_link_allowed}}
|
||||
{{i18n 'category.topic_featured_link_allowed'}}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{#if emailInEnabled}}
|
||||
<section class='field'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.email_in_allow_strangers}}
|
||||
{{i18n 'category.email_in_allow_strangers'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
<label for="category-email-in">
|
||||
{{d-icon "far-envelope"}}
|
||||
{{i18n 'category.email_in'}}
|
||||
</label>
|
||||
{{text-field id="category-email-in" class="email-in" value=category.email_in}}
|
||||
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.mailinglist_mirror}}
|
||||
{{i18n 'category.mailinglist_mirror'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{{plugin-outlet name="category-email-in" args=(hash category=category)}}
|
||||
{{/if}}
|
||||
|
||||
{{#if siteSettings.tagging_enabled}}
|
||||
<section class='field minimum-required-tags'>
|
||||
<label for="category-minimum-tags">
|
||||
{{i18n 'category.minimum_required_tags'}}
|
||||
<section class="field">
|
||||
<label for="category-number-featured-topics">
|
||||
{{#if category.parent_category_id}}
|
||||
{{i18n "category.subcategory_num_featured_topics"}}
|
||||
{{else}}
|
||||
{{i18n "category.num_featured_topics"}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{text-field value=category.minimum_required_tags id="category-minimum-tags" type="number" min="0"}}
|
||||
|
||||
{{text-field value=category.num_featured_topics id="category-number-featured-topics" type="number"}}
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.custom_fields.require_topic_approval}}
|
||||
{{i18n 'category.require_topic_approval'}}
|
||||
</label>
|
||||
</section>
|
||||
<section class="field">
|
||||
<label for="category-search-priority">
|
||||
{{i18n "category.search_priority.label"}}
|
||||
</label>
|
||||
<div class="controls">
|
||||
{{combo-box valueAttribute="value"
|
||||
id="category-search-priority"
|
||||
content=searchPrioritiesOptions
|
||||
value=category.search_priority}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.custom_fields.require_reply_approval}}
|
||||
{{i18n 'category.require_reply_approval'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{{plugin-outlet name="category-custom-settings" args=(hash category=category)}}
|
||||
|
||||
{{#unless emailInEnabled}}
|
||||
<section class='field'>
|
||||
{{i18n 'category.email_in_disabled'}}
|
||||
<a href="{{get-url '/admin/site_settings/category/email'}}">{{i18n 'category.email_in_disabled_click'}}</a>
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.allow_badges}}
|
||||
{{i18n 'category.allow_badges_label'}}
|
||||
</label>
|
||||
</section>
|
||||
{{/unless}}
|
||||
|
||||
{{#if siteSettings.topic_featured_link_enabled}}
|
||||
<section class='field'>
|
||||
<div class="allowed-topic-featured-link-category">
|
||||
<label class="checkbox-label">
|
||||
{{input type="checkbox" checked=category.topic_featured_link_allowed}}
|
||||
{{i18n 'category.topic_featured_link_allowed'}}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.suppress_from_latest}}
|
||||
{{i18n "category.suppress_from_latest"}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.navigate_to_first_post_after_read}}
|
||||
{{i18n "category.navigate_to_first_post_after_read"}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.all_topics_wiki}}
|
||||
{{i18n "category.all_topics_wiki"}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
<h3>{{i18n 'category.settings_sections.moderation'}}</h3>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.custom_fields.require_topic_approval}}
|
||||
{{i18n 'category.require_topic_approval'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.custom_fields.require_reply_approval}}
|
||||
{{i18n 'category.require_reply_approval'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
<div class="control-group">
|
||||
<label for="topic-auto-close">
|
||||
{{i18n 'topic.auto_close.label'}}
|
||||
</label>
|
||||
{{text-field value=category.auto_close_hours id="topic-auto-close" type="number"}}
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.auto_close_based_on_last_post}}
|
||||
{{i18n 'topic.auto_close.based_on_last_post'}}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{#if siteSettings.tagging_enabled}}
|
||||
<section class='field minimum-required-tags'>
|
||||
<label for="category-minimum-tags">
|
||||
{{i18n 'category.minimum_required_tags'}}
|
||||
</label>
|
||||
{{text-field value=category.minimum_required_tags id="category-minimum-tags" type="number" min="0"}}
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class="field">
|
||||
<label for="category-number-daily-bump">
|
||||
{{i18n "category.num_auto_bump_daily"}}
|
||||
</label>
|
||||
{{text-field value=category.custom_fields.num_auto_bump_daily id="category-number-daily-bump" type="number"}}
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
<h3> {{i18n "category.settings_sections.appearance"}}</h3>
|
||||
|
||||
<section class="field default-view-field">
|
||||
<label for="category-default-view">
|
||||
{{i18n "category.default_view"}}
|
||||
</label>
|
||||
<div class="controls">
|
||||
{{combo-box valueAttribute="value" id="category-default-view" content=availableViews value=category.default_view}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="field default-top-period-field">
|
||||
<label for="category-default-period">
|
||||
{{i18n "category.default_top_period"}}
|
||||
</label>
|
||||
<div class="controls">
|
||||
{{combo-box valueAttribute="value" id="category-default-period" content=availableTopPeriods value=category.default_top_period}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label for="category-sort-order">
|
||||
{{i18n "category.sort_order"}}
|
||||
</label>
|
||||
<div class="controls">
|
||||
{{combo-box valueAttribute="value" content=availableSorts value=category.sort_order none="category.sort_options.default"}}
|
||||
{{#unless isDefaultSortOrder}}
|
||||
{{combo-box castBoolean=true valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{#if isParentCategory}}
|
||||
<section class="field show-subcategory-list-field">
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.show_subcategory_list}}
|
||||
{{i18n "category.show_subcategory_list"}}
|
||||
</label>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{#if showSubcategoryListStyle}}
|
||||
<section class="field subcategory-list-style-field">
|
||||
<label for="subcategory-list-style">
|
||||
{{i18n "category.subcategory_list_style"}}
|
||||
</label>
|
||||
{{combo-box valueAttribute="value" id="subcategory-list-style" content=availableSubcategoryListStyles value=category.subcategory_list_style}}
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3> {{i18n "category.settings_sections.email"}}</h3>
|
||||
|
||||
{{#if emailInEnabled}}
|
||||
<section class='field'>
|
||||
<label for="category-email-in">
|
||||
{{d-icon "far-envelope"}}
|
||||
{{i18n 'category.email_in'}}
|
||||
</label>
|
||||
{{text-field id="category-email-in" class="email-in" value=category.email_in}}
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.email_in_allow_strangers}}
|
||||
{{i18n 'category.email_in_allow_strangers'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=category.mailinglist_mirror}}
|
||||
{{i18n 'category.mailinglist_mirror'}}
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{{plugin-outlet name="category-email-in" args=(hash category=category)}}
|
||||
{{/if}}
|
||||
|
||||
{{#unless emailInEnabled}}
|
||||
<section class='field'>
|
||||
{{i18n 'category.email_in_disabled'}}
|
||||
<a href="{{get-url '/admin/site_settings/category/email'}}">{{i18n 'category.email_in_disabled_click'}}</a>
|
||||
</section>
|
||||
{{/unless}}
|
||||
|
||||
</section>
|
||||
|
||||
{{plugin-outlet name="category-custom-settings" args=(hash category=category) connectorTagName="" tagName="section"}}
|
||||
|
||||
@ -16,4 +16,6 @@
|
||||
disabled=loading
|
||||
icon="user-plus"
|
||||
label="groups.request"}}
|
||||
{{else}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
|
||||
@ -1,36 +1,38 @@
|
||||
{{#if this.currentUser.admin}}
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="name">{{i18n 'groups.name'}}</label>
|
||||
{{#if canEdit}}
|
||||
{{#if this.currentUser.admin}}
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="name">{{i18n 'groups.name'}}</label>
|
||||
|
||||
{{text-field name="name"
|
||||
class="input-xxlarge group-form-name"
|
||||
value=nameInput
|
||||
placeholderKey="admin.groups.name_placeholder"}}
|
||||
{{text-field name="name"
|
||||
class="input-xxlarge group-form-name"
|
||||
value=nameInput
|
||||
placeholderKey="admin.groups.name_placeholder"}}
|
||||
|
||||
{{input-tip validation=nameValidation}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label" for='full_name'>{{i18n 'groups.manage.full_name'}}</label>
|
||||
|
||||
{{text-field name='full_name'
|
||||
class="input-xxlarge group-form-full-name"
|
||||
value=model.full_name}}
|
||||
</div>
|
||||
|
||||
{{#if this.currentUser.admin}}
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="title">
|
||||
{{i18n 'admin.groups.default_title'}}
|
||||
</label>
|
||||
|
||||
{{input value=model.title name="title" class="input-xxlarge"}}
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n 'admin.groups.default_title_description'}}
|
||||
{{input-tip validation=nameValidation}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label" for='full_name'>{{i18n 'groups.manage.full_name'}}</label>
|
||||
|
||||
{{text-field name='full_name'
|
||||
class="input-xxlarge group-form-full-name"
|
||||
value=model.full_name}}
|
||||
</div>
|
||||
|
||||
{{#if this.currentUser.admin}}
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="title">
|
||||
{{i18n 'admin.groups.default_title'}}
|
||||
</label>
|
||||
|
||||
{{input value=model.title name="title" class="input-xxlarge"}}
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n 'admin.groups.default_title_description'}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div class="control-group">
|
||||
@ -38,10 +40,12 @@
|
||||
{{d-editor value=model.bio_raw class="group-form-bio input-xxlarge"}}
|
||||
</div>
|
||||
|
||||
{{yield}}
|
||||
{{#if canEdit}}
|
||||
{{yield}}
|
||||
|
||||
<div class="control-group">
|
||||
{{group-flair-inputs model=model}}
|
||||
</div>
|
||||
<div class="control-group">
|
||||
{{group-flair-inputs model=model}}
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="group-edit" args=(hash group=model)}}
|
||||
{{plugin-outlet name="group-edit" args=(hash group=model)}}
|
||||
{{/if}}
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
<span class='groups-info-name'>{{group.displayName}}</span>
|
||||
|
||||
{{#if showFullName}}
|
||||
<span class='groups-info-full-name'>{{group.full_name}}</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if group.title}}
|
||||
<div>
|
||||
<span class='groups-info-title'>{{group.title}}</span>
|
||||
</div>
|
||||
<span class='groups-info-name'>{{group.full_name}}</span>
|
||||
{{else}}
|
||||
<span class='groups-info-name'>{{group.displayName}}</span>
|
||||
{{/if}}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
{{#if post}}
|
||||
<div class='reviewable-conversation-post'>
|
||||
{{#link-to 'user' post.user class="username"}}@{{post.user.username}}{{/link-to}} {{{post.excerpt}}}
|
||||
{{#if showUsername}}
|
||||
{{#link-to 'user' post.user class="username"}}@{{post.user.username}}{{/link-to}}
|
||||
{{/if}}
|
||||
{{{post.excerpt}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -2,6 +2,6 @@
|
||||
{{#if user}}
|
||||
{{#user-link user=user}}{{avatar user imageSize="large"}}{{/user-link}}
|
||||
{{else}}
|
||||
{{fa-icon "far-trash-alt" class="deleted-user-avatar"}}
|
||||
{{d-icon "far-trash-alt" class="deleted-user-avatar"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,11 @@
|
||||
<div class='post-contents'>
|
||||
{{reviewable-created-by-name user=reviewable.target_created_by tagName=''}}
|
||||
<div class='post-body'>
|
||||
{{{reviewable.cooked}}}
|
||||
{{#if reviewable.blank_post}}
|
||||
<p>{{i18n "review.deleted_post"}}</p>
|
||||
{{else}}
|
||||
{{{reviewable.cooked}}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{yield}}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div class='reviewable-item {{customClass}}' data-reviewable-id={{reviewable.id}}>
|
||||
<div class='reviewable-meta-data'>
|
||||
<span class='reviewable-type'>{{reviewable.humanType}}</span>
|
||||
<span class='badge-notification new-posts score' title={{i18n "review.scores.score"}}>{{format-score reviewable.score}}</span>
|
||||
<span class='badge-notification new-posts score' title={{i18n "review.scores.about"}}>{{format-score reviewable.score}}</span>
|
||||
{{#if reviewable.reply_count}}
|
||||
<span class='reply-count'>{{i18n "review.replies" count=reviewable.reply_count}}</span>
|
||||
{{/if}}
|
||||
@ -9,19 +9,20 @@
|
||||
{{#link-to 'review.show' reviewable.id}}{{age-with-tooltip reviewable.created_at}}{{/link-to}}
|
||||
</span>
|
||||
<span class='status'>
|
||||
{{#if reviewable.approved}}
|
||||
<span class="approved"> {{d-icon "check"}} {{i18n "review.statuses.approved.title"}} </span>
|
||||
{{else if reviewable.rejected}}
|
||||
<span class="rejected"> {{d-icon "times"}} {{i18n "review.statuses.rejected.title"}} </span>
|
||||
{{else if reviewable.ignored}}
|
||||
<span class="ignored"> {{d-icon "external-link-alt"}} {{i18n "review.statuses.ignored.title"}} </span>
|
||||
{{/if}}
|
||||
{{reviewable-status reviewable.status}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class='reviewable-contents'>
|
||||
{{#if editing}}
|
||||
<div class='editable-fields'>
|
||||
{{#if reviewable.created_by}}
|
||||
<div class='editable-created-by'>
|
||||
{{avatar reviewable.created_by imageSize="tiny"}}
|
||||
{{reviewable-created-by-name user=reviewable.created_by tagName=''}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#each reviewable.editable_fields as |f|}}
|
||||
<div class='editable-field {{dasherize f.id}}'>
|
||||
{{component
|
||||
@ -36,10 +37,7 @@
|
||||
</div>
|
||||
{{else}}
|
||||
{{#component reviewableComponent reviewable=reviewable tagName=''}}
|
||||
<div class='reviewable-scores-and-history'>
|
||||
{{reviewable-scores scores=reviewable.reviewable_scores tagName=''}}
|
||||
{{reviewable-histories histories=reviewable.reviewable_histories tagName=''}}
|
||||
</div>
|
||||
{{reviewable-scores reviewable=reviewable tagName=''}}
|
||||
{{/component}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
<tr class='reviewable-score'>
|
||||
<td class='user'>
|
||||
{{#user-link user=rs.user}}
|
||||
{{avatar rs.user imageSize="tiny"}}
|
||||
{{rs.user.username}}
|
||||
{{/user-link}}
|
||||
{{user-flag-percentage
|
||||
agreed=rs.agree_stats.agreed
|
||||
disagreed=rs.agree_stats.disagreed
|
||||
ignored=rs.agree_stats.ignored}}
|
||||
</td>
|
||||
<td>
|
||||
{{d-icon rs.score_type.icon}}
|
||||
{{title}}
|
||||
<span class="badge-notification new-posts score" title={{i18n "review.scores.about"}}>{{format-score rs.score}}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{format-date rs.created_at format="tiny"}}
|
||||
</td>
|
||||
|
||||
<td class="reviewable-score-spacer">
|
||||
{{d-icon "angle-double-right"}}
|
||||
</td>
|
||||
|
||||
<td class='reviewed-by'>
|
||||
{{#if rs.reviewed_by}}
|
||||
{{#user-link user=rs.reviewed_by}}
|
||||
{{avatar rs.reviewed_by imageSize="tiny"}}
|
||||
{{rs.reviewed_by.username}}
|
||||
{{/user-link}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{reviewable-status rs.status}}
|
||||
</td>
|
||||
<td>
|
||||
{{#if rs.reviewed_by}}
|
||||
{{format-date rs.reviewed_at format="tiny"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{#if rs.reason}}
|
||||
<tr>
|
||||
<td colspan='7'>
|
||||
<div class='reviewable-score-reason'>{{{rs.reason}}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
|
||||
{{#if rs.reviewable_conversation}}
|
||||
<tr>
|
||||
<td colspan='7'>
|
||||
<div class='reviewable-conversation'>
|
||||
{{#each rs.reviewable_conversation.conversation_posts as |p index|}}
|
||||
{{reviewable-conversation-post post=p index=index}}
|
||||
{{/each}}
|
||||
<div class='controls'>
|
||||
<a href={{rs.reviewable_conversation.permalink}} class='btn btn-small'>
|
||||
{{i18n "review.conversation.view_full"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
@ -0,0 +1,14 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: "",
|
||||
|
||||
@computed("rs.score_type.title", "reviewable.target_created_by")
|
||||
title(title, targetCreatedBy) {
|
||||
if (title && targetCreatedBy) {
|
||||
return title.replace("{{username}}", targetCreatedBy.username);
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
});
|
||||
@ -1,40 +1,8 @@
|
||||
{{#if scores}}
|
||||
{{#if reviewable.reviewable_scores}}
|
||||
<table class='reviewable-scores'>
|
||||
<tbody>
|
||||
{{#each scores as |rs|}}
|
||||
<tr class='reviewable-score'>
|
||||
|
||||
<td>{{d-icon "flag"}} {{rs.score_type.title}} <span class="badge-notification new-posts score">{{format-score rs.score}}</span></td>
|
||||
<td class='user'>
|
||||
{{#user-link user=rs.user}}
|
||||
{{avatar rs.user imageSize="tiny"}}
|
||||
{{rs.user.username}}
|
||||
{{/user-link}}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
{{user-flag-percentage
|
||||
agreed=rs.agree_stats.agreed
|
||||
disagreed=rs.agree_stats.disagreed
|
||||
ignored=rs.agree_stats.ignored}}
|
||||
</td>
|
||||
</tr>
|
||||
{{#if rs.reviewable_conversation}}
|
||||
<tr>
|
||||
<td colspan='3'>
|
||||
<div class='reviewable-conversation'>
|
||||
{{#each rs.reviewable_conversation.conversation_posts as |p|}}
|
||||
{{reviewable-conversation-post post=p}}
|
||||
{{/each}}
|
||||
<div class='controls'>
|
||||
<a href={{rs.reviewable_conversation.permalink}} class='btn btn-small'>
|
||||
{{i18n "review.conversation.view_full"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
{{#each reviewable.reviewable_scores as |rs|}}
|
||||
{{reviewable-score rs=rs reviewable=reviewable}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -7,6 +7,9 @@
|
||||
{{else if (has-block)}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
<span class="title-text">{{i18n "topic.deleted"}}</span>
|
||||
<span class="title-text">
|
||||
{{i18n "review.topics.deleted"}}
|
||||
{{link-to (i18n "review.topics.original") "topic" "-" reviewable.removed_topic_id}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -1,33 +1,52 @@
|
||||
{{#if visible}}
|
||||
<div class="card-content">
|
||||
|
||||
<div class="card-row first-row">
|
||||
<div class="user-card-avatar">
|
||||
{{#if user.profile_hidden}}
|
||||
<span class="card-huge-avatar">{{bound-avatar user "huge"}}</span>
|
||||
{{else}}
|
||||
<a href="{{user.path}}" {{action "showUser" user}} class="card-huge-avatar">{{bound-avatar user "huge"}}</a>
|
||||
{{/if}}
|
||||
{{#if user.primary_group_name}}
|
||||
{{avatar-flair
|
||||
flairURL=user.primary_group_flair_url
|
||||
flairBgColor=user.primary_group_flair_bg_color
|
||||
flairColor=user.primary_group_flair_color
|
||||
groupName=user.primary_group_name}}
|
||||
{{/if}}
|
||||
{{plugin-outlet name="user-card-avatar-flair" args=(hash user=user) tagName='div'}}
|
||||
{{#if loading}}
|
||||
<div class="card-row first-row">
|
||||
<div class="user-card-avatar">
|
||||
<div class="card-avatar-placeholder animated-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="names">
|
||||
<span>
|
||||
|
||||
<div class="card-row second-row">
|
||||
<div class="animated-placeholder"></div>
|
||||
</div>
|
||||
<div class="card-row third-row">
|
||||
<div class="animated-placeholder"></div>
|
||||
</div>
|
||||
<div class="card-row fourth-row">
|
||||
<div class="animated-placeholder"></div>
|
||||
</div>
|
||||
<div class="card-row sixth-row">
|
||||
<div class="animated-placeholder"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="card-row first-row">
|
||||
<div class="user-card-avatar">
|
||||
{{#if user.profile_hidden}}
|
||||
<span class="card-huge-avatar">{{bound-avatar user "huge"}}</span>
|
||||
{{else}}
|
||||
<a href="{{user.path}}" {{action "showUser" user}} class="card-huge-avatar">{{bound-avatar user "huge"}}</a>
|
||||
{{/if}}
|
||||
{{#if user.primary_group_name}}
|
||||
{{avatar-flair
|
||||
flairURL=user.primary_group_flair_url
|
||||
flairBgColor=user.primary_group_flair_bg_color
|
||||
flairColor=user.primary_group_flair_color
|
||||
groupName=user.primary_group_name}}
|
||||
{{/if}}
|
||||
{{plugin-outlet name="user-card-avatar-flair" args=(hash user=user) tagName='div'}}
|
||||
</div>
|
||||
<div class="names">
|
||||
<h1 class="{{staff}} {{newUser}} {{if nameFirst "full-name" "username"}}">
|
||||
{{#if user.profile_hidden}}
|
||||
<span>
|
||||
<span class="name-username-wrapper">
|
||||
{{if nameFirst user.name (format-username username)}}
|
||||
{{user-status user currentUser=currentUser}}
|
||||
</span>
|
||||
{{else}}
|
||||
<a href="{{user.path}}" {{action "showUser" user}} class='user-profile-link'>
|
||||
{{if nameFirst user.name (format-username username)}}
|
||||
<span class="name-username-wrapper">
|
||||
{{if nameFirst user.name (format-username username)}}
|
||||
</span>
|
||||
{{user-status user currentUser=currentUser}}
|
||||
</a>
|
||||
{{/if}}
|
||||
@ -47,105 +66,103 @@
|
||||
<h2 class="staged">{{i18n 'user.staged'}}</h2>
|
||||
{{/if}}
|
||||
{{plugin-outlet name="user-card-post-names" args=(hash user=user) tagName='div'}}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="usercard-controls">
|
||||
{{#if user.can_send_private_message_to_user}}
|
||||
<li class='compose-pm'>
|
||||
{{d-button
|
||||
class="btn-primary"
|
||||
action=(route-action "composePrivateMessage" user post)
|
||||
icon="envelope"
|
||||
label="user.private_message"}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showFilter}}
|
||||
<li>
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
action=(action "togglePosts" user)
|
||||
icon="filter"
|
||||
translatedLabel=togglePostsLabel}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if hasUserFilters}}
|
||||
<li>
|
||||
{{d-button
|
||||
action=(action "cancelFilter")
|
||||
icon="times"
|
||||
label="topic.filters.cancel"}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showDelete}}
|
||||
<li>
|
||||
{{d-button
|
||||
class="btn-danger"
|
||||
action=(action "deleteUser")
|
||||
actionParam=user
|
||||
icon="exclamation-triangle"
|
||||
label="admin.user.delete"}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{plugin-outlet
|
||||
name="user-card-additional-controls"
|
||||
args=(hash user=user close=(action "close"))
|
||||
tagName=""}}
|
||||
</div>
|
||||
|
||||
{{#if user.profile_hidden}}
|
||||
<div class="card-row second-row">
|
||||
<div class='profile-hidden'>
|
||||
<span>{{i18n "user.profile_hidden"}}</span>
|
||||
</div>
|
||||
<ul class="usercard-controls">
|
||||
{{#if user.can_send_private_message_to_user}}
|
||||
<li class='compose-pm'>
|
||||
{{d-button
|
||||
class="btn-primary"
|
||||
action=(route-action "composePrivateMessage" user post)
|
||||
icon="envelope"
|
||||
label="user.private_message"}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showFilter}}
|
||||
<li>
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
action=(action "togglePosts" user)
|
||||
icon="filter"
|
||||
translatedLabel=togglePostsLabel}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if hasUserFilters}}
|
||||
<li>
|
||||
{{d-button
|
||||
action=(action "cancelFilter")
|
||||
icon="times"
|
||||
label="topic.filters.cancel"}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showDelete}}
|
||||
<li>
|
||||
{{d-button
|
||||
class="btn-danger"
|
||||
action=(action "deleteUser")
|
||||
actionParam=user
|
||||
icon="exclamation-triangle"
|
||||
label="admin.user.delete"}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{plugin-outlet
|
||||
name="user-card-additional-controls"
|
||||
args=(hash user=user close=(action "close"))
|
||||
tagName=""}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isSuspendedOrHasBio}}
|
||||
<div class="card-row second-row">
|
||||
{{#if user.suspend_reason}}
|
||||
<div class='suspended'>
|
||||
<div class="suspension-date">
|
||||
{{d-icon "ban"}}
|
||||
{{i18n 'user.suspended_notice' date=user.suspendedTillDate}}
|
||||
</div>
|
||||
<div class='suspension-reason'>
|
||||
<span class="suspension-reason-title">{{i18n 'user.suspended_reason'}}</span>
|
||||
<span class="suspension-reason-description">{{user.suspend_reason}}</span>
|
||||
</div>
|
||||
{{#if user.profile_hidden}}
|
||||
<div class="card-row second-row">
|
||||
<div class='profile-hidden'>
|
||||
<span>{{i18n "user.profile_hidden"}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if user.bio_cooked}}
|
||||
<div class='bio'>{{text-overflow class="overflow" text=user.bio_excerpt}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if hasLocationOrWebsite}}
|
||||
<div class="card-row third-row">
|
||||
<div class="location-and-website">
|
||||
{{#if user.location}}
|
||||
<span class='location'>{{d-icon "map-marker-alt"}}
|
||||
<span>{{user.location}}</span></span>
|
||||
{{/if}}
|
||||
{{#if user.website_name}}
|
||||
<span class='website-name'>
|
||||
{{d-icon "globe"}}
|
||||
{{#if linkWebsite}}
|
||||
<a href="{{user.website}}" rel={{unless removeNoFollow 'nofollow noopener'}}
|
||||
target="_blank">{{user.website_name}}</a>
|
||||
{{else}}
|
||||
<span title={{user.website}}>{{user.website_name}}</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{plugin-outlet name="user-card-location-and-website" args=(hash user=user)}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if isSuspendedOrHasBio}}
|
||||
<div class="card-row second-row">
|
||||
{{#if user.suspend_reason}}
|
||||
<div class='suspended'>
|
||||
<div class="suspension-date">
|
||||
{{d-icon "ban"}}
|
||||
{{i18n 'user.suspended_notice' date=user.suspendedTillDate}}
|
||||
</div>
|
||||
<div class='suspension-reason'>
|
||||
<span class="suspension-reason-title">{{i18n 'user.suspended_reason'}}</span>
|
||||
<span class="suspension-reason-description">{{user.suspend_reason}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if user.bio_cooked}}
|
||||
<div class='bio'>{{text-overflow class="overflow" text=user.bio_excerpt}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if hasLocationOrWebsite}}
|
||||
<div class="card-row third-row">
|
||||
<div class="location-and-website">
|
||||
{{#if user.location}}
|
||||
<span class='location'>{{d-icon "map-marker-alt"}}
|
||||
<span>{{user.location}}</span></span>
|
||||
{{/if}}
|
||||
{{#if user.website_name}}
|
||||
<span class='website-name'>
|
||||
{{d-icon "globe"}}
|
||||
{{#if linkWebsite}}
|
||||
<a href="{{user.website}}" rel={{unless removeNoFollow 'nofollow noopener'}}
|
||||
target="_blank">{{user.website_name}}</a>
|
||||
{{else}}
|
||||
<span title={{user.website}}>{{user.website_name}}</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{plugin-outlet name="user-card-location-and-website" args=(hash user=user)}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if user.time_read}}
|
||||
<div class="card-row fourth-row">
|
||||
{{#unless user.profile_hidden}}
|
||||
<div class="metadata">
|
||||
@ -155,13 +172,15 @@
|
||||
{{/if}}
|
||||
<h3><span class='desc'>{{i18n 'joined'}}</span>
|
||||
{{format-date user.created_at leaveAgo="true"}}</h3>
|
||||
<h3 title="{{timeReadTooltip}}">
|
||||
<span class='desc'>{{i18n 'time_read'}}</span>
|
||||
{{format-duration user.time_read}}
|
||||
{{#if showRecentTimeRead}}
|
||||
<span>({{i18n 'time_read_recently' time_read=recentTimeRead}})</span>
|
||||
{{/if}}
|
||||
</h3>
|
||||
{{#if user.time_read}}
|
||||
<h3 title="{{timeReadTooltip}}">
|
||||
<span class='desc'>{{i18n 'time_read'}}</span>
|
||||
{{format-duration user.time_read}}
|
||||
{{#if showRecentTimeRead}}
|
||||
<span>({{i18n 'time_read_recently' time_read=recentTimeRead}})</span>
|
||||
{{/if}}
|
||||
</h3>
|
||||
{{/if}}
|
||||
{{#if showCheckEmail}}
|
||||
<h3 class="email">
|
||||
{{d-icon "far-envelope" title="user.email.title"}}
|
||||
@ -169,11 +188,11 @@
|
||||
{{user.email}}
|
||||
{{else}}
|
||||
{{d-button
|
||||
action=(action "checkEmail")
|
||||
actionParam=user
|
||||
icon="far-envelope"
|
||||
label="admin.users.check_email.text"
|
||||
class="btn-primary"}}
|
||||
action=(action "checkEmail")
|
||||
actionParam=user
|
||||
icon="far-envelope"
|
||||
label="admin.users.check_email.text"
|
||||
class="btn-primary"}}
|
||||
{{/if}}
|
||||
</h3>
|
||||
{{/if}}
|
||||
@ -182,43 +201,42 @@
|
||||
{{/unless}}
|
||||
{{plugin-outlet name="user-card-after-metadata" args=(hash user=user)}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if publicUserFields}}
|
||||
<div class="card-row fifth-row">
|
||||
<div class="public-user-fields">
|
||||
{{#each publicUserFields as |uf|}}
|
||||
{{#if uf.value}}
|
||||
<div class="public-user-field {{uf.field.dasherized_name}}">
|
||||
<span class="user-field-name">{{uf.field.name}}:</span>
|
||||
<span class="user-field-value">{{uf.value}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="user-card-before-badges" args=(hash user=user)}}
|
||||
|
||||
{{#if showBadges}}
|
||||
<div class="card-row sixth-row">
|
||||
{{#if user.featured_user_badges}}
|
||||
<div class="badge-section">
|
||||
{{#each user.featured_user_badges as |ub|}}
|
||||
{{user-badge badge=ub.badge user=user}}
|
||||
{{#if publicUserFields}}
|
||||
<div class="card-row fifth-row">
|
||||
<div class="public-user-fields">
|
||||
{{#each publicUserFields as |uf|}}
|
||||
{{#if uf.value}}
|
||||
<div class="public-user-field {{uf.field.dasherized_name}}">
|
||||
<span class="user-field-name">{{uf.field.name}}:</span>
|
||||
<span class="user-field-value">{{uf.value}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{#if showMoreBadges}}
|
||||
<span class='more-user-badges'>
|
||||
{{#link-to 'user.badges' user}}
|
||||
{{i18n 'badges.more_badges' count=moreBadgesCount}}
|
||||
{{/link-to}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="user-card-before-badges" args=(hash user=user)}}
|
||||
|
||||
{{#if showBadges}}
|
||||
<div class="card-row sixth-row">
|
||||
{{#if user.featured_user_badges}}
|
||||
<div class="badge-section">
|
||||
{{#each user.featured_user_badges as |ub|}}
|
||||
{{user-badge badge=ub.badge user=user}}
|
||||
{{/each}}
|
||||
{{#if showMoreBadges}}
|
||||
<span class='more-user-badges'>
|
||||
{{#link-to 'user.badges' user}}
|
||||
{{i18n 'badges.more_badges' count=moreBadgesCount}}
|
||||
{{/link-to}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -162,7 +162,11 @@
|
||||
{{#if model.draftConflictUser}}
|
||||
{{avatar model.draftConflictUser imageSize="small"}}
|
||||
{{/if}}
|
||||
{{model.draftStatus}}
|
||||
{{#if model.draftSaving}}<div class="spinner small"></div>{{/if}}
|
||||
{{#if model.draftSaved}}{{d-icon 'check' class='save-animation'}}{{/if}}
|
||||
{{#if model.draftStatus}}
|
||||
<span title="{{model.draftStatus}}">{{d-icon 'user-edit'}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
<section class="user-content">
|
||||
|
||||
<div class="group-members-actions">
|
||||
{{text-field value=filterInput
|
||||
placeholderKey=filterPlaceholder
|
||||
@ -49,3 +51,5 @@
|
||||
{{else}}
|
||||
<div>{{i18n "groups.empty.requests"}}</div>
|
||||
{{/if}}
|
||||
|
||||
</section>
|
||||
|
||||
@ -25,69 +25,61 @@
|
||||
{{#conditional-loading-spinner condition=model.loading}}
|
||||
{{#load-more selector=".groups-table .groups-table-row" action=(action "loadMore")}}
|
||||
<div class='container'>
|
||||
<table class="groups-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{{directory-toggle field="name" labelKey="groups.group_name" order=order asc=asc}}
|
||||
{{directory-toggle field="user_count" labelKey="groups.user_count" order=order asc=asc}}
|
||||
<th>{{i18n "groups.index.group_type"}}</th>
|
||||
<th>{{i18n "groups.membership"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each model as |group|}}
|
||||
<tr class="groups-table-row">
|
||||
<td class="groups-info">
|
||||
{{#link-to "group.members" group.name}}
|
||||
{{#if group.flair_url}}
|
||||
<span class='group-avatar-flair'>
|
||||
{{avatar-flair
|
||||
flairURL=group.flair_url
|
||||
flairBgColor=group.flair_bg_color
|
||||
flairColor=group.flair_color
|
||||
groupName=group.name}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{groups-info group=group}}
|
||||
{{/link-to}}
|
||||
</td>
|
||||
|
||||
<td class="groups-user-count">{{d-icon "group"}}{{group.user_count}}</td>
|
||||
|
||||
<td class="groups-table-type">
|
||||
{{#if group.public_admission}}
|
||||
{{i18n 'groups.index.public'}}
|
||||
{{else if group.isPrivate}}
|
||||
{{d-icon "far-eye-slash"}}
|
||||
{{i18n 'groups.index.private'}}
|
||||
{{else}}
|
||||
{{#if group.automatic}}
|
||||
{{i18n 'groups.index.automatic'}}
|
||||
{{else}}
|
||||
{{i18n 'groups.index.closed'}}
|
||||
{{/if}}
|
||||
<div class="groups-boxes">
|
||||
{{#each model as |group|}}
|
||||
{{#link-to "group.members" group.name class="group-box"}}
|
||||
<div class="group-box-inner">
|
||||
<div class="group-info-wrapper">
|
||||
{{#if group.flair_url}}
|
||||
<span class='group-avatar-flair'>
|
||||
{{avatar-flair
|
||||
flairURL=group.flair_url
|
||||
flairBgColor=group.flair_bg_color
|
||||
flairColor=group.flair_color
|
||||
groupName=group.name}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<span class="group-info">
|
||||
{{groups-info group=group}}
|
||||
|
||||
<td class="groups-table-membership">
|
||||
{{#if group.is_group_owner}}
|
||||
<span>
|
||||
<div class="group-user-count">{{d-icon "user"}}{{group.user_count}}</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="group-description">{{{group.bio_excerpt}}}</div>
|
||||
|
||||
<div class="group-membership">
|
||||
{{#group-membership-button tagName='' model=group showLogin=(route-action "showLogin")}}
|
||||
{{#if group.is_group_owner}}
|
||||
<span class="is-group-owner">
|
||||
{{d-icon "shield"}}
|
||||
{{i18n "groups.index.is_group_owner"}}
|
||||
</span>
|
||||
{{else if group.is_group_user}}
|
||||
<span>
|
||||
{{else if group.is_group_user}}
|
||||
<span class="is-group-member">
|
||||
{{d-icon "check"}}
|
||||
{{i18n "groups.index.is_group_user"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{group-membership-button tagName='' model=group showLogin=(route-action "showLogin")}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else if group.public_admission}}
|
||||
{{i18n 'groups.index.public'}}
|
||||
{{else if group.isPrivate}}
|
||||
{{d-icon "far-eye-slash"}}
|
||||
{{i18n 'groups.index.private'}}
|
||||
{{else}}
|
||||
{{#if group.automatic}}
|
||||
{{i18n 'groups.index.automatic'}}
|
||||
{{else}}
|
||||
{{d-icon "ban"}}
|
||||
{{i18n 'groups.index.closed'}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/group-membership-button}}
|
||||
</div>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/load-more}}
|
||||
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
{{#d-modal-body title="post.controls.add_post_notice"}}
|
||||
<form>{{textarea value=notice}}</form>
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button
|
||||
class="btn-primary"
|
||||
action=(action "setNotice")
|
||||
disabled=disabled
|
||||
label=(if saving "saving" "save")}}
|
||||
{{d-modal-cancel close=(route-action "closeModal")}}
|
||||
</div>
|
||||
@ -14,6 +14,17 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#unless siteSettings.sso_overrides_avatar}}
|
||||
<div class="control-group pref-avatar">
|
||||
<label class="control-label" id="profile-picture">{{i18n 'user.avatar.title'}}</label>
|
||||
<div class="controls">
|
||||
{{! we want the "huge" version even though we're downsizing it in CSS }}
|
||||
{{bound-avatar model "huge"}}
|
||||
{{d-button action=(route-action "showAvatarSelector") actionParam=model class="btn-default pad-left" icon="pencil-alt"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if canEditName}}
|
||||
<div class="control-group pref-name">
|
||||
<label class="control-label">{{i18n 'user.name.title'}}</label>
|
||||
@ -141,17 +152,6 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless siteSettings.sso_overrides_avatar}}
|
||||
<div class="control-group pref-avatar">
|
||||
<label class="control-label" id="profile-picture">{{i18n 'user.avatar.title'}}</label>
|
||||
<div class="controls">
|
||||
{{! we want the "huge" version even though we're downsizing it in CSS }}
|
||||
{{bound-avatar model "huge"}}
|
||||
{{d-button action=(route-action "showAvatarSelector") actionParam=model class="btn-default pad-left" icon="pencil-alt"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if canSelectTitle}}
|
||||
<div class="control-group pref-title">
|
||||
<label class="control-label">{{i18n 'user.title.title'}}</label>
|
||||
|
||||
@ -49,8 +49,6 @@
|
||||
|
||||
{{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab}}
|
||||
{{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}}
|
||||
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}}
|
||||
{{preference-checkbox labelKey="user.disable_jump_reply" checked=model.user_option.disable_jump_reply}}
|
||||
{{#if siteSettings.automatically_unpin_topics}}
|
||||
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}}
|
||||
{{/if}}
|
||||
@ -58,6 +56,14 @@
|
||||
{{#if isiPad}}
|
||||
{{preference-checkbox labelKey="user.enable_physical_keyboard" checked=disableSafariHacks}}
|
||||
{{/if}}
|
||||
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}}
|
||||
<div class='controls controls-dropdown'>
|
||||
<label for="user-email-level">{{i18n 'user.title_count_mode.title'}}</label>
|
||||
{{combo-box valueAttribute="value"
|
||||
content=titleCountModes
|
||||
value=model.user_option.title_count_mode
|
||||
id="user-title-count-mode"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="user-preferences-interface" args=(hash model=model save=(action "save"))}}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<div class="instructions">{{i18n 'user.muted_users_instructions'}}</div>
|
||||
|
||||
{{#if siteSettings.ignore_user_enabled}}
|
||||
{{#if ignoredEnabled}}
|
||||
<div class="controls tracking-controls">
|
||||
<label>{{d-icon "eye-slash" class="icon"}} {{i18n 'user.ignored_users'}}</label>
|
||||
{{user-selector excludeCurrentUser=true
|
||||
|
||||
@ -184,6 +184,8 @@
|
||||
rebakePost=(action "rebakePost")
|
||||
changePostOwner=(action "changePostOwner")
|
||||
grantBadge=(action "grantBadge")
|
||||
addNotice=(action "addNotice")
|
||||
removeNotice=(action "removeNotice")
|
||||
lockPost=(action "lockPost")
|
||||
unlockPost=(action "unlockPost")
|
||||
unhidePost=(action "unhidePost")
|
||||
@ -211,10 +213,41 @@
|
||||
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
|
||||
{{#if loadedAllPosts}}
|
||||
|
||||
{{#if model.pending_posts_count}}
|
||||
{{#if model.pending_posts}}
|
||||
<div class='pending-posts'>
|
||||
{{#each model.pending_posts as |pending|}}
|
||||
<div class='reviewable-item'>
|
||||
<div class='reviewable-meta-data'>
|
||||
<span class='reviewable-type'>
|
||||
{{i18n "review.awaiting_approval"}}
|
||||
</span>
|
||||
<span class='created-at'>
|
||||
{{age-with-tooltip pending.created_at}}
|
||||
</span>
|
||||
</div>
|
||||
<div class='post-contents-wrapper'>
|
||||
{{reviewable-created-by user=currentUser tagName=''}}
|
||||
<div class='post-contents'>
|
||||
{{reviewable-created-by-name user=currentUser tagName=''}}
|
||||
<div class='post-body'>{{cook-text pending.raw}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='reviewable-actions'>
|
||||
{{d-button
|
||||
class="btn-danger"
|
||||
label="review.delete"
|
||||
icon="trash-alt"
|
||||
action=(action "deletePending" pending) }}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.queued_posts_count}}
|
||||
<div class="has-pending-posts">
|
||||
<div>
|
||||
{{{i18n "review.topic_has_pending" count=model.pending_posts_count}}}
|
||||
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
|
||||
</div>
|
||||
|
||||
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
|
||||
|
||||
@ -193,7 +193,7 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if canDeleteUser}}
|
||||
<div>{{d-button action=(action "adminDelete") icon="exclamation-triangle" label="user.admin_delete" class="btn-danger"}}</div>
|
||||
<div class='pull-right'>{{d-button action=(action "adminDelete") icon="exclamation-triangle" label="user.admin_delete" class="btn-danger"}}</div>
|
||||
{{/if}}
|
||||
</dl>
|
||||
{{plugin-outlet name="user-profile-secondary" args=(hash model=model)}}
|
||||
|
||||
@ -10,11 +10,10 @@
|
||||
|
||||
{{#conditional-loading-spinner condition=model.loading}}
|
||||
{{#if model.length}}
|
||||
<div class='total-rows'>{{i18n "directory.total_rows" count=model.totalRows}}</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th> </th>
|
||||
<th>{{i18n "directory.total_rows" count=model.totalRows}}</th>
|
||||
{{directory-toggle field="likes_received" order=order asc=asc icon="heart"}}
|
||||
{{directory-toggle field="likes_given" order=order asc=asc icon="heart"}}
|
||||
{{directory-toggle field="topic_count" order=order asc=asc}}
|
||||
|
||||
@ -29,6 +29,7 @@ export default createWidget("embedded-post", {
|
||||
buildKey: attrs => `embedded-post-${attrs.id}`,
|
||||
|
||||
html(attrs, state) {
|
||||
attrs.embeddedPost = true;
|
||||
return [
|
||||
h("div.reply", { attributes: { "data-post-id": attrs.id } }, [
|
||||
h("div.row", [
|
||||
@ -41,7 +42,7 @@ export default createWidget("embedded-post", {
|
||||
shareUrl: attrs.shareUrl
|
||||
})
|
||||
]),
|
||||
new PostCooked(attrs, new DecoratorHelper(this))
|
||||
new PostCooked(attrs, new DecoratorHelper(this), this.currentUser)
|
||||
])
|
||||
])
|
||||
])
|
||||
|
||||
56
app/assets/javascripts/discourse/widgets/footer-nav.js.es6
Normal file
56
app/assets/javascripts/discourse/widgets/footer-nav.js.es6
Normal file
@ -0,0 +1,56 @@
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
|
||||
|
||||
createWidget("footer-nav", {
|
||||
tagName: "div.footer-nav-widget",
|
||||
|
||||
html(attrs) {
|
||||
const buttons = [];
|
||||
|
||||
buttons.push(
|
||||
this.attach("flat-button", {
|
||||
action: "goBack",
|
||||
icon: "chevron-left",
|
||||
className: "btn-large",
|
||||
disabled: !attrs.canGoBack
|
||||
})
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
this.attach("flat-button", {
|
||||
action: "goForward",
|
||||
icon: "chevron-right",
|
||||
className: "btn-large",
|
||||
disabled: !attrs.canGoForward
|
||||
})
|
||||
);
|
||||
|
||||
if (isAppWebview()) {
|
||||
buttons.push(
|
||||
this.attach("flat-button", {
|
||||
action: "share",
|
||||
icon: "link",
|
||||
className: "btn-large"
|
||||
})
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
this.attach("flat-button", {
|
||||
action: "dismiss",
|
||||
icon: "chevron-down",
|
||||
className: "btn-large"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
},
|
||||
|
||||
dismiss() {
|
||||
postRNWebviewMessage("dismiss", true);
|
||||
},
|
||||
|
||||
share() {
|
||||
postRNWebviewMessage("shareUrl", window.location.href);
|
||||
}
|
||||
});
|
||||
@ -184,18 +184,18 @@ createWidget("header-icons", {
|
||||
action: "toggleHamburger",
|
||||
href: "",
|
||||
contents() {
|
||||
if (!attrs.flagCount) {
|
||||
return;
|
||||
let { currentUser } = this;
|
||||
if (currentUser && currentUser.reviewable_count) {
|
||||
return h(
|
||||
"div.badge-notification.reviewables",
|
||||
{
|
||||
attributes: {
|
||||
title: I18n.t("notifications.reviewable_items")
|
||||
}
|
||||
},
|
||||
this.currentUser.reviewable_count
|
||||
);
|
||||
}
|
||||
return h(
|
||||
"div.badge-notification.flagged-posts",
|
||||
{
|
||||
attributes: {
|
||||
title: I18n.t("notifications.total_flagged")
|
||||
}
|
||||
},
|
||||
attrs.flagCount
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -410,7 +410,9 @@ export default createWidget("header", {
|
||||
|
||||
if (currentPath === "full-page-search") {
|
||||
scrollTop();
|
||||
$(".full-page-search").focus();
|
||||
$(".full-page-search")
|
||||
.trigger("touchstart")
|
||||
.focus();
|
||||
return false;
|
||||
} else {
|
||||
return DiscourseURL.routeTo("/search" + params);
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
const CLICK_ATTRIBUTE_NAME = "_discourse_click_widget";
|
||||
const CLICK_OUTSIDE_ATTRIBUTE_NAME = "_discourse_click_outside_widget";
|
||||
const MOUSE_DOWN_OUTSIDE_ATTRIBUTE_NAME =
|
||||
"_discourse_mouse_down_outside_widget";
|
||||
const KEY_UP_ATTRIBUTE_NAME = "_discourse_key_up_widget";
|
||||
const KEY_DOWN_ATTRIBUTE_NAME = "_discourse_key_down_widget";
|
||||
const DRAG_ATTRIBUTE_NAME = "_discourse_drag_widget";
|
||||
@ -33,6 +35,10 @@ export const WidgetClickOutsideHook = buildHook(
|
||||
CLICK_OUTSIDE_ATTRIBUTE_NAME,
|
||||
"data-click-outside"
|
||||
);
|
||||
export const WidgetMouseDownOutsideHook = buildHook(
|
||||
MOUSE_DOWN_OUTSIDE_ATTRIBUTE_NAME,
|
||||
"data-mouse-down-outside"
|
||||
);
|
||||
export const WidgetKeyUpHook = buildHook(KEY_UP_ATTRIBUTE_NAME);
|
||||
export const WidgetKeyDownHook = buildHook(KEY_DOWN_ATTRIBUTE_NAME);
|
||||
export const WidgetDragHook = buildHook(DRAG_ATTRIBUTE_NAME);
|
||||
@ -136,6 +142,20 @@ WidgetClickHook.setupDocumentCallback = function() {
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("mousedown.discourse-widget", e => {
|
||||
let node = e.target;
|
||||
const $outside = $("[data-mouse-down-outside]");
|
||||
$outside.each((i, outNode) => {
|
||||
if (outNode.contains(node)) {
|
||||
return;
|
||||
}
|
||||
const widget2 = outNode[MOUSE_DOWN_OUTSIDE_ATTRIBUTE_NAME];
|
||||
if (widget2) {
|
||||
widget2.mouseDownOutside(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("keyup.discourse-widget", e => {
|
||||
nodeCallback(e.target, KEY_UP_ATTRIBUTE_NAME, w => w.keyUp(e));
|
||||
});
|
||||
|
||||
@ -37,24 +37,33 @@ export function buildManageButtons(attrs, currentUser, siteSettings) {
|
||||
contents.push(buttonAtts);
|
||||
}
|
||||
|
||||
if (attrs.canManage) {
|
||||
contents.push({
|
||||
icon: "cog",
|
||||
label: "post.controls.rebake",
|
||||
action: "rebakePost",
|
||||
className: "btn-default rebuild-html"
|
||||
});
|
||||
|
||||
if (attrs.hidden) {
|
||||
if (currentUser.staff) {
|
||||
if (attrs.noticeType) {
|
||||
contents.push({
|
||||
icon: "far-eye",
|
||||
label: "post.controls.unhide",
|
||||
action: "unhidePost",
|
||||
className: "btn-default unhide-post"
|
||||
icon: "user-shield",
|
||||
label: "post.controls.remove_post_notice",
|
||||
action: "removeNotice",
|
||||
className: "btn-default remove-notice"
|
||||
});
|
||||
} else {
|
||||
contents.push({
|
||||
icon: "user-shield",
|
||||
label: "post.controls.add_post_notice",
|
||||
action: "addNotice",
|
||||
className: "btn-default add-notice"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.canManage && attrs.hidden) {
|
||||
contents.push({
|
||||
icon: "far-eye",
|
||||
label: "post.controls.unhide",
|
||||
action: "unhidePost",
|
||||
className: "btn-default unhide-post"
|
||||
});
|
||||
}
|
||||
|
||||
if (currentUser.admin) {
|
||||
contents.push({
|
||||
icon: "user",
|
||||
@ -74,14 +83,23 @@ export function buildManageButtons(attrs, currentUser, siteSettings) {
|
||||
});
|
||||
}
|
||||
|
||||
const action = attrs.locked ? "unlock" : "lock";
|
||||
contents.push({
|
||||
icon: action,
|
||||
label: `post.controls.${action}_post`,
|
||||
action: `${action}Post`,
|
||||
title: `post.controls.${action}_post_description`,
|
||||
className: `btn-default ${action}-post`
|
||||
});
|
||||
if (attrs.locked) {
|
||||
contents.push({
|
||||
icon: "unlock",
|
||||
label: "post.controls.unlock_post",
|
||||
action: "unlockPost",
|
||||
title: "post.controls.unlock_post_description",
|
||||
className: "btn-default unlock-post"
|
||||
});
|
||||
} else {
|
||||
contents.push({
|
||||
icon: "lock",
|
||||
label: "post.controls.lock_post",
|
||||
action: "lockPost",
|
||||
title: "post.controls.lock_post_description",
|
||||
className: "btn-default lock-post"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.canManage || attrs.canWiki) {
|
||||
@ -102,6 +120,15 @@ export function buildManageButtons(attrs, currentUser, siteSettings) {
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.canManage) {
|
||||
contents.push({
|
||||
icon: "cog",
|
||||
label: "post.controls.rebake",
|
||||
action: "rebakePost",
|
||||
className: "btn-default rebuild-html"
|
||||
});
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
|
||||
@ -12,18 +12,12 @@ export function addDecorator(cb) {
|
||||
}
|
||||
|
||||
export default class PostCooked {
|
||||
constructor(attrs, decoratorHelper) {
|
||||
constructor(attrs, decoratorHelper, currentUser) {
|
||||
this.attrs = attrs;
|
||||
this.expanding = false;
|
||||
this._highlighted = false;
|
||||
|
||||
if (decoratorHelper) {
|
||||
this.decoratorHelper = decoratorHelper;
|
||||
if (decoratorHelper.widget && decoratorHelper.widget.currentUser) {
|
||||
this.currentUser = decoratorHelper.widget.currentUser;
|
||||
}
|
||||
}
|
||||
|
||||
this.decoratorHelper = decoratorHelper;
|
||||
this.currentUser = currentUser;
|
||||
this.ignoredUsers = this.currentUser
|
||||
? this.currentUser.ignored_users
|
||||
: null;
|
||||
@ -151,7 +145,11 @@ export default class PostCooked {
|
||||
const $blockQuote = $("> blockquote", $aside);
|
||||
$aside.data("original-contents", $blockQuote.html());
|
||||
|
||||
const originalText = $blockQuote.text().trim();
|
||||
const originalText =
|
||||
$blockQuote.text().trim() ||
|
||||
$("> blockquote", this.attrs.cooked)
|
||||
.text()
|
||||
.trim();
|
||||
$blockQuote.html(I18n.t("loading"));
|
||||
let topicId = this.attrs.topicId;
|
||||
if ($aside.data("topic")) {
|
||||
@ -229,7 +227,7 @@ export default class PostCooked {
|
||||
.trim()
|
||||
.slice(0, -1);
|
||||
if (username.length > 0 && this.ignoredUsers.includes(username)) {
|
||||
$aside.find("p").replaceWith(`<i>${I18n.t("post.ignored")}</i>`);
|
||||
$aside.find("p").remove();
|
||||
}
|
||||
}
|
||||
$(".quote-controls", $aside).html(expandContract + navLink);
|
||||
@ -264,6 +262,7 @@ export default class PostCooked {
|
||||
|
||||
_computeCooked() {
|
||||
if (
|
||||
this.attrs.embeddedPost &&
|
||||
this.ignoredUsers &&
|
||||
this.ignoredUsers.length > 0 &&
|
||||
this.ignoredUsers.includes(this.attrs.username)
|
||||
|
||||
@ -355,7 +355,9 @@ createWidget("post-contents", {
|
||||
},
|
||||
|
||||
html(attrs, state) {
|
||||
let result = [new PostCooked(attrs, new DecoratorHelper(this))];
|
||||
let result = [
|
||||
new PostCooked(attrs, new DecoratorHelper(this), this.currentUser)
|
||||
];
|
||||
result = result.concat(applyDecorators(this, "after-cooked", attrs, state));
|
||||
|
||||
if (attrs.cooked_hidden) {
|
||||
@ -432,13 +434,7 @@ createWidget("post-notice", {
|
||||
tagName: "div.post-notice",
|
||||
|
||||
buildClasses(attrs) {
|
||||
const classes = [];
|
||||
|
||||
if (attrs.postNoticeType === "first") {
|
||||
classes.push("new-user");
|
||||
} else if (attrs.postNoticeType === "returning") {
|
||||
classes.push("returning-user");
|
||||
}
|
||||
const classes = [attrs.noticeType.replace(/_/g, "-")];
|
||||
|
||||
if (
|
||||
new Date() - new Date(attrs.created_at) >
|
||||
@ -456,13 +452,16 @@ createWidget("post-notice", {
|
||||
? attrs.username
|
||||
: attrs.name;
|
||||
let text, icon;
|
||||
if (attrs.postNoticeType === "first") {
|
||||
if (attrs.noticeType === "custom") {
|
||||
icon = "user-shield";
|
||||
text = attrs.noticeMessage;
|
||||
} else if (attrs.noticeType === "new_user") {
|
||||
icon = "hands-helping";
|
||||
text = I18n.t("post.notice.first", { user });
|
||||
} else if (attrs.postNoticeType === "returning") {
|
||||
text = I18n.t("post.notice.new_user", { user });
|
||||
} else if (attrs.noticeType === "returning_user") {
|
||||
icon = "far-smile";
|
||||
const distance = (new Date() - new Date(attrs.postNoticeTime)) / 1000;
|
||||
text = I18n.t("post.notice.return", {
|
||||
const distance = (new Date() - new Date(attrs.noticeTime)) / 1000;
|
||||
text = I18n.t("post.notice.returning_user", {
|
||||
user,
|
||||
time: durationTiny(distance, { addAgo: true })
|
||||
});
|
||||
@ -550,7 +549,7 @@ createWidget("post-article", {
|
||||
);
|
||||
}
|
||||
|
||||
if (attrs.postNoticeType) {
|
||||
if (attrs.noticeType) {
|
||||
rows.push(h("div.row", [this.attach("post-notice", attrs)]));
|
||||
}
|
||||
|
||||
@ -657,14 +656,6 @@ export default createWidget("post", {
|
||||
} else {
|
||||
classNames.push("regular");
|
||||
}
|
||||
if (
|
||||
this.currentUser &&
|
||||
this.currentUser.ignored_users &&
|
||||
this.currentUser.ignored_users.length > 0 &&
|
||||
this.currentUser.ignored_users.includes(attrs.username)
|
||||
) {
|
||||
classNames.push("post-ignored");
|
||||
}
|
||||
if (addPostClassesCallbacks) {
|
||||
for (let i = 0; i < addPostClassesCallbacks.length; i++) {
|
||||
let pluginClasses = addPostClassesCallbacks[i].call(this, attrs);
|
||||
|
||||
@ -199,7 +199,7 @@ export default createWidget("search-menu", {
|
||||
});
|
||||
},
|
||||
|
||||
clickOutside() {
|
||||
mouseDownOutside() {
|
||||
this.sendWidgetAction("toggleSearchMenu");
|
||||
},
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
WidgetClickOutsideHook,
|
||||
WidgetKeyUpHook,
|
||||
WidgetKeyDownHook,
|
||||
WidgetMouseDownOutsideHook,
|
||||
WidgetDragHook
|
||||
} from "discourse/widgets/hooks";
|
||||
import { h } from "virtual-dom";
|
||||
@ -84,6 +85,13 @@ function drawWidget(builder, attrs, state) {
|
||||
if (this.click) {
|
||||
properties["widget-click"] = new WidgetClickHook(this);
|
||||
}
|
||||
|
||||
if (this.mouseDownOutside) {
|
||||
properties["widget-mouse-down-outside"] = new WidgetMouseDownOutsideHook(
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
if (this.drag) {
|
||||
properties["widget-drag"] = new WidgetDragHook(this);
|
||||
}
|
||||
|
||||
@ -16,3 +16,4 @@
|
||||
//= require ./pretty-text/engines/discourse-markdown/text-post-process
|
||||
//= require ./pretty-text/engines/discourse-markdown/image-protocol
|
||||
//= require ./pretty-text/engines/discourse-markdown/inject-line-number
|
||||
//= require ./pretty-text/engines/discourse-markdown/d-wrap
|
||||
|
||||
@ -44,7 +44,7 @@ function addHashtag(buffer, matches, state) {
|
||||
export function setup(helper) {
|
||||
helper.registerPlugin(md => {
|
||||
const rule = {
|
||||
matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w-:]{1,101})/,
|
||||
matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})/,
|
||||
onMatch: addHashtag
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
import { parseBBCodeTag } from "pretty-text/engines/discourse-markdown/bbcode-block";
|
||||
|
||||
const WRAP_CLASS = "d-wrap";
|
||||
|
||||
function parseAttributes(tagInfo) {
|
||||
const attributes = tagInfo.attrs._default || "";
|
||||
|
||||
return (
|
||||
parseBBCodeTag(`[wrap wrap=${attributes}]`, 0, attributes.length + 12)
|
||||
.attrs || {}
|
||||
);
|
||||
}
|
||||
|
||||
function applyDataAttributes(token, state, attributes) {
|
||||
Object.keys(attributes).forEach(tag => {
|
||||
const value = state.md.utils.escapeHtml(attributes[tag]);
|
||||
tag = state.md.utils.escapeHtml(tag.replace(/[^a-z0-9\-]/g, ""));
|
||||
|
||||
if (value && tag && tag.length > 1) {
|
||||
token.attrs.push([`data-${tag}`, value]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const blockRule = {
|
||||
tag: "wrap",
|
||||
|
||||
before(state, tagInfo) {
|
||||
let token = state.push("wrap_open", "div", 1);
|
||||
token.attrs = [["class", WRAP_CLASS]];
|
||||
|
||||
applyDataAttributes(token, state, parseAttributes(tagInfo));
|
||||
},
|
||||
|
||||
after(state) {
|
||||
state.push("wrap_close", "div", -1);
|
||||
}
|
||||
};
|
||||
|
||||
const inlineRule = {
|
||||
tag: "wrap",
|
||||
|
||||
replace(state, tagInfo, content) {
|
||||
let token = state.push("wrap_open", "span", 1);
|
||||
token.attrs = [["class", WRAP_CLASS]];
|
||||
|
||||
applyDataAttributes(token, state, parseAttributes(tagInfo));
|
||||
|
||||
if (content) {
|
||||
token = state.push("text", "", 0);
|
||||
token.content = content;
|
||||
}
|
||||
|
||||
token = state.push("wrap_close", "span", -1);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export function setup(helper) {
|
||||
helper.registerPlugin(md => {
|
||||
md.inline.bbcode.ruler.push("inline-wrap", inlineRule);
|
||||
md.block.bbcode.ruler.push("block-wrap", blockRule);
|
||||
});
|
||||
|
||||
helper.whiteList([`div.${WRAP_CLASS}`, `span.${WRAP_CLASS}`, "span[data-*]"]);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user