Version bump

This commit is contained in:
Joffrey JAFFEUX 2019-04-24 16:23:00 +02:00
commit f5416a987f
641 changed files with 25684 additions and 6349 deletions

19
Gemfile
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,6 @@ const SCSS_VARIABLE_NAMES = [
"facebook",
"cas",
"twitter",
"yahoo",
"github",
"base-font-size",
"base-line-height",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ export default Ember.Component.extend({
$("html").off("keydown.discourse-modal");
},
click(e) {
mouseDown(e) {
if (!this.get("dismissable")) {
return;
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
showUsername: Ember.computed.gte("index", 1)
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
import { applyFlaggedProperties } from "discourse/components/site-header";
export default {
name: "apply-flagged-properties",
initialize: applyFlaggedProperties
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -445,6 +445,9 @@ export default {
$selected = $articles
.toArray()
.find(article => article.getBoundingClientRect().top > offset);
if (!$selected) {
$selected = $articles[$articles.length - 1];
}
direction = 0;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,3 +33,7 @@
{{outlet "modal"}}
{{topic-entrance}}
{{outlet "composer"}}
{{#if showFooterNav}}
{{footer-nav}}
{{/if}}

View File

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

View File

@ -16,4 +16,6 @@
disabled=loading
icon="user-plus"
label="groups.request"}}
{{else}}
{{yield}}
{{/if}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -199,7 +199,7 @@ export default createWidget("search-menu", {
});
},
clickOutside() {
mouseDownOutside() {
this.sendWidgetAction("toggleSearchMenu");
},

View File

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

View File

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

View File

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

View File

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