Version bump
This commit is contained in:
commit
f55439e33e
@ -21,7 +21,7 @@ env:
|
||||
|
||||
addons:
|
||||
chrome: stable
|
||||
postgresql: 9.6
|
||||
postgresql: "9.6"
|
||||
apt:
|
||||
update: true
|
||||
packages:
|
||||
@ -40,10 +40,9 @@ services:
|
||||
- redis-server
|
||||
|
||||
sudo: required
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
|
||||
cache:
|
||||
apt: true
|
||||
yarn: true
|
||||
directories:
|
||||
- vendor/bundle
|
||||
|
||||
18
Gemfile
18
Gemfile
@ -16,13 +16,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.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 'actionmailer', '6.0.0'
|
||||
gem 'actionpack', '6.0.0'
|
||||
gem 'actionview', '6.0.0'
|
||||
gem 'activemodel', '6.0.0'
|
||||
gem 'activerecord', '6.0.0'
|
||||
gem 'activesupport', '6.0.0'
|
||||
gem 'railties', '6.0.0'
|
||||
gem 'sprockets-rails'
|
||||
end
|
||||
|
||||
@ -46,7 +46,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.9.12'
|
||||
gem 'onebox', '1.9.13'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -140,7 +140,7 @@ group :test, :development do
|
||||
gem 'mocha', require: false
|
||||
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 'rspec-rails', '4.0.0.beta2', require: false
|
||||
gem 'shoulda-matchers', '~> 3.1', '>= 3.1.3', require: false
|
||||
gem 'rspec-html-matchers'
|
||||
gem 'pry-nav'
|
||||
|
||||
94
Gemfile.lock
94
Gemfile.lock
@ -1,47 +1,46 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activejob (= 5.2.3)
|
||||
actionmailer (6.0.0)
|
||||
actionpack (= 6.0.0)
|
||||
actionview (= 6.0.0)
|
||||
activejob (= 6.0.0)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
actionpack (6.0.0)
|
||||
actionview (= 6.0.0)
|
||||
activesupport (= 6.0.0)
|
||||
rack (~> 2.0)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actionview (6.0.0)
|
||||
activesupport (= 6.0.0)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
activejob (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
activejob (6.0.0)
|
||||
activesupport (= 6.0.0)
|
||||
globalid (>= 0.3.6)
|
||||
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.3)
|
||||
activemodel (6.0.0)
|
||||
activesupport (= 6.0.0)
|
||||
activerecord (6.0.0)
|
||||
activemodel (= 6.0.0)
|
||||
activesupport (= 6.0.0)
|
||||
activesupport (6.0.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.1, >= 2.1.8)
|
||||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
annotate (2.7.5)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 13.0)
|
||||
arel (9.0.0)
|
||||
ast (2.4.0)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.154.0)
|
||||
@ -183,11 +182,11 @@ GEM
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.9.2)
|
||||
mini_mime (1.0.1)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
mini_racer (0.2.6)
|
||||
libv8 (>= 6.9.411)
|
||||
mini_scheduler (0.12.1)
|
||||
mini_scheduler (0.12.2)
|
||||
sidekiq
|
||||
mini_sql (0.2.2)
|
||||
mini_suffix (0.3.0)
|
||||
@ -241,7 +240,7 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.9.12)
|
||||
onebox (1.9.13)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -282,20 +281,20 @@ GEM
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
rails-html-sanitizer (1.2.0)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails_multisite (2.0.7)
|
||||
activerecord (> 4.2, < 7)
|
||||
railties (> 4.2, < 7)
|
||||
railties (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
railties (6.0.0)
|
||||
actionpack (= 6.0.0)
|
||||
activesupport (= 6.0.0)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
raindrops (0.19.0)
|
||||
rake (12.3.2)
|
||||
rake (12.3.3)
|
||||
rake-compiler (1.0.7)
|
||||
rake
|
||||
rb-fsevent (0.10.3)
|
||||
@ -330,14 +329,14 @@ GEM
|
||||
rspec-mocks (3.8.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-rails (3.8.2)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
rspec-core (~> 3.8.0)
|
||||
rspec-expectations (~> 3.8.0)
|
||||
rspec-mocks (~> 3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-rails (4.0.0.beta2)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
railties (>= 4.2)
|
||||
rspec-core (~> 3.8)
|
||||
rspec-expectations (~> 3.8)
|
||||
rspec-mocks (~> 3.8)
|
||||
rspec-support (~> 3.8)
|
||||
rspec-support (3.8.0)
|
||||
rtlit (0.0.5)
|
||||
rubocop (0.69.0)
|
||||
@ -417,18 +416,19 @@ GEM
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
yaml-lint (0.0.10)
|
||||
zeitwerk (2.1.10)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
actionmailer (= 5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
actionmailer (= 6.0.0)
|
||||
actionpack (= 6.0.0)
|
||||
actionview (= 6.0.0)
|
||||
active_model_serializers (~> 0.8.3)
|
||||
activemodel (= 5.2.3)
|
||||
activerecord (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
activemodel (= 6.0.0)
|
||||
activerecord (= 6.0.0)
|
||||
activesupport (= 6.0.0)
|
||||
annotate
|
||||
aws-sdk-s3
|
||||
aws-sdk-sns
|
||||
@ -493,7 +493,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.9.12)
|
||||
onebox (= 1.9.13)
|
||||
openid-redis-store
|
||||
parallel_tests
|
||||
pg
|
||||
@ -504,7 +504,7 @@ DEPENDENCIES
|
||||
rack-mini-profiler
|
||||
rack-protection
|
||||
rails_multisite
|
||||
railties (= 5.2.3)
|
||||
railties (= 6.0.0)
|
||||
rake
|
||||
rb-fsevent
|
||||
rb-inotify (~> 0.9)
|
||||
@ -517,7 +517,7 @@ DEPENDENCIES
|
||||
rqrcode
|
||||
rspec
|
||||
rspec-html-matchers
|
||||
rspec-rails
|
||||
rspec-rails (= 4.0.0.beta2)
|
||||
rtlit
|
||||
rubocop
|
||||
ruby-prof
|
||||
|
||||
@ -63,7 +63,7 @@ Backup.reopenClass({
|
||||
bootbox.alert(result.message);
|
||||
} else {
|
||||
// redirect to homepage (session might be lost)
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
window.location = Discourse.getURL("/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import round from "discourse/lib/round";
|
||||
import { fillMissingDates, formatUsername } from "discourse/lib/utilities";
|
||||
import {
|
||||
fillMissingDates,
|
||||
formatUsername,
|
||||
toNumber
|
||||
} from "discourse/lib/utilities";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { number, durationTiny } from "discourse/lib/formatter";
|
||||
import { renderAvatar } from "discourse/helpers/user-avatar";
|
||||
@ -374,14 +378,14 @@ const Report = Discourse.Model.extend({
|
||||
|
||||
_secondsLabel(value) {
|
||||
return {
|
||||
value,
|
||||
value: toNumber(value),
|
||||
formatedValue: durationTiny(value)
|
||||
};
|
||||
},
|
||||
|
||||
_percentLabel(value) {
|
||||
return {
|
||||
value,
|
||||
value: toNumber(value),
|
||||
formatedValue: value ? `${value}%` : "—"
|
||||
};
|
||||
},
|
||||
@ -394,14 +398,14 @@ const Report = Discourse.Model.extend({
|
||||
const formatedValue = () => (formatNumbers ? number(value) : value);
|
||||
|
||||
return {
|
||||
value,
|
||||
value: toNumber(value),
|
||||
formatedValue: value ? formatedValue() : "—"
|
||||
};
|
||||
},
|
||||
|
||||
_bytesLabel(value) {
|
||||
return {
|
||||
value,
|
||||
value: toNumber(value),
|
||||
formatedValue: I18n.toHumanSize(value)
|
||||
};
|
||||
},
|
||||
|
||||
@ -19,16 +19,18 @@ const VersionCheck = Discourse.Model.extend({
|
||||
|
||||
@computed("git_branch", "installed_sha")
|
||||
gitLink(gitBranch, installedSHA) {
|
||||
if (gitBranch) {
|
||||
if (gitBranch && installedSHA) {
|
||||
return `https://github.com/discourse/discourse/compare/${installedSHA}...${gitBranch}`;
|
||||
} else {
|
||||
} else if (installedSHA) {
|
||||
return `https://github.com/discourse/discourse/tree/${installedSHA}`;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("installed_sha")
|
||||
shortSha(installedSHA) {
|
||||
return installedSHA.substr(0, 10);
|
||||
if (installedSHA) {
|
||||
return installedSHA.substr(0, 10);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ export default Discourse.Route.extend({
|
||||
);
|
||||
if (log.operation === "restore") {
|
||||
// redirect to homepage when the restore is done (session might be lost)
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
window.location = Discourse.getURL("/");
|
||||
}
|
||||
} else {
|
||||
this.controllerFor("adminBackupsLogs")
|
||||
|
||||
@ -5,7 +5,12 @@
|
||||
|
||||
<div class="version-number">
|
||||
<h4>{{i18n 'admin.dashboard.installed_version'}}</h4>
|
||||
<h3><a href={{versionCheck.gitLink}} target="_blank">{{dash-if-empty versionCheck.installed_describe}}</a></h3>
|
||||
<h3>{{dash-if-empty versionCheck.installed_version}}</h3>
|
||||
{{#if versionCheck.gitLink}}
|
||||
<div class="sha-link">
|
||||
(<a href={{versionCheck.gitLink}} target="_blank">{{versionCheck.shortSha}}</a>)
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if versionCheck.noCheckPerformed}}
|
||||
|
||||
@ -24,12 +24,16 @@ export default Ember.Mixin.create({
|
||||
Discourse.set("hasFocus", true);
|
||||
|
||||
document.addEventListener("visibilitychange", onchange);
|
||||
document.addEventListener("resume", onchange);
|
||||
document.addEventListener("freeze", onchange);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this._super(...arguments);
|
||||
|
||||
document.removeEventListener("visibilitychange", onchange);
|
||||
document.removeEventListener("resume", onchange);
|
||||
document.removeEventListener("freeze", onchange);
|
||||
|
||||
onchange = undefined;
|
||||
}
|
||||
|
||||
@ -131,6 +131,25 @@ export default Ember.Component.extend(KeyEnterEscape, {
|
||||
$document.on(DRAG_EVENTS, throttledPerformDrag);
|
||||
$document.on(END_EVENTS, endDrag);
|
||||
});
|
||||
|
||||
if (window.visualViewport !== undefined) {
|
||||
this.viewportResize();
|
||||
window.visualViewport.addEventListener("resize", this.viewportResize);
|
||||
}
|
||||
},
|
||||
|
||||
viewportResize() {
|
||||
const composerVH = window.visualViewport.height * 0.01;
|
||||
|
||||
if (window.visualViewport.height !== window.innerHeight) {
|
||||
document.documentElement.classList.add("keyboard-visible");
|
||||
} else {
|
||||
document.documentElement.classList.remove("keyboard-visible");
|
||||
}
|
||||
document.documentElement.style.setProperty(
|
||||
"--composer-vh",
|
||||
`${composerVH}px`
|
||||
);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
@ -155,6 +174,9 @@ export default Ember.Component.extend(KeyEnterEscape, {
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.appEvents.off("composer:resize", this, this.resize);
|
||||
if (window.visualViewport !== undefined) {
|
||||
window.visualViewport.removeEventListener("resize", this.viewportResize);
|
||||
}
|
||||
},
|
||||
|
||||
click() {
|
||||
|
||||
@ -1026,39 +1026,37 @@ export default Ember.Component.extend({
|
||||
Ember.run.debounce(
|
||||
this,
|
||||
() => {
|
||||
const inlineOneboxes = {};
|
||||
const oneboxes = {};
|
||||
const inlineOneboxes = {};
|
||||
|
||||
let oneboxLeft =
|
||||
this.siteSettings.max_oneboxes_per_post -
|
||||
$(
|
||||
`aside.onebox, a.${INLINE_ONEBOX_CSS_CLASS}, a.${LOADING_ONEBOX_CSS_CLASS}`
|
||||
).length;
|
||||
// Oneboxes = `a.onebox` -> `a.onebox-loading` -> `aside.onebox`
|
||||
// Inline Oneboxes = `a.inline-onebox-loading` -> `a.inline-onebox`
|
||||
|
||||
let loadedOneboxes = $preview.find(
|
||||
`aside.onebox, a.${LOADING_ONEBOX_CSS_CLASS}, a.${INLINE_ONEBOX_CSS_CLASS}`
|
||||
).length;
|
||||
|
||||
$preview
|
||||
.find(`a.${INLINE_ONEBOX_LOADING_CSS_CLASS}, a.onebox`)
|
||||
.each((_index, link) => {
|
||||
.find(`a.onebox, a.${INLINE_ONEBOX_LOADING_CSS_CLASS}`)
|
||||
.each((_, link) => {
|
||||
const $link = $(link);
|
||||
const text = $link.text();
|
||||
|
||||
const isInline =
|
||||
$link.attr("class") === INLINE_ONEBOX_LOADING_CSS_CLASS;
|
||||
const m = isInline ? inlineOneboxes : oneboxes;
|
||||
|
||||
const map = isInline ? inlineOneboxes : oneboxes;
|
||||
|
||||
if (oneboxLeft <= 0) {
|
||||
if (map[text] !== undefined) {
|
||||
map[text].push(link);
|
||||
if (loadedOneboxes < this.siteSettings.max_oneboxes_per_post) {
|
||||
if (m[text] === undefined) {
|
||||
m[text] = [];
|
||||
loadedOneboxes++;
|
||||
}
|
||||
m[text].push(link);
|
||||
} else {
|
||||
if (m[text] !== undefined) {
|
||||
m[text].push(link);
|
||||
} else if (isInline) {
|
||||
$link.removeClass(INLINE_ONEBOX_LOADING_CSS_CLASS);
|
||||
}
|
||||
} else {
|
||||
if (!map[text]) {
|
||||
map[text] = [];
|
||||
oneboxLeft--;
|
||||
}
|
||||
|
||||
map[text].push(link);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1072,6 +1070,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
450
|
||||
);
|
||||
|
||||
// Short upload urls need resolution
|
||||
resolveAllShortUrls(ajax);
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ 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;
|
||||
@ -37,7 +36,7 @@ const FooterNavComponent = MountWidget.extend(
|
||||
this.appEvents.on("modal:body-dismissed", this, "_modalOff");
|
||||
}
|
||||
|
||||
if (isiPad()) {
|
||||
if (this.capabilities.isIpadOS) {
|
||||
$("body").addClass("footer-nav-ipad");
|
||||
} else {
|
||||
this.bindScrolling({ name: "footer-nav" });
|
||||
@ -56,7 +55,7 @@ const FooterNavComponent = MountWidget.extend(
|
||||
this.appEvents.off("modal:body-removed", this, "_modalOff");
|
||||
}
|
||||
|
||||
if (isiPad()) {
|
||||
if (this.capabilities.isIpadOS) {
|
||||
$("body").removeClass("footer-nav-ipad");
|
||||
} else {
|
||||
this.unbindScrolling("footer-nav");
|
||||
|
||||
@ -170,7 +170,7 @@ export default Ember.Component.extend({
|
||||
)
|
||||
.on("keydown.share-view", this._boundKeydownHandler);
|
||||
|
||||
this.appEvents.on("share:url", this._shareUrlHandler);
|
||||
this.appEvents.on("share:url", this, "_shareUrlHandler");
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
@ -181,7 +181,7 @@ export default Ember.Component.extend({
|
||||
.off("mousedown.outside-share-link", this._boundMouseDownHandler)
|
||||
.off("keydown.share-view", this._boundKeydownHandler);
|
||||
|
||||
this.appEvents.off("share:url", this._shareUrlHandler);
|
||||
this.appEvents.off("share:url", this, "_shareUrlHandler");
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
@ -362,6 +362,12 @@ export default SiteHeaderComponent;
|
||||
|
||||
export function headerHeight() {
|
||||
const $header = $("header.d-header");
|
||||
|
||||
// Header may not exist in tests (e.g. in the user menu component test).
|
||||
if ($header.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const headerOffset = $header.offset();
|
||||
const headerOffsetTop = headerOffset ? headerOffset.top : 0;
|
||||
return parseInt(
|
||||
|
||||
@ -158,12 +158,17 @@ export default Ember.Component.extend({
|
||||
const offset = window.pageYOffset || $html.scrollTop();
|
||||
const progressHeight = this.site.mobileView
|
||||
? 0
|
||||
: $("#topic-progress").height();
|
||||
: $("#topic-progress").outerHeight();
|
||||
const maximumOffset = $("#topic-bottom").offset().top + progressHeight;
|
||||
const windowHeight = $(window).height();
|
||||
const composerHeight = $("#reply-control").height() || 0;
|
||||
const isDocked = offset >= maximumOffset - windowHeight + composerHeight;
|
||||
const bottom = $("body").height() - maximumOffset;
|
||||
let bottom = $("body").height() - maximumOffset;
|
||||
|
||||
const $iPadFooterNav = $(".footer-nav-ipad .footer-nav");
|
||||
if ($iPadFooterNav && $iPadFooterNav.length > 0) {
|
||||
bottom += $iPadFooterNav.outerHeight();
|
||||
}
|
||||
const wrapperDir = $html.hasClass("rtl") ? "left" : "right";
|
||||
|
||||
if (composerHeight > 0) {
|
||||
@ -175,7 +180,7 @@ export default Ember.Component.extend({
|
||||
this.set("docked", isDocked);
|
||||
|
||||
const $replyArea = $("#reply-control .reply-area");
|
||||
if ($replyArea && $replyArea.length > 0) {
|
||||
if ($replyArea && $replyArea.length > 0 && wrapperDir === "left") {
|
||||
$wrapper.css(wrapperDir, `${$replyArea.offset().left}px`);
|
||||
} else {
|
||||
$wrapper.css(wrapperDir, "1em");
|
||||
|
||||
@ -70,13 +70,16 @@ export default Ember.Component.extend(LoadMore, {
|
||||
} else {
|
||||
Draft.get(item.draft_key)
|
||||
.then(d => {
|
||||
if (d.draft) {
|
||||
composer.open({
|
||||
draft: d.draft,
|
||||
draftKey: item.draft_key,
|
||||
draftSequence: d.draft_sequence
|
||||
});
|
||||
const draft = d.draft || item.data;
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
composer.open({
|
||||
draft,
|
||||
draftKey: item.draft_key,
|
||||
draftSequence: d.draft_sequence
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
|
||||
@ -217,7 +217,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
// On Mobile, Android or iOS always go with full screen
|
||||
if (
|
||||
this.isMobileDevice ||
|
||||
(capabilities && (capabilities.isIOS || capabilities.isAndroid))
|
||||
(capabilities &&
|
||||
(capabilities.isIOS ||
|
||||
capabilities.isAndroid ||
|
||||
capabilities.isSafari))
|
||||
) {
|
||||
fullScreenLogin = true;
|
||||
}
|
||||
@ -346,7 +349,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
$.removeCookie("destination_url");
|
||||
window.location.href = destinationUrl;
|
||||
} else if (window.location.pathname === Discourse.getURL("/login")) {
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
window.location = Discourse.getURL("/");
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
@ -190,7 +190,7 @@ export default Ember.Controller.extend(
|
||||
() => {
|
||||
bootbox.alert(
|
||||
I18n.t("user.deleted_yourself"),
|
||||
() => (window.location.pathname = Discourse.getURL("/"))
|
||||
() => (window.location = Discourse.getURL("/"))
|
||||
);
|
||||
},
|
||||
() => {
|
||||
@ -238,7 +238,7 @@ export default Ember.Controller.extend(
|
||||
if (!token) {
|
||||
const redirect = this.siteSettings.logout_redirect;
|
||||
if (Ember.isEmpty(redirect)) {
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
window.location = Discourse.getURL("/");
|
||||
} else {
|
||||
window.location.href = redirect;
|
||||
}
|
||||
|
||||
@ -690,20 +690,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
},
|
||||
|
||||
jumpToPost(postNumber) {
|
||||
if (this.get("model.postStream.isMegaTopic")) {
|
||||
this._jumpToPostNumber(postNumber);
|
||||
} else {
|
||||
const postStream = this.get("model.postStream");
|
||||
let postId = postStream.findPostIdForPostNumber(postNumber);
|
||||
|
||||
// If we couldn't find the post, find the closest post to it
|
||||
if (!postId) {
|
||||
const closest = postStream.closestPostNumberFor(postNumber);
|
||||
postId = postStream.findPostIdForPostNumber(closest);
|
||||
}
|
||||
|
||||
this._jumpToPostId(postId);
|
||||
}
|
||||
this._jumpToPostNumber(postNumber);
|
||||
},
|
||||
|
||||
jumpTop() {
|
||||
@ -1352,17 +1339,12 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
})
|
||||
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
case "read":
|
||||
case "read": {
|
||||
postStream
|
||||
.triggerChangedPost(data.id, data.updated_at, {
|
||||
preserveCooked: true
|
||||
})
|
||||
.then(() =>
|
||||
refresh({
|
||||
id: data.id,
|
||||
refreshReaders: topic.show_read_indicator
|
||||
})
|
||||
);
|
||||
.triggerReadPost(data.id, data.readers_count)
|
||||
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
}
|
||||
case "revised":
|
||||
case "rebaked": {
|
||||
postStream
|
||||
|
||||
@ -75,7 +75,7 @@ export function categoryLinkHTML(category, options) {
|
||||
registerUnbound("category-link", categoryLinkHTML);
|
||||
|
||||
function defaultCategoryLinkRenderer(category, opts) {
|
||||
let description = get(category, "description_text");
|
||||
let descriptionText = get(category, "description_text");
|
||||
let restricted = get(category, "read_restricted");
|
||||
let url = opts.url
|
||||
? opts.url
|
||||
@ -121,7 +121,7 @@ function defaultCategoryLinkRenderer(category, opts) {
|
||||
'data-drop-close="true" class="' +
|
||||
classNames +
|
||||
'"' +
|
||||
(description ? 'title="' + escapeExpression(description) + '" ' : "") +
|
||||
(descriptionText ? 'title="' + descriptionText + '" ' : "") +
|
||||
">";
|
||||
|
||||
let categoryName = escapeExpression(get(category, "name"));
|
||||
|
||||
@ -3,9 +3,17 @@ import { updateRelativeAge } from "discourse/lib/formatter";
|
||||
// Updates the relative ages of dates on the screen.
|
||||
export default {
|
||||
name: "relative-ages",
|
||||
initialize: function() {
|
||||
setInterval(function() {
|
||||
|
||||
initialize() {
|
||||
this._interval = setInterval(function() {
|
||||
updateRelativeAge($(".relative-date"));
|
||||
}, 60 * 1000);
|
||||
},
|
||||
|
||||
teardown() {
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval);
|
||||
this._interval = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -159,7 +159,7 @@ export function ajax() {
|
||||
if (
|
||||
args.type &&
|
||||
args.type.toUpperCase() !== "GET" &&
|
||||
url !== "/clicks/track" &&
|
||||
url !== Discourse.getURL("/clicks/track") &&
|
||||
!Discourse.Session.currentProp("csrfToken")
|
||||
) {
|
||||
promise = new Ember.RSVP.Promise((resolve, reject) => {
|
||||
|
||||
@ -102,9 +102,9 @@ export default {
|
||||
data.append("url", href);
|
||||
data.append("post_id", postId);
|
||||
data.append("topic_id", topicId);
|
||||
navigator.sendBeacon("/clicks/track", data);
|
||||
navigator.sendBeacon(Discourse.getURL("/clicks/track"), data);
|
||||
} else {
|
||||
trackPromise = ajax("/clicks/track", {
|
||||
trackPromise = ajax(Discourse.getURL("/clicks/track"), {
|
||||
type: "POST",
|
||||
data: {
|
||||
url: href,
|
||||
|
||||
@ -262,7 +262,7 @@ function relativeAgeTinyShowsYear(relativeAgeString) {
|
||||
return relativeAgeString.match(/'[\d]{2}$/);
|
||||
}
|
||||
|
||||
function relativeAgeMediumSpan(distance, leaveAgo) {
|
||||
export function relativeAgeMediumSpan(distance, leaveAgo) {
|
||||
let formatted;
|
||||
const distanceInMinutes = Math.round(distance / 60.0);
|
||||
|
||||
@ -283,14 +283,24 @@ function relativeAgeMediumSpan(distance, leaveAgo) {
|
||||
case distanceInMinutes >= 90 && distanceInMinutes <= 1409:
|
||||
formatted = t("x_hours", { count: Math.round(distanceInMinutes / 60.0) });
|
||||
break;
|
||||
case distanceInMinutes >= 1410 && distanceInMinutes <= 2159:
|
||||
case distanceInMinutes >= 1410 && distanceInMinutes <= 2519:
|
||||
formatted = t("x_days", { count: 1 });
|
||||
break;
|
||||
case distanceInMinutes >= 2160:
|
||||
case distanceInMinutes >= 2520 && distanceInMinutes <= 129599:
|
||||
formatted = t("x_days", {
|
||||
count: Math.round((distanceInMinutes - 720.0) / 1440.0)
|
||||
});
|
||||
break;
|
||||
case distanceInMinutes >= 129600 && distanceInMinutes <= 525599:
|
||||
formatted = t("x_months", {
|
||||
count: Math.round(distanceInMinutes / 43200.0)
|
||||
});
|
||||
break;
|
||||
default:
|
||||
formatted = t("x_years", {
|
||||
count: Math.round(distanceInMinutes / 525600.0)
|
||||
});
|
||||
break;
|
||||
}
|
||||
return formatted || "—";
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ export default function logout(siteSettings, keyValueStore) {
|
||||
|
||||
const redirect = siteSettings.logout_redirect;
|
||||
if (Ember.isEmpty(redirect)) {
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
window.location = Discourse.getURL("/");
|
||||
} else {
|
||||
window.location.href = redirect;
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import debounce from "discourse/lib/debounce";
|
||||
import { isAppleDevice, safariHacksDisabled } from "discourse/lib/utilities";
|
||||
import { safariHacksDisabled } from "discourse/lib/utilities";
|
||||
|
||||
// TODO: remove calcHeight once iOS 13 adoption > 90%
|
||||
// In iOS 13 and up we use visualViewport API to calculate height
|
||||
|
||||
// we can't tell what the actual visible window height is
|
||||
// because we cannot account for the height of the mobile keyboard
|
||||
@ -70,7 +73,9 @@ export function isWorkaroundActive() {
|
||||
|
||||
// per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810
|
||||
function positioningWorkaround($fixedElement) {
|
||||
if (!isAppleDevice() || safariHacksDisabled()) {
|
||||
const caps = Discourse.__container__.lookup("capabilities:main");
|
||||
|
||||
if (!caps.isIOS || caps.isIpadOS || safariHacksDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -89,9 +94,14 @@ function positioningWorkaround($fixedElement) {
|
||||
|
||||
fixedElement.style.position = "";
|
||||
fixedElement.style.top = "";
|
||||
fixedElement.style.height = oldHeight;
|
||||
|
||||
Ember.run.later(() => $(fixedElement).removeClass("no-transition"), 500);
|
||||
if (window.visualViewport === undefined) {
|
||||
fixedElement.style.height = oldHeight;
|
||||
Ember.run.later(
|
||||
() => $(fixedElement).removeClass("no-transition"),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
$(window).scrollTop(originalScrollTop);
|
||||
|
||||
@ -165,10 +175,11 @@ function positioningWorkaround($fixedElement) {
|
||||
|
||||
fixedElement.style.top = "0px";
|
||||
|
||||
const height = calcHeight();
|
||||
fixedElement.style.height = height + "px";
|
||||
|
||||
$(fixedElement).addClass("no-transition");
|
||||
if (window.visualViewport === undefined) {
|
||||
const height = calcHeight();
|
||||
fixedElement.style.height = height + "px";
|
||||
$(fixedElement).addClass("no-transition");
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
this.focus();
|
||||
|
||||
@ -4,6 +4,7 @@ import Category from "discourse/models/category";
|
||||
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
|
||||
import userSearch from "discourse/lib/user-search";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import User from "discourse/models/user";
|
||||
import Post from "discourse/models/post";
|
||||
import Topic from "discourse/models/topic";
|
||||
@ -31,6 +32,7 @@ export function translateResults(results, opts) {
|
||||
}
|
||||
post = Post.create(post);
|
||||
post.set("topic", topicMap[post.topic_id]);
|
||||
post.blurb = emojiUnescape(post.blurb);
|
||||
return post;
|
||||
});
|
||||
|
||||
|
||||
@ -19,6 +19,11 @@
|
||||
return "http://twitter.com/intent/tweet?url=" + encodeURIComponent(link) + "&text=" + encodeURIComponent(title);
|
||||
},
|
||||
|
||||
// If provided, handle by custom javascript rather than default url open
|
||||
clickHandler: function(link, title){
|
||||
alert("Hello!")
|
||||
}
|
||||
|
||||
// If true, opens in a popup of `popupHeight` size. If false it's opened in a new tab
|
||||
shouldOpenInPopup: true,
|
||||
popupHeight: 265
|
||||
@ -48,23 +53,27 @@ export default {
|
||||
},
|
||||
|
||||
shareSource(source, data) {
|
||||
const url = source.generateUrl(data.url, data.title);
|
||||
const options = {
|
||||
menubar: "no",
|
||||
toolbar: "no",
|
||||
resizable: "yes",
|
||||
scrollbars: "yes",
|
||||
width: 600,
|
||||
height: source.popupHeight || 315
|
||||
};
|
||||
const stringOptions = Object.keys(options)
|
||||
.map(k => `${k}=${options[k]}`)
|
||||
.join(",");
|
||||
|
||||
if (source.shouldOpenInPopup) {
|
||||
window.open(url, "", stringOptions);
|
||||
if (source.clickHandler) {
|
||||
source.clickHandler(data.url, data.title);
|
||||
} else {
|
||||
window.open(url, "_blank");
|
||||
const url = source.generateUrl(data.url, data.title);
|
||||
const options = {
|
||||
menubar: "no",
|
||||
toolbar: "no",
|
||||
resizable: "yes",
|
||||
scrollbars: "yes",
|
||||
width: 600,
|
||||
height: source.popupHeight || 315
|
||||
};
|
||||
const stringOptions = Object.keys(options)
|
||||
.map(k => `${k}=${options[k]}`)
|
||||
.join(",");
|
||||
|
||||
if (source.shouldOpenInPopup) {
|
||||
window.open(url, "", stringOptions);
|
||||
} else {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -538,10 +538,8 @@ export function determinePostReplaceSelection({
|
||||
export function isAppleDevice() {
|
||||
// IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
|
||||
// This will apply hack on all iDevices
|
||||
return (
|
||||
navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
|
||||
!navigator.userAgent.match(/Trident/g)
|
||||
);
|
||||
const caps = Discourse.__container__.lookup("capabilities:main");
|
||||
return caps.isIOS && !navigator.userAgent.match(/Trident/g);
|
||||
}
|
||||
|
||||
let iPadDetected = undefined;
|
||||
@ -599,8 +597,12 @@ export function clipboardData(e, canUpload) {
|
||||
return { clipboard, types, canUpload, canPasteHtml };
|
||||
}
|
||||
|
||||
export function toNumber(input) {
|
||||
return typeof input === "number" ? input : parseFloat(input);
|
||||
}
|
||||
|
||||
export function isNumeric(input) {
|
||||
return !isNaN(parseFloat(input)) && isFinite(input);
|
||||
return !isNaN(toNumber(input)) && isFinite(input);
|
||||
}
|
||||
|
||||
export function fillMissingDates(data, startDate, endDate) {
|
||||
|
||||
@ -358,6 +358,17 @@ const Composer = RestModel.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (topicFirstPost) {
|
||||
// user should modify topic template
|
||||
const category = this.category;
|
||||
if (category && category.topic_template) {
|
||||
if (this.reply.trim() === category.topic_template.trim()) {
|
||||
bootbox.alert(I18n.t("composer.error.topic_template_not_modified"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.privateMessage) {
|
||||
// need at least one user when sending a PM
|
||||
return (
|
||||
|
||||
@ -19,7 +19,10 @@ const NavItem = Discourse.Model.extend({
|
||||
displayName(categoryName, name, count) {
|
||||
count = count || 0;
|
||||
|
||||
if (name === "latest" && !Discourse.Site.currentProp("mobileView")) {
|
||||
if (
|
||||
name === "latest" &&
|
||||
(!Discourse.Site.currentProp("mobileView") || this.tagId !== undefined)
|
||||
) {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
|
||||
@ -716,6 +716,19 @@ export default RestModel.extend({
|
||||
return resolved;
|
||||
},
|
||||
|
||||
triggerReadPost(postId, readersCount) {
|
||||
const resolved = Ember.RSVP.Promise.resolve();
|
||||
resolved.then(() => {
|
||||
const post = this.findLoadedPost(postId);
|
||||
if (post && readersCount > post.readers_count) {
|
||||
post.set("readers_count", readersCount);
|
||||
this.storePost(post);
|
||||
}
|
||||
});
|
||||
|
||||
return resolved;
|
||||
},
|
||||
|
||||
postForPostNumber(postNumber) {
|
||||
if (!this.hasPosts) {
|
||||
return;
|
||||
|
||||
@ -35,10 +35,14 @@ export default {
|
||||
caps.canPasteImages = caps.isChrome || caps.isFirefox;
|
||||
}
|
||||
|
||||
caps.isIOS =
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
caps.isIpadOS =
|
||||
ua.indexOf("Mac OS") !== -1 &&
|
||||
!/iPhone|iPod/.test(navigator.userAgent) &&
|
||||
touch;
|
||||
|
||||
caps.isIpadOS = ua.indexOf("Mac OS") !== -1 && touch;
|
||||
caps.isIOS =
|
||||
(/iPhone|iPod/.test(navigator.userAgent) || caps.isIpadOS) &&
|
||||
!window.MSStream;
|
||||
}
|
||||
|
||||
// We consider high res a device with 1280 horizontal pixels. High DPI tablets like
|
||||
|
||||
@ -7,7 +7,7 @@ export default Discourse.Route.extend({
|
||||
const params = this.paramsFor("associate-account");
|
||||
this.replaceWith(`preferences.account`, this.currentUser).then(() =>
|
||||
Ember.run.next(() =>
|
||||
ajax(`/associate/${encodeURIComponent(params.token)}`)
|
||||
ajax(`/associate/${encodeURIComponent(params.token)}.json`)
|
||||
.then(model => showModal("associate-account-confirm", { model }))
|
||||
.catch(popupAjaxError)
|
||||
)
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
|
||||
{{#if model.category_moderators.length}}
|
||||
{{#each model.category_moderators as |cm|}}
|
||||
<section class='about category-moderators'>
|
||||
<section class='about category-moderators moderators-{{cm.category.slug}}'>
|
||||
<h3>{{category-link cm.category}}{{i18n "about.moderators"}}</h3>
|
||||
<div class='users'>
|
||||
{{#each cm.moderators as |m|}}
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
{{#if showInput}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
<a href>{{i18n key}}</a>
|
||||
<a href>
|
||||
{{#if key}}
|
||||
{{i18n key}}
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
{{d-icon icon}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
disabled=reviewableUpdating}}
|
||||
{{else}}
|
||||
{{d-button
|
||||
class=(concat "reviewable-action " (dasherize first.id))
|
||||
class=(concat "reviewable-action " (dasherize first.id) " " first.button_class)
|
||||
icon=first.icon
|
||||
action=(action "perform" first)
|
||||
translatedLabel=first.label
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
{{/unless}}
|
||||
|
||||
{{#if canEdit}}
|
||||
{{#link-to-input onClick=(action "displayEditReason") showInput=showEditReason key="composer.show_edit_reason" class="display-edit-reason"}}
|
||||
{{#link-to-input onClick=(action "displayEditReason") showInput=showEditReason icon="info-circle" class="display-edit-reason"}}
|
||||
{{text-field value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}}
|
||||
{{/link-to-input}}
|
||||
{{/if}}
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
{{/d-section}}
|
||||
|
||||
<section class='user-content user-preferences'>
|
||||
{{plugin-outlet name="above-user-preferences"}}
|
||||
{{plugin-outlet name="above-user-preferences" args=(hash model=model)}}
|
||||
|
||||
<form class="form-vertical">
|
||||
{{outlet}}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
{{plugin-outlet name="above-user-profile" tagName='' args=(hash model=model)}}
|
||||
<div class="container {{if viewingSelf 'viewing-self'}}">
|
||||
{{#d-section class="user-main"}}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
formatUsername
|
||||
} from "discourse/lib/utilities";
|
||||
import hbs from "discourse/widgets/hbs-compiler";
|
||||
import { durationTiny } from "discourse/lib/formatter";
|
||||
import { relativeAgeMediumSpan } from "discourse/lib/formatter";
|
||||
import { prioritizeNameInUx } from "discourse/lib/settings";
|
||||
|
||||
function transformWithCallbacks(post) {
|
||||
@ -472,7 +472,7 @@ createWidget("post-notice", {
|
||||
"p",
|
||||
I18n.t("post.notice.returning_user", {
|
||||
user,
|
||||
time: durationTiny(distance, { addAgo: true })
|
||||
time: relativeAgeMediumSpan(distance, true)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { h } from "virtual-dom";
|
||||
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
|
||||
import UserAction from "discourse/models/user-action";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
import { postUrl } from "discourse/lib/utilities";
|
||||
|
||||
const ICON = "bookmark";
|
||||
|
||||
createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
|
||||
buildKey: () => "quick-access-bookmarks",
|
||||
|
||||
hasMore() {
|
||||
// Always show the button to the bookmarks page.
|
||||
return true;
|
||||
},
|
||||
|
||||
showAllHref() {
|
||||
return `${this.attrs.path}/activity/bookmarks`;
|
||||
},
|
||||
|
||||
emptyStatePlaceholderItem() {
|
||||
return h("li.read", this.state.emptyStatePlaceholderItemText);
|
||||
},
|
||||
|
||||
findNewItems() {
|
||||
return ajax("/user_actions.json", {
|
||||
cache: "false",
|
||||
data: {
|
||||
username: this.currentUser.username,
|
||||
filter: UserAction.TYPES.bookmarks,
|
||||
limit: this.estimateItemLimit(),
|
||||
no_results_help_key: "user_activity.no_bookmarks"
|
||||
}
|
||||
}).then(({ user_actions, no_results_help }) => {
|
||||
// The empty state help text for bookmarks page is localized on the
|
||||
// server.
|
||||
this.state.emptyStatePlaceholderItemText = no_results_help;
|
||||
return user_actions;
|
||||
});
|
||||
},
|
||||
|
||||
itemHtml(bookmark) {
|
||||
return this.attach("quick-access-item", {
|
||||
icon: ICON,
|
||||
href: postUrl(bookmark.slug, bookmark.topic_id, bookmark.post_number),
|
||||
content: bookmark.title,
|
||||
username: bookmark.username
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import { h } from "virtual-dom";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
|
||||
/**
|
||||
* This helper widget tries to enforce a consistent look and behavior for any
|
||||
* item under any quick access panels.
|
||||
*
|
||||
* It accepts the following attributes:
|
||||
* action
|
||||
* actionParam
|
||||
* content
|
||||
* escapedContent
|
||||
* href
|
||||
* icon
|
||||
* read
|
||||
* username
|
||||
*/
|
||||
createWidget("quick-access-item", {
|
||||
tagName: "li",
|
||||
|
||||
buildClasses(attrs) {
|
||||
const result = [];
|
||||
if (attrs.className) {
|
||||
result.push(attrs.className);
|
||||
}
|
||||
if (attrs.read === undefined || attrs.read) {
|
||||
result.push("read");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
html({ icon, href }) {
|
||||
return h("a", { attributes: { href } }, [
|
||||
iconNode(icon),
|
||||
new RawHtml({
|
||||
html: `<div>${this._usernameHtml()}${this._contentHtml()}</div>`
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
||||
click(e) {
|
||||
this.attrs.read = true;
|
||||
if (this.attrs.action) {
|
||||
e.preventDefault();
|
||||
return this.sendWidgetAction(this.attrs.action, this.attrs.actionParam);
|
||||
}
|
||||
},
|
||||
|
||||
_contentHtml() {
|
||||
const content =
|
||||
this.attrs.escapedContent || escapeExpression(this.attrs.content);
|
||||
return emojiUnescape(content);
|
||||
},
|
||||
|
||||
_usernameHtml() {
|
||||
return this.attrs.username ? `<span>${this.attrs.username}</span> ` : "";
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,50 @@
|
||||
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
import { postUrl } from "discourse/lib/utilities";
|
||||
|
||||
const ICON = "notification.private_message";
|
||||
|
||||
function toItem(message) {
|
||||
const lastReadPostNumber = message.last_read_post_number || 0;
|
||||
const nextUnreadPostNumber = Math.min(
|
||||
lastReadPostNumber + 1,
|
||||
message.highest_post_number
|
||||
);
|
||||
|
||||
return {
|
||||
escapedContent: message.fancy_title,
|
||||
href: postUrl(message.slug, message.id, nextUnreadPostNumber),
|
||||
icon: ICON,
|
||||
read: message.last_read_post_number >= message.highest_post_number,
|
||||
username: message.last_poster_username
|
||||
};
|
||||
}
|
||||
|
||||
createWidgetFrom(QuickAccessPanel, "quick-access-messages", {
|
||||
buildKey: () => "quick-access-messages",
|
||||
emptyStatePlaceholderItemKey: "choose_topic.none_found",
|
||||
|
||||
hasMore() {
|
||||
// Always show the button to the messages page for composing, archiving,
|
||||
// etc.
|
||||
return true;
|
||||
},
|
||||
|
||||
showAllHref() {
|
||||
return `${this.attrs.path}/messages`;
|
||||
},
|
||||
|
||||
findNewItems() {
|
||||
return this.store
|
||||
.findFiltered("topicList", {
|
||||
filter: `topics/private-messages/${this.currentUser.username_lower}`
|
||||
})
|
||||
.then(({ topic_list }) => {
|
||||
return topic_list.topics.map(toItem).slice(0, this.estimateItemLimit());
|
||||
});
|
||||
},
|
||||
|
||||
itemHtml(message) {
|
||||
return this.attach("quick-access-item", message);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,55 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
|
||||
|
||||
createWidgetFrom(QuickAccessPanel, "quick-access-notifications", {
|
||||
buildKey: () => "quick-access-notifications",
|
||||
emptyStatePlaceholderItemKey: "notifications.empty",
|
||||
|
||||
markReadRequest() {
|
||||
return ajax("/notifications/mark-read", { method: "PUT" });
|
||||
},
|
||||
|
||||
newItemsLoaded() {
|
||||
if (!this.currentUser.enforcedSecondFactor) {
|
||||
this.currentUser.set("unread_notifications", 0);
|
||||
}
|
||||
},
|
||||
|
||||
itemHtml(notification) {
|
||||
const notificationName = this.site.notificationLookup[
|
||||
notification.notification_type
|
||||
];
|
||||
|
||||
return this.attach(
|
||||
`${notificationName.dasherize()}-notification-item`,
|
||||
notification,
|
||||
{},
|
||||
{ fallbackWidgetName: "default-notification-item" }
|
||||
);
|
||||
},
|
||||
|
||||
findNewItems() {
|
||||
return this._findStaleItemsInStore().refresh();
|
||||
},
|
||||
|
||||
showAllHref() {
|
||||
return `${this.attrs.path}/notifications`;
|
||||
},
|
||||
|
||||
hasUnread() {
|
||||
return this.getItems().filterBy("read", false).length > 0;
|
||||
},
|
||||
|
||||
_findStaleItemsInStore() {
|
||||
return this.store.findStale(
|
||||
"notification",
|
||||
{
|
||||
recent: true,
|
||||
silent: this.currentUser.enforcedSecondFactor,
|
||||
limit: this.estimateItemLimit()
|
||||
},
|
||||
{ cacheKey: "recent-notifications" }
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,143 @@
|
||||
import Session from "discourse/models/session";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { h } from "virtual-dom";
|
||||
import { headerHeight } from "discourse/components/site-header";
|
||||
|
||||
const AVERAGE_ITEM_HEIGHT = 55;
|
||||
|
||||
/**
|
||||
* This tries to enforce a consistent flow of fetching, caching, refreshing,
|
||||
* and rendering for "quick access items".
|
||||
*
|
||||
* There are parts to introducing a new quick access panel:
|
||||
* 1. A user menu link that sends a `quickAccess` action, with a unique `type`.
|
||||
* 2. A `quick-access-${type}` widget, extended from `quick-access-panel`.
|
||||
*/
|
||||
export default createWidget("quick-access-panel", {
|
||||
tagName: "div.quick-access-panel",
|
||||
emptyStatePlaceholderItemKey: "",
|
||||
|
||||
buildKey: () => {
|
||||
throw Error('Cannot attach abstract widget "quick-access-panel".');
|
||||
},
|
||||
|
||||
markReadRequest() {
|
||||
return Ember.RSVP.Promise.resolve();
|
||||
},
|
||||
|
||||
hasUnread() {
|
||||
return false;
|
||||
},
|
||||
|
||||
showAllHref() {
|
||||
return "";
|
||||
},
|
||||
|
||||
hasMore() {
|
||||
return this.getItems().length >= this.estimateItemLimit();
|
||||
},
|
||||
|
||||
findNewItems() {
|
||||
return Ember.RSVP.Promise.resolve([]);
|
||||
},
|
||||
|
||||
newItemsLoaded() {},
|
||||
|
||||
itemHtml(item) {}, // eslint-disable-line no-unused-vars
|
||||
|
||||
emptyStatePlaceholderItem() {
|
||||
if (this.emptyStatePlaceholderItemKey) {
|
||||
return h("li.read", I18n.t(this.emptyStatePlaceholderItemKey));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
defaultState() {
|
||||
return { items: [], loading: false, loaded: false };
|
||||
},
|
||||
|
||||
markRead() {
|
||||
return this.markReadRequest().then(() => {
|
||||
this.refreshNotifications(this.state);
|
||||
});
|
||||
},
|
||||
|
||||
estimateItemLimit() {
|
||||
// Estimate (poorly) the amount of notifications to return.
|
||||
let limit = Math.round(
|
||||
($(window).height() - headerHeight()) / AVERAGE_ITEM_HEIGHT
|
||||
);
|
||||
|
||||
// We REALLY don't want to be asking for negative counts of notifications
|
||||
// less than 5 is also not that useful.
|
||||
if (limit < 5) {
|
||||
limit = 5;
|
||||
} else if (limit > 40) {
|
||||
limit = 40;
|
||||
}
|
||||
|
||||
return limit;
|
||||
},
|
||||
|
||||
refreshNotifications(state) {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getItems().length === 0) {
|
||||
state.loading = true;
|
||||
}
|
||||
|
||||
this.findNewItems()
|
||||
.then(newItems => this.setItems(newItems))
|
||||
.catch(() => this.setItems([]))
|
||||
.finally(() => {
|
||||
state.loading = false;
|
||||
state.loaded = true;
|
||||
this.newItemsLoaded();
|
||||
this.sendWidgetAction("itemsLoaded", {
|
||||
hasUnread: this.hasUnread(),
|
||||
markRead: () => this.markRead()
|
||||
});
|
||||
this.scheduleRerender();
|
||||
});
|
||||
},
|
||||
|
||||
html(attrs, state) {
|
||||
if (!state.loaded) {
|
||||
this.refreshNotifications(state);
|
||||
}
|
||||
|
||||
if (state.loading) {
|
||||
return [h("div.spinner-container", h("div.spinner"))];
|
||||
}
|
||||
|
||||
const items = this.getItems().length
|
||||
? this.getItems().map(item => this.itemHtml(item))
|
||||
: [this.emptyStatePlaceholderItem()];
|
||||
|
||||
if (this.hasMore()) {
|
||||
items.push(
|
||||
h(
|
||||
"li.read.last.show-all",
|
||||
this.attach("link", {
|
||||
title: "view_all",
|
||||
icon: "chevron-down",
|
||||
href: this.showAllHref()
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return [h("ul", items)];
|
||||
},
|
||||
|
||||
getItems() {
|
||||
return Session.currentProp(`${this.key}-items`) || [];
|
||||
},
|
||||
|
||||
setItems(newItems) {
|
||||
Session.currentProp(`${this.key}-items`, newItems);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,91 @@
|
||||
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
|
||||
createWidgetFrom(QuickAccessPanel, "quick-access-profile", {
|
||||
buildKey: () => "quick-access-profile",
|
||||
|
||||
hasMore() {
|
||||
// Never show the button to the full profile page.
|
||||
return false;
|
||||
},
|
||||
|
||||
findNewItems() {
|
||||
return Ember.RSVP.Promise.resolve(this._getItems());
|
||||
},
|
||||
|
||||
itemHtml(item) {
|
||||
return this.attach("quick-access-item", item);
|
||||
},
|
||||
|
||||
_getItems() {
|
||||
const items = this._getDefaultItems();
|
||||
if (this._showToggleAnonymousButton()) {
|
||||
items.push(this._toggleAnonymousButton());
|
||||
}
|
||||
if (this.attrs.showLogoutButton) {
|
||||
items.push(this._logOutButton());
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
_getDefaultItems() {
|
||||
return [
|
||||
{
|
||||
icon: "user",
|
||||
href: `${this.attrs.path}/summary`,
|
||||
content: I18n.t("user.summary.title")
|
||||
},
|
||||
{
|
||||
icon: "stream",
|
||||
href: `${this.attrs.path}/activity`,
|
||||
content: I18n.t("user.activity_stream")
|
||||
},
|
||||
{
|
||||
icon: "envelope",
|
||||
href: `${this.attrs.path}/messages`,
|
||||
content: I18n.t("user.private_messages")
|
||||
},
|
||||
{
|
||||
icon: "cog",
|
||||
href: `${this.attrs.path}/preferences`,
|
||||
content: I18n.t("user.preferences")
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
_toggleAnonymousButton() {
|
||||
if (this.currentUser.is_anonymous) {
|
||||
return {
|
||||
action: "toggleAnonymous",
|
||||
className: "disable-anonymous",
|
||||
content: I18n.t("switch_from_anon"),
|
||||
icon: "ban"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
action: "toggleAnonymous",
|
||||
className: "enable-anonymous",
|
||||
content: I18n.t("switch_to_anon"),
|
||||
icon: "user-secret"
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
_logOutButton() {
|
||||
return {
|
||||
action: "logout",
|
||||
className: "logout",
|
||||
content: I18n.t("user.log_out"),
|
||||
icon: "sign-out-alt"
|
||||
};
|
||||
},
|
||||
|
||||
_showToggleAnonymousButton() {
|
||||
return (
|
||||
(this.siteSettings.allow_anonymous_posting &&
|
||||
this.currentUser.trust_level >=
|
||||
this.siteSettings.anonymous_posting_min_trust_level) ||
|
||||
this.currentUser.is_anonymous
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -3,6 +3,17 @@ import { h } from "virtual-dom";
|
||||
import { formatUsername } from "discourse/lib/utilities";
|
||||
import hbs from "discourse/widgets/hbs-compiler";
|
||||
|
||||
const UserMenuAction = {
|
||||
QUICK_ACCESS: "quickAccess"
|
||||
};
|
||||
|
||||
const QuickAccess = {
|
||||
BOOKMARKS: "bookmarks",
|
||||
MESSAGES: "messages",
|
||||
NOTIFICATIONS: "notifications",
|
||||
PROFILE: "profile"
|
||||
};
|
||||
|
||||
let extraGlyphs;
|
||||
|
||||
export function addUserMenuGlyph(glyph) {
|
||||
@ -15,6 +26,8 @@ createWidget("user-menu-links", {
|
||||
|
||||
profileLink() {
|
||||
const link = {
|
||||
action: UserMenuAction.QUICK_ACCESS,
|
||||
actionParam: QuickAccess.PROFILE,
|
||||
route: "user",
|
||||
model: this.currentUser,
|
||||
className: "user-activity-link",
|
||||
@ -30,8 +43,21 @@ createWidget("user-menu-links", {
|
||||
return link;
|
||||
},
|
||||
|
||||
notificationsGlyph() {
|
||||
return {
|
||||
label: "user.notifications",
|
||||
className: "user-notifications-link",
|
||||
icon: "bell",
|
||||
href: `${this.attrs.path}/notifications`,
|
||||
action: UserMenuAction.QUICK_ACCESS,
|
||||
actionParam: QuickAccess.NOTIFICATIONS
|
||||
};
|
||||
},
|
||||
|
||||
bookmarksGlyph() {
|
||||
return {
|
||||
action: UserMenuAction.QUICK_ACCESS,
|
||||
actionParam: QuickAccess.BOOKMARKS,
|
||||
label: "user.bookmarks",
|
||||
className: "user-bookmarks-link",
|
||||
icon: "bookmark",
|
||||
@ -41,6 +67,8 @@ createWidget("user-menu-links", {
|
||||
|
||||
messagesGlyph() {
|
||||
return {
|
||||
action: UserMenuAction.QUICK_ACCESS,
|
||||
actionParam: QuickAccess.MESSAGES,
|
||||
label: "user.private_messages",
|
||||
className: "user-pms-link",
|
||||
icon: "envelope",
|
||||
@ -49,24 +77,20 @@ createWidget("user-menu-links", {
|
||||
},
|
||||
|
||||
linkHtml(link) {
|
||||
if (this.isActive(link)) {
|
||||
link = this.markAsActive(link);
|
||||
}
|
||||
return this.attach("link", link);
|
||||
},
|
||||
|
||||
glyphHtml(glyph) {
|
||||
if (this.isActive(glyph)) {
|
||||
glyph = this.markAsActive(glyph);
|
||||
}
|
||||
return this.attach("link", $.extend(glyph, { hideLabel: true }));
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
const { currentUser, siteSettings } = this;
|
||||
|
||||
const isAnon = currentUser.is_anonymous;
|
||||
const allowAnon =
|
||||
(siteSettings.allow_anonymous_posting &&
|
||||
currentUser.trust_level >=
|
||||
siteSettings.anonymous_posting_min_trust_level) ||
|
||||
isAnon;
|
||||
|
||||
const path = attrs.path;
|
||||
html() {
|
||||
const links = [this.profileLink()];
|
||||
const glyphs = [];
|
||||
|
||||
@ -81,42 +105,39 @@ createWidget("user-menu-links", {
|
||||
});
|
||||
}
|
||||
|
||||
glyphs.push(this.notificationsGlyph());
|
||||
glyphs.push(this.bookmarksGlyph());
|
||||
|
||||
if (siteSettings.enable_personal_messages) {
|
||||
if (this.siteSettings.enable_personal_messages) {
|
||||
glyphs.push(this.messagesGlyph());
|
||||
}
|
||||
|
||||
if (allowAnon) {
|
||||
if (!isAnon) {
|
||||
glyphs.push({
|
||||
action: "toggleAnonymous",
|
||||
label: "switch_to_anon",
|
||||
className: "enable-anonymous",
|
||||
icon: "user-secret"
|
||||
});
|
||||
} else {
|
||||
glyphs.push({
|
||||
action: "toggleAnonymous",
|
||||
label: "switch_from_anon",
|
||||
className: "disable-anonymous",
|
||||
icon: "ban"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// preferences always goes last
|
||||
glyphs.push({
|
||||
label: "user.preferences",
|
||||
className: "user-preferences-link",
|
||||
icon: "cog",
|
||||
href: `${path}/preferences`
|
||||
});
|
||||
|
||||
return h("ul.menu-links-row", [
|
||||
links.map(l => h("li.user", this.linkHtml(l))),
|
||||
h("li.glyphs", glyphs.map(l => this.glyphHtml(l)))
|
||||
]);
|
||||
},
|
||||
|
||||
markAsActive(definition) {
|
||||
// Clicking on an active quick access tab icon should redirect the user to
|
||||
// the full page.
|
||||
definition.action = null;
|
||||
definition.actionParam = null;
|
||||
|
||||
if (definition.className) {
|
||||
definition.className += " active";
|
||||
} else {
|
||||
definition.className = "active";
|
||||
}
|
||||
|
||||
return definition;
|
||||
},
|
||||
|
||||
isActive({ action, actionParam }) {
|
||||
return (
|
||||
action === UserMenuAction.QUICK_ACCESS &&
|
||||
actionParam === this.attrs.currentQuickAccess
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -148,6 +169,7 @@ export default createWidget("user-menu", {
|
||||
|
||||
defaultState() {
|
||||
return {
|
||||
currentQuickAccess: QuickAccess.NOTIFICATIONS,
|
||||
hasUnread: false,
|
||||
markUnread: null
|
||||
};
|
||||
@ -155,37 +177,18 @@ export default createWidget("user-menu", {
|
||||
|
||||
panelContents() {
|
||||
const path = this.currentUser.get("path");
|
||||
const { currentQuickAccess } = this.state;
|
||||
|
||||
let result = [
|
||||
this.attach("user-menu-links", { path }),
|
||||
this.attach("user-notifications", { path })
|
||||
const result = [
|
||||
this.attach("user-menu-links", {
|
||||
path,
|
||||
currentQuickAccess
|
||||
}),
|
||||
this.quickAccessPanel(path)
|
||||
];
|
||||
|
||||
if (this.settings.showLogoutButton || this.state.hasUnread) {
|
||||
result.push(h("hr.bottom-area"));
|
||||
}
|
||||
|
||||
if (this.settings.showLogoutButton) {
|
||||
result.push(
|
||||
h("div.logout-link", [
|
||||
h(
|
||||
"ul.menu-links",
|
||||
h(
|
||||
"li",
|
||||
this.attach("link", {
|
||||
action: "logout",
|
||||
className: "logout",
|
||||
icon: "sign-out-alt",
|
||||
href: "",
|
||||
label: "user.log_out"
|
||||
})
|
||||
)
|
||||
)
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.hasUnread) {
|
||||
result.push(h("hr.bottom-area"));
|
||||
result.push(this.attach("user-menu-dismiss-link"));
|
||||
}
|
||||
|
||||
@ -196,8 +199,8 @@ export default createWidget("user-menu", {
|
||||
return this.state.markRead();
|
||||
},
|
||||
|
||||
notificationsLoaded({ notifications, markRead }) {
|
||||
this.state.hasUnread = notifications.filterBy("read", false).length > 0;
|
||||
itemsLoaded({ hasUnread, markRead }) {
|
||||
this.state.hasUnread = hasUnread;
|
||||
this.state.markRead = markRead;
|
||||
},
|
||||
|
||||
@ -234,5 +237,20 @@ export default createWidget("user-menu", {
|
||||
} else {
|
||||
this.sendWidgetAction("toggleUserMenu");
|
||||
}
|
||||
},
|
||||
|
||||
quickAccess(type) {
|
||||
if (this.state.currentQuickAccess !== type) {
|
||||
this.state.currentQuickAccess = type;
|
||||
}
|
||||
},
|
||||
|
||||
quickAccessPanel(path) {
|
||||
const { showLogoutButton } = this.settings;
|
||||
// This deliberately does NOT fallback to a default quick access panel.
|
||||
return this.attach(`quick-access-${this.state.currentQuickAccess}`, {
|
||||
path,
|
||||
showLogoutButton
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { headerHeight } from "discourse/components/site-header";
|
||||
import { h } from "virtual-dom";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
export default createWidget("user-notifications", {
|
||||
tagName: "div.notifications",
|
||||
buildKey: () => "user-notifications",
|
||||
|
||||
defaultState() {
|
||||
return { notifications: [], loading: false, loaded: false };
|
||||
},
|
||||
|
||||
markRead() {
|
||||
ajax("/notifications/mark-read", { method: "PUT" }).then(() => {
|
||||
this.refreshNotifications(this.state);
|
||||
});
|
||||
},
|
||||
|
||||
refreshNotifications(state) {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// estimate (poorly) the amount of notifications to return
|
||||
let limit = Math.round(($(window).height() - headerHeight()) / 55);
|
||||
// we REALLY don't want to be asking for negative counts of notifications
|
||||
// less than 5 is also not that useful
|
||||
if (limit < 5) {
|
||||
limit = 5;
|
||||
}
|
||||
if (limit > 40) {
|
||||
limit = 40;
|
||||
}
|
||||
|
||||
const silent = this.currentUser.get("enforcedSecondFactor");
|
||||
const stale = this.store.findStale(
|
||||
"notification",
|
||||
{ recent: true, silent, limit },
|
||||
{ cacheKey: "recent-notifications" }
|
||||
);
|
||||
|
||||
if (stale.hasResults) {
|
||||
const results = stale.results;
|
||||
let content = results.get("content");
|
||||
|
||||
// we have to truncate to limit, otherwise we will render too much
|
||||
if (content && content.length > limit) {
|
||||
content = content.splice(0, limit);
|
||||
results.set("content", content);
|
||||
results.set("totalRows", limit);
|
||||
}
|
||||
|
||||
state.notifications = results;
|
||||
} else {
|
||||
state.loading = true;
|
||||
}
|
||||
|
||||
stale
|
||||
.refresh()
|
||||
.then(notifications => {
|
||||
if (!silent) {
|
||||
this.currentUser.set("unread_notifications", 0);
|
||||
}
|
||||
state.notifications = notifications;
|
||||
})
|
||||
.catch(() => {
|
||||
state.notifications = [];
|
||||
})
|
||||
.finally(() => {
|
||||
state.loading = false;
|
||||
state.loaded = true;
|
||||
this.sendWidgetAction("notificationsLoaded", {
|
||||
notifications: state.notifications,
|
||||
markRead: () => this.markRead()
|
||||
});
|
||||
this.scheduleRerender();
|
||||
});
|
||||
},
|
||||
|
||||
html(attrs, state) {
|
||||
if (!state.loaded) {
|
||||
this.refreshNotifications(state);
|
||||
}
|
||||
|
||||
const result = [];
|
||||
if (state.loading) {
|
||||
result.push(h("div.spinner-container", h("div.spinner")));
|
||||
} else if (state.notifications.length) {
|
||||
const notificationItems = state.notifications.map(notificationAttrs => {
|
||||
const notificationName = this.site.notificationLookup[
|
||||
notificationAttrs.notification_type
|
||||
];
|
||||
|
||||
return this.attach(
|
||||
`${notificationName.dasherize()}-notification-item`,
|
||||
notificationAttrs,
|
||||
{},
|
||||
{ fallbackWidgetName: "default-notification-item" }
|
||||
);
|
||||
});
|
||||
|
||||
result.push(h("hr"));
|
||||
|
||||
const items = [notificationItems];
|
||||
|
||||
if (notificationItems.length > 5) {
|
||||
items.push(
|
||||
h(
|
||||
"li.read.last.heading.show-all",
|
||||
this.attach("button", {
|
||||
title: "notifications.more",
|
||||
icon: "chevron-down",
|
||||
action: "showAllNotifications",
|
||||
className: "btn"
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
result.push(h("ul", items));
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
showAllNotifications() {
|
||||
DiscourseURL.routeTo(`${this.attrs.path}/notifications`);
|
||||
}
|
||||
});
|
||||
@ -116,7 +116,15 @@ I18n.interpolate = function(message, options) {
|
||||
for (var i = 0; placeholder = matches[i]; i++) {
|
||||
name = placeholder.replace(this.PLACEHOLDER, "$1");
|
||||
|
||||
value = options[name];
|
||||
if (typeof options[name] === "string") {
|
||||
// The dollar sign (`$`) is a special replace pattern, and `$&` inserts
|
||||
// the matched string. Thus dollars signs need to be escaped with the
|
||||
// special pattern `$$`, which inserts a single `$`.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
|
||||
value = options[name].replace(/\$/g, "$$$$");
|
||||
} else {
|
||||
value = options[name];
|
||||
}
|
||||
|
||||
if (!this.isValidNode(options, name)) {
|
||||
value = "[missing " + placeholder + " value]";
|
||||
|
||||
@ -167,6 +167,7 @@ const DEFAULT_LIST = [
|
||||
"iframe[marginheight]",
|
||||
"iframe[marginwidth]",
|
||||
"iframe[width]",
|
||||
"iframe[allowfullscreen]",
|
||||
"img[alt]",
|
||||
"img[height]",
|
||||
"img[title]",
|
||||
|
||||
@ -84,9 +84,9 @@ export default SelectKitRowComponent.extend({
|
||||
},
|
||||
|
||||
@computed("category.description_text")
|
||||
descriptionText(description) {
|
||||
if (description) {
|
||||
return this._formatCategoryDescription(description);
|
||||
descriptionText(descriptionText) {
|
||||
if (descriptionText) {
|
||||
return this._formatCategoryDescription(descriptionText);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -53,6 +53,9 @@
|
||||
&:hover {
|
||||
color: $primary-medium;
|
||||
background: $primary-low;
|
||||
.d-icon {
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -626,10 +626,14 @@
|
||||
flex: 1 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
h4 {
|
||||
h4,
|
||||
.sha-link {
|
||||
font-size: $font-down-2;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.sha-link {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.version-status {
|
||||
display: flex;
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
color: $danger;
|
||||
}
|
||||
.desc {
|
||||
color: dark-light-choose($primary-medium, $secondary-medium);
|
||||
color: $primary-medium;
|
||||
}
|
||||
h3 {
|
||||
font-size: $font-0;
|
||||
|
||||
@ -169,12 +169,23 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.whisper {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.display-edit-reason {
|
||||
display: inline;
|
||||
display: inline-flex;
|
||||
a {
|
||||
display: inline-flex;
|
||||
}
|
||||
.d-icon {
|
||||
padding: 0.3em 0.5em;
|
||||
color: $tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
#edit-reason {
|
||||
margin: 4px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.user-selector,
|
||||
|
||||
@ -147,7 +147,7 @@
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
.notifications {
|
||||
.quick-access-panel {
|
||||
width: 100%;
|
||||
display: table;
|
||||
|
||||
@ -187,6 +187,11 @@
|
||||
padding: 0;
|
||||
> div {
|
||||
overflow: hidden; // clears the text from wrapping below icons
|
||||
|
||||
// Truncate items with more than 2 lines.
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,9 +228,12 @@
|
||||
border-width: 2px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.show-all .btn {
|
||||
.show-all a {
|
||||
width: 100%;
|
||||
padding: 2px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
color: dark-light-choose($primary-medium, $secondary-high);
|
||||
background: blend-primary-secondary(5%);
|
||||
&:hover {
|
||||
@ -237,29 +245,24 @@
|
||||
@include unselectable;
|
||||
}
|
||||
|
||||
.logout-link,
|
||||
.dismiss-link {
|
||||
display: inline-block;
|
||||
}
|
||||
.dismiss-link {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications .logout {
|
||||
padding: 0.25em;
|
||||
&:hover {
|
||||
background-color: $highlight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
div.menu-links-header {
|
||||
width: 100%;
|
||||
display: table;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.5em;
|
||||
.menu-links-row {
|
||||
border-bottom: 1px solid dark-light-choose($primary-low, $secondary-medium);
|
||||
display: flex;
|
||||
|
||||
// Tabs should have "ears".
|
||||
padding: 0 4px;
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -271,6 +274,42 @@ div.menu-links-header {
|
||||
flex-wrap: wrap;
|
||||
text-align: right;
|
||||
max-width: 65%; //IE11
|
||||
|
||||
a {
|
||||
// Expand the click area a bit.
|
||||
padding-left: 0.6em;
|
||||
padding-right: 0.6em;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
// This is to make sure active and inactive tab icons have the same
|
||||
// size. `box-sizing` does not work and I have no idea why.
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
a.active {
|
||||
border: 1px solid dark-light-choose($primary-low, $secondary-medium);
|
||||
border-bottom: 0;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: z("header") + 1; // Higher than .menu-panel
|
||||
width: 100%;
|
||||
height: 0;
|
||||
content: "";
|
||||
border-top: 1px solid $secondary;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,12 +322,24 @@ div.menu-links-header {
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
a.user-activity-link {
|
||||
max-width: 150px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: -0.5em 0;
|
||||
max-width: 130px;
|
||||
|
||||
// `overflow: hidden` on `.user-activity-link` would hide the `::after`
|
||||
// pseudo element (used to create the tab-looking effect). Sets `overflow:
|
||||
// hidden` on the child username label instead.
|
||||
overflow: visible;
|
||||
|
||||
span.d-label {
|
||||
display: block;
|
||||
max-width: 130px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@include breakpoint(mobile-medium) {
|
||||
max-width: 125px;
|
||||
}
|
||||
@ -311,6 +362,6 @@ div.menu-links-header {
|
||||
}
|
||||
|
||||
.d-icon-user {
|
||||
margin-right: 0.2em;
|
||||
margin-right: 0.475em;
|
||||
}
|
||||
}
|
||||
|
||||
@ -464,8 +464,9 @@ aside.onebox.twitterstatus .onebox-body {
|
||||
}
|
||||
}
|
||||
|
||||
// Onebox - Imgur - Album
|
||||
.onebox.imgur-album {
|
||||
// Onebox - Imgur/Flickr - Album
|
||||
.onebox.imgur-album,
|
||||
.onebox.flickr-album {
|
||||
.outer-box {
|
||||
position: absolute;
|
||||
z-index: z("base");
|
||||
|
||||
@ -262,10 +262,6 @@
|
||||
.reviewable-action,
|
||||
.reviewable-action-dropdown {
|
||||
margin-right: 0.5em;
|
||||
|
||||
&.delete-user {
|
||||
@extend .btn-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
padding-bottom: 1em;
|
||||
|
||||
@ -23,28 +23,29 @@
|
||||
animation-name: button-jump-up;
|
||||
width: 145px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin-bottom: 0px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -2em;
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#topic-progress-wrapper {
|
||||
display: flex;
|
||||
.topic-admin-menu-button-container {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: -38px;
|
||||
width: 0px;
|
||||
.widget-button {
|
||||
height: 35px;
|
||||
border-right: 1px solid dark-light-diff($primary, $secondary, 80%, -70%);
|
||||
display: flex;
|
||||
> span {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.topic-admin-popup-menu.right-side {
|
||||
position: relative;
|
||||
right: 50px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: auto;
|
||||
transition: bottom 0.5s;
|
||||
transform: translateZ(
|
||||
0
|
||||
); // iOS11 Rendering bug https://meta.discourse.org/t/wrench-menu-not-disappearing-on-ios/94297
|
||||
|
||||
@ -78,6 +78,10 @@ body.footer-nav-ipad {
|
||||
padding-bottom: 0; // resets safe-area-inset-bottom
|
||||
}
|
||||
|
||||
#reply-control.fullscreen {
|
||||
z-index: z("ipad-header-nav") + 1;
|
||||
}
|
||||
|
||||
&.docked .d-header {
|
||||
margin-top: $footer-nav-height;
|
||||
}
|
||||
|
||||
@ -241,6 +241,9 @@ a.toggle-preview {
|
||||
&.fullscreen {
|
||||
// important needed because of inline styles when height is changed manually with grippie
|
||||
height: 100vh !important;
|
||||
@supports (--custom: property) {
|
||||
height: calc(var(--composer-vh, 1vh) * 100) !important;
|
||||
}
|
||||
z-index: z("header") + 1;
|
||||
.d-editor-preview-wrapper {
|
||||
margin-top: 1%;
|
||||
@ -279,3 +282,9 @@ a.toggle-preview {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen-composer.keyboard-visible {
|
||||
#reply-control.fullscreen {
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,10 +196,10 @@ div.lightbox-wrapper {
|
||||
}
|
||||
}
|
||||
|
||||
.topic-details-flex {
|
||||
.topic-column-wrapper {
|
||||
display: flex;
|
||||
|
||||
.topic-details-column-1 {
|
||||
.topic-column.details-column {
|
||||
flex-direction: column;
|
||||
width: 80%;
|
||||
|
||||
@ -225,7 +225,7 @@ div.lightbox-wrapper {
|
||||
}
|
||||
}
|
||||
|
||||
.topic-details-column-2 {
|
||||
.topic-column.featured-image-column {
|
||||
.topic-featured-image img {
|
||||
max-width: 200px;
|
||||
max-height: 100px;
|
||||
|
||||
@ -21,6 +21,14 @@
|
||||
height: 250px;
|
||||
&.edit-title {
|
||||
height: 100%;
|
||||
height: calc(var(--composer-vh, 1vh) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
html.keyboard-visible &.open {
|
||||
height: calc(var(--composer-vh, 1vh) * 100);
|
||||
.reply-area {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -50,17 +50,12 @@
|
||||
|
||||
#topic-progress-wrapper {
|
||||
position: fixed;
|
||||
width: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: z("timeline");
|
||||
margin-right: 148px;
|
||||
&:not(.docked) {
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.topic-admin-menu-button-container .toggle-admin-menu {
|
||||
height: 43px;
|
||||
}
|
||||
}
|
||||
|
||||
#topic-progress-expanded {
|
||||
|
||||
@ -64,7 +64,7 @@ class ApplicationController < ActionController::Base
|
||||
after_action :remember_theme_id
|
||||
|
||||
def remember_theme_id
|
||||
if @theme_ids.present?
|
||||
if @theme_ids.present? && request.format == "html"
|
||||
Stylesheet::Watcher.theme_id = @theme_ids.first if defined? Stylesheet::Watcher
|
||||
end
|
||||
end
|
||||
|
||||
@ -8,7 +8,7 @@ class EmbedController < ApplicationController
|
||||
skip_before_action :check_xhr, :preload_json, :verify_authenticity_token
|
||||
|
||||
before_action :ensure_embeddable, except: [ :info, :topics ]
|
||||
before_action :get_embeddable_css_class, except: [ :info, :topics ]
|
||||
before_action :prepare_embeddable, except: [ :info ]
|
||||
before_action :ensure_api_request, only: [ :info ]
|
||||
|
||||
layout 'embed'
|
||||
@ -123,10 +123,13 @@ class EmbedController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def get_embeddable_css_class
|
||||
def prepare_embeddable
|
||||
@embeddable_css_class = ""
|
||||
embeddable_host = EmbeddableHost.record_for_url(request.referer)
|
||||
@embeddable_css_class = " class=\"#{embeddable_host.class_name}\"" if embeddable_host.present? && embeddable_host.class_name.present?
|
||||
|
||||
@data_referer = request.referer
|
||||
@data_referer = '*' if SiteSetting.embed_any_origin? && @data_referer.blank?
|
||||
end
|
||||
|
||||
def ensure_api_request
|
||||
|
||||
@ -47,11 +47,16 @@ class MetadataController < ApplicationController
|
||||
}
|
||||
|
||||
logo = SiteSetting.site_manifest_icon_url
|
||||
manifest[:icons] << {
|
||||
src: UrlHelper.absolute(logo),
|
||||
sizes: "512x512",
|
||||
type: MiniMime.lookup_by_filename(logo)&.content_type || "image/png"
|
||||
} if logo
|
||||
if logo
|
||||
icon_entry = {
|
||||
src: UrlHelper.absolute(logo),
|
||||
sizes: "512x512",
|
||||
type: MiniMime.lookup_by_filename(logo)&.content_type || "image/png"
|
||||
}
|
||||
manifest[:icons] << icon_entry.dup
|
||||
icon_entry[:purpose] = "maskable"
|
||||
manifest[:icons] << icon_entry
|
||||
end
|
||||
|
||||
manifest[:short_name] = SiteSetting.short_title if SiteSetting.short_title.present?
|
||||
|
||||
|
||||
@ -4,15 +4,20 @@ class PostReadersController < ApplicationController
|
||||
requires_login
|
||||
|
||||
def index
|
||||
post = Post.includes(topic: %i[allowed_groups]).find(params[:id])
|
||||
read_state = post.topic.allowed_groups.any? { |g| g.publish_read_state? && g.users.include?(current_user) }
|
||||
raise Discourse::InvalidAccess unless read_state
|
||||
post = Post.includes(topic: %i[topic_allowed_groups topic_allowed_users]).find(params[:id])
|
||||
ensure_can_see_readers!(post)
|
||||
|
||||
readers = User
|
||||
.where(staged: false)
|
||||
.where.not(id: post.user_id)
|
||||
.joins(:topic_users)
|
||||
.where.not(topic_users: { last_read_post_number: nil })
|
||||
.where('topic_users.topic_id = ? AND topic_users.last_read_post_number >= ?', post.topic_id, post.post_number)
|
||||
.where.not(id: [current_user.id, post.user_id])
|
||||
|
||||
if post.whisper?
|
||||
non_group_members = post.topic.topic_allowed_users.map(&:user_id)
|
||||
readers = readers.where.not(id: non_group_members)
|
||||
end
|
||||
|
||||
readers = readers.map do |r|
|
||||
{
|
||||
@ -24,4 +29,15 @@ class PostReadersController < ApplicationController
|
||||
|
||||
render_json_dump(post_readers: readers)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_can_see_readers!(post)
|
||||
show_readers = GroupUser
|
||||
.where(user: current_user)
|
||||
.joins(:group)
|
||||
.where(groups: { id: post.topic.topic_allowed_groups.map(&:group_id), publish_read_state: true }).exists?
|
||||
|
||||
raise Discourse::InvalidAccess unless show_readers
|
||||
end
|
||||
end
|
||||
|
||||
@ -4,19 +4,20 @@ class UserActionsController < ApplicationController
|
||||
|
||||
def index
|
||||
params.require(:username)
|
||||
params.permit(:filter, :offset, :acting_username)
|
||||
params.permit(:filter, :offset, :acting_username, :limit)
|
||||
|
||||
user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts))
|
||||
raise Discourse::NotFound unless guardian.can_see_profile?(user)
|
||||
|
||||
offset = [0, params[:offset].to_i].max
|
||||
action_types = (params[:filter] || "").split(",").map(&:to_i)
|
||||
limit = params.fetch(:limit, 30).to_i
|
||||
|
||||
opts = {
|
||||
user_id: user.id,
|
||||
user: user,
|
||||
offset: offset,
|
||||
limit: 30,
|
||||
limit: limit,
|
||||
action_types: action_types,
|
||||
guardian: guardian,
|
||||
ignore_private_messages: params[:filter] ? false : true,
|
||||
|
||||
@ -141,6 +141,10 @@ class UsersController < ApplicationController
|
||||
def username
|
||||
params.require(:new_username)
|
||||
|
||||
if clashing_with_existing_route?(params[:new_username]) || User.reserved_username?(params[:new_username])
|
||||
return render_json_error(I18n.t("login.reserved_username"))
|
||||
end
|
||||
|
||||
user = fetch_user_from_params
|
||||
guardian.ensure_can_edit_username!(user)
|
||||
|
||||
@ -359,7 +363,7 @@ class UsersController < ApplicationController
|
||||
return fail_with("login.email_too_long")
|
||||
end
|
||||
|
||||
if User.reserved_username?(params[:username])
|
||||
if clashing_with_existing_route?(params[:username]) || User.reserved_username?(params[:username])
|
||||
return fail_with("login.reserved_username")
|
||||
end
|
||||
|
||||
@ -1355,4 +1359,18 @@ class UsersController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def clashing_with_existing_route?(username)
|
||||
normalized_username = User.normalize_username(username)
|
||||
http_verbs = %w[GET POST PUT DELETE PATCH]
|
||||
allowed_actions = %w[show update destroy]
|
||||
|
||||
http_verbs.any? do |verb|
|
||||
begin
|
||||
path = Rails.application.routes.recognize_path("/u/#{normalized_username}", method: verb)
|
||||
allowed_actions.exclude?(path[:action])
|
||||
rescue ActionController::RoutingError
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -384,7 +384,7 @@ module ApplicationHelper
|
||||
return "" if erbs.blank?
|
||||
|
||||
result = +""
|
||||
erbs.each { |erb| result << render(file: erb) }
|
||||
erbs.each { |erb| result << render(inline: File.read(erb)) }
|
||||
result.html_safe
|
||||
end
|
||||
|
||||
@ -437,17 +437,14 @@ module ApplicationHelper
|
||||
|
||||
def theme_lookup(name)
|
||||
Theme.lookup_field(theme_ids, mobile_view? ? :mobile : :desktop, name)
|
||||
&.html_safe
|
||||
end
|
||||
|
||||
def theme_translations_lookup
|
||||
Theme.lookup_field(theme_ids, :translations, I18n.locale)
|
||||
&.html_safe
|
||||
end
|
||||
|
||||
def theme_js_lookup
|
||||
Theme.lookup_field(theme_ids, :extra_js, nil)
|
||||
&.html_safe
|
||||
end
|
||||
|
||||
def discourse_stylesheet_link_tag(name, opts = {})
|
||||
|
||||
@ -22,7 +22,7 @@ module Jobs
|
||||
.where(staged: false)
|
||||
.find_each do |user|
|
||||
next unless user.email_confirmed?
|
||||
group.add(user)
|
||||
group.add(user, automatic: true)
|
||||
GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user)
|
||||
end
|
||||
|
||||
|
||||
@ -148,7 +148,7 @@ module Jobs
|
||||
|
||||
if start_raw == post.raw && raw != post.raw
|
||||
changes = { raw: raw, edit_reason: I18n.t("upload.edit_reason") }
|
||||
post.revise(Discourse.system_user, changes, bypass_bump: true)
|
||||
post.revise(Discourse.system_user, changes, bypass_bump: true, skip_staff_log: true)
|
||||
elsif has_downloaded_image || has_new_large_image || has_new_broken_image
|
||||
post.trigger_post_process(bypass_bump: true)
|
||||
end
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_dependency 'flag_query'
|
||||
|
||||
module Jobs
|
||||
|
||||
class PendingReviewablesReminder < Jobs::Scheduled
|
||||
|
||||
@ -34,7 +34,7 @@ module Jobs
|
||||
|
||||
if SiteSetting.pop3_polling_ssl
|
||||
if SiteSetting.pop3_polling_openssl_verify
|
||||
pop3.enable_ssl
|
||||
pop3.enable_ssl(max_version: OpenSSL::SSL::TLS1_2_VERSION)
|
||||
else
|
||||
pop3.enable_ssl(OpenSSL::SSL::VERIFY_NONE)
|
||||
end
|
||||
|
||||
@ -3,15 +3,33 @@
|
||||
class Jobs::ReviewablePriorities < Jobs::Scheduled
|
||||
every 1.day
|
||||
|
||||
def execute(args)
|
||||
# We need this many reviewables before we'll calculate priorities
|
||||
def self.min_reviewables
|
||||
15
|
||||
end
|
||||
|
||||
# We calculate the percentiles here for medium and high. Low is always 0 (all)
|
||||
res = DB.query_single(<<~SQL)
|
||||
# We want to look at scores for items with this many reviewables (flags) attached
|
||||
def self.target_count
|
||||
2
|
||||
end
|
||||
|
||||
def execute(args)
|
||||
return unless Reviewable.where('score > 0').count >= self.class.min_reviewables
|
||||
|
||||
res = DB.query_single(<<~SQL, target_count: self.class.target_count)
|
||||
SELECT COALESCE(PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY score), 0.0) AS medium,
|
||||
COALESCE(PERCENTILE_DISC(0.85) WITHIN GROUP (ORDER BY score), 0.0) AS high
|
||||
FROM reviewables
|
||||
FROM (
|
||||
SELECT r.score
|
||||
FROM reviewables AS r
|
||||
INNER JOIN reviewable_scores AS rs ON rs.reviewable_id = r.id
|
||||
GROUP BY r.id
|
||||
HAVING COUNT(*) >= :target_count
|
||||
) AS x
|
||||
SQL
|
||||
|
||||
return unless res && res.size == 2
|
||||
|
||||
medium, high = res
|
||||
|
||||
Reviewable.set_priorities(medium: medium, high: high)
|
||||
|
||||
@ -261,7 +261,8 @@ class Category < ActiveRecord::Base
|
||||
|
||||
@@cache ||= LruRedux::ThreadSafeCache.new(1000)
|
||||
@@cache.getset(self.description) do
|
||||
Nokogiri::HTML.fragment(self.description).text.strip.html_safe
|
||||
text = Nokogiri::HTML.fragment(self.description).text.strip
|
||||
Rack::Utils.escape_html(text).html_safe
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -65,7 +65,8 @@ class DirectoryItem < ActiveRecord::Base
|
||||
0
|
||||
FROM users u
|
||||
LEFT JOIN directory_items di ON di.user_id = u.id AND di.period_type = :period_type
|
||||
WHERE di.id IS NULL AND u.id > 0 AND u.silenced_till IS NULL and u.active
|
||||
WHERE di.id IS NULL AND u.id > 0 AND u.silenced_till IS NULL AND u.active
|
||||
#{SiteSetting.must_approve_users ? 'AND u.approved' : ''}
|
||||
", period_type: period_types[period_type]
|
||||
|
||||
# Calculate new values and update records
|
||||
|
||||
@ -613,7 +613,7 @@ class Group < ActiveRecord::Base
|
||||
|
||||
PUBLISH_CATEGORIES_LIMIT = 10
|
||||
|
||||
def add(user, notify: false)
|
||||
def add(user, notify: false, automatic: false)
|
||||
self.users.push(user) unless self.users.include?(user)
|
||||
|
||||
if notify
|
||||
@ -635,12 +635,15 @@ class Group < ActiveRecord::Base
|
||||
Discourse.request_refresh!(user_ids: [user.id])
|
||||
end
|
||||
|
||||
DiscourseEvent.trigger(:user_added_to_group, user, self, automatic: automatic)
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def remove(user)
|
||||
self.group_users.where(user: user).each(&:destroy)
|
||||
user.update_columns(primary_group_id: nil) if user.primary_group_id == self.id
|
||||
DiscourseEvent.trigger(:user_removed_from_group, user, self)
|
||||
end
|
||||
|
||||
def add_owner(user)
|
||||
|
||||
@ -116,10 +116,19 @@ class Post < ActiveRecord::Base
|
||||
}
|
||||
|
||||
scope :have_uploads, -> {
|
||||
where(
|
||||
"(posts.cooked LIKE '%<a %' OR posts.cooked LIKE '%<img %') AND (posts.cooked LIKE ? OR posts.cooked LIKE '%/original/%' OR posts.cooked LIKE '%/optimized/%' OR posts.cooked LIKE '%data-orig-src=%')",
|
||||
"%/uploads/#{RailsMultisite::ConnectionManagement.current_db}/%"
|
||||
)
|
||||
where("
|
||||
(
|
||||
posts.cooked LIKE '%<a %' OR
|
||||
posts.cooked LIKE '%<img %' OR
|
||||
posts.cooked LIKE '%<video %'
|
||||
) AND (
|
||||
posts.cooked LIKE ? OR
|
||||
posts.cooked LIKE '%/original/%' OR
|
||||
posts.cooked LIKE '%/optimized/%' OR
|
||||
posts.cooked LIKE '%data-orig-src=%' OR
|
||||
posts.cooked LIKE '%/uploads/short-url/%'
|
||||
)", "%/uploads/#{RailsMultisite::ConnectionManagement.current_db}/%"
|
||||
)
|
||||
}
|
||||
|
||||
delegate :username, to: :user
|
||||
@ -170,6 +179,11 @@ class Post < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def readers_count
|
||||
read_count = reads - 1 # Excludes poster
|
||||
read_count < 0 ? 0 : read_count
|
||||
end
|
||||
|
||||
def publish_change_to_clients!(type, opts = {})
|
||||
# special failsafe for posts missing topics consistency checks should fix,
|
||||
# but message is safe to skip
|
||||
@ -913,10 +927,14 @@ class Post < ActiveRecord::Base
|
||||
sha1 = Upload.sha1_from_short_url(src)
|
||||
yield(src, nil, sha1)
|
||||
next
|
||||
elsif src.include?("/uploads/short-url/")
|
||||
sha1 = Upload.sha1_from_short_path(src)
|
||||
yield(src, nil, sha1)
|
||||
next
|
||||
end
|
||||
|
||||
next if upload_patterns.none? { |pattern| src =~ pattern }
|
||||
next if Rails.configuration.multisite && src.exclude?(current_db) && src.exclude?("short-url")
|
||||
next if Rails.configuration.multisite && src.exclude?(current_db)
|
||||
|
||||
src = "#{SiteSetting.force_https ? "https" : "http"}:#{src}" if src.start_with?("//")
|
||||
next unless Discourse.store.has_been_uploaded?(src) || (include_local_upload && src =~ /\A\/[^\/]/i)
|
||||
|
||||
@ -9,10 +9,11 @@ class PostAnalyzer
|
||||
@raw = raw
|
||||
@topic_id = topic_id
|
||||
@onebox_urls = []
|
||||
@found_oneboxes = false
|
||||
end
|
||||
|
||||
def found_oneboxes?
|
||||
@onebox_urls.present?
|
||||
@found_oneboxes
|
||||
end
|
||||
|
||||
def has_oneboxes?
|
||||
@ -36,7 +37,9 @@ class PostAnalyzer
|
||||
result = Oneboxer.apply(cooked) do |url|
|
||||
@onebox_urls << url
|
||||
Oneboxer.invalidate(url) if opts[:invalidate_oneboxes]
|
||||
Oneboxer.cached_onebox(url)
|
||||
onebox = Oneboxer.cached_onebox(url)
|
||||
@found_oneboxes = true if onebox.present?
|
||||
onebox
|
||||
end
|
||||
|
||||
cooked = result.to_html if result.changed?
|
||||
@ -126,7 +129,7 @@ class PostAnalyzer
|
||||
|
||||
# How many links are present in the post
|
||||
def link_count
|
||||
raw_links.size
|
||||
raw_links.size + @onebox_urls.size
|
||||
end
|
||||
|
||||
def cooked_stripped
|
||||
|
||||
@ -78,6 +78,12 @@ class Reviewable < ActiveRecord::Base
|
||||
)
|
||||
end
|
||||
|
||||
# This number comes from looking at forums in the wild and what numbers work.
|
||||
# As the site accumulates real data it'll be based on the site activity instead.
|
||||
def self.typical_sensitivity
|
||||
12.5
|
||||
end
|
||||
|
||||
# Generate `pending?`, `rejected?`, etc helper methods
|
||||
statuses.each do |name, id|
|
||||
define_method("#{name}?") { status == id }
|
||||
@ -195,11 +201,13 @@ class Reviewable < ActiveRecord::Base
|
||||
return Float::MAX if sensitivity == 0
|
||||
|
||||
ratio = sensitivity / Reviewable.sensitivity[:low].to_f
|
||||
high = PluginStore.get('reviewables', "priority_#{Reviewable.priorities[:high]}")
|
||||
return (10.0 * scale) if high.nil?
|
||||
high = (
|
||||
PluginStore.get('reviewables', "priority_#{Reviewable.priorities[:high]}") ||
|
||||
typical_sensitivity
|
||||
).to_f
|
||||
|
||||
# We want this to be hard to reach
|
||||
(high.to_f * ratio) * scale
|
||||
((high.to_f * ratio) * scale).truncate(2)
|
||||
end
|
||||
|
||||
def self.sensitivity_score(sensitivity, scale: 1.0)
|
||||
|
||||
@ -269,10 +269,11 @@ protected
|
||||
end
|
||||
end
|
||||
|
||||
def build_action(actions, id, icon:, bundle: nil, client_action: nil, confirm: false)
|
||||
def build_action(actions, id, icon:, button_class: nil, bundle: nil, client_action: nil, confirm: false)
|
||||
actions.add(id, bundle: bundle) do |action|
|
||||
prefix = "reviewables.actions.#{id}"
|
||||
action.icon = icon
|
||||
action.button_class = button_class
|
||||
action.label = "#{prefix}.title"
|
||||
action.description = "#{prefix}.description"
|
||||
action.client_action = client_action
|
||||
|
||||
@ -38,6 +38,7 @@ class ReviewableQueuedPost < Reviewable
|
||||
if pending? && guardian.can_delete_user?(created_by)
|
||||
actions.add(:delete_user) do |action|
|
||||
action.icon = 'trash-alt'
|
||||
action.button_class = 'btn-danger'
|
||||
action.label = 'reviewables.actions.delete_user.title'
|
||||
action.confirm_message = 'reviewables.actions.delete_user.confirm'
|
||||
end
|
||||
|
||||
@ -59,19 +59,14 @@ class ReviewableScore < ActiveRecord::Base
|
||||
user_stat = user&.user_stat
|
||||
return 0.0 if user_stat.blank?
|
||||
|
||||
calc_user_accuracy_bonus(
|
||||
user_stat.flags_agreed,
|
||||
user_stat.flags_disagreed,
|
||||
user_stat.flags_ignored
|
||||
)
|
||||
calc_user_accuracy_bonus(user_stat.flags_agreed, user_stat.flags_disagreed)
|
||||
end
|
||||
|
||||
def self.calc_user_accuracy_bonus(agreed, disagreed, ignored)
|
||||
def self.calc_user_accuracy_bonus(agreed, disagreed)
|
||||
agreed ||= 0
|
||||
disagreed ||= 0
|
||||
ignored ||= 0
|
||||
|
||||
total = (agreed + disagreed + ignored).to_f
|
||||
total = (agreed + disagreed).to_f
|
||||
return 0.0 if total <= 5
|
||||
|
||||
(agreed / total) * 5.0
|
||||
|
||||
@ -112,6 +112,7 @@ class SiteSetting < ActiveRecord::Base
|
||||
end
|
||||
|
||||
WATCHED_SETTINGS ||= [
|
||||
:default_locale,
|
||||
:attachment_content_type_blacklist,
|
||||
:attachment_filename_blacklist,
|
||||
:unicode_username_character_whitelist,
|
||||
|
||||
@ -264,6 +264,7 @@ class Theme < ActiveRecord::Base
|
||||
|
||||
if with_scheme
|
||||
targets.prepend(:desktop, :mobile, :admin)
|
||||
targets.append(*Discourse.find_plugin_css_assets(mobile_view: true, desktop_view: true))
|
||||
Stylesheet::Manager.cache.clear if clear_manager_cache
|
||||
end
|
||||
|
||||
|
||||
@ -138,7 +138,7 @@ class ThemeField < ActiveRecord::Base
|
||||
when "hbs"
|
||||
js_compiler.append_ember_template(filename.sub("discourse/templates/", ""), content)
|
||||
when "raw.hbs"
|
||||
js_compiler.append_raw_template(filename, content)
|
||||
js_compiler.append_raw_template(filename.sub("discourse/templates/", ""), content)
|
||||
else
|
||||
raise ThemeJavascriptCompiler::CompileError.new(I18n.t("themes.compile_error.unrecognized_extension", extension: extension))
|
||||
end
|
||||
@ -343,7 +343,7 @@ class ThemeField < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def compile_scss
|
||||
Stylesheet::Compiler.compile("@import \"common/foundation/variables\"; @import \"theme_variables\"; @import \"theme_field\";",
|
||||
Stylesheet::Compiler.compile("@import \"common/foundation/variables\"; @import \"common/foundation/mixins\"; @import \"theme_variables\"; @import \"theme_field\";",
|
||||
"theme.scss",
|
||||
theme_field: self.value.dup,
|
||||
theme: self.theme
|
||||
|
||||
@ -357,7 +357,7 @@ SQL
|
||||
if topic&.private_message?
|
||||
groups = read_allowed_groups_of(topic)
|
||||
post = Post.find_by(topic_id: topic.id, post_number: last_read_post_number)
|
||||
trigger_post_read_count_update(post, groups)
|
||||
trigger_post_read_count_update(post, groups, last_read_post_number, user_id)
|
||||
update_topic_list_read_indicator(topic, groups, last_read_post_number, user_id, false)
|
||||
end
|
||||
end
|
||||
@ -389,8 +389,9 @@ SQL
|
||||
MessageBus.publish("/private-messages/unread-indicator/#{topic.id}", message, user_ids: groups_to_update.flat_map(&:members))
|
||||
end
|
||||
|
||||
def self.trigger_post_read_count_update(post, groups)
|
||||
def self.trigger_post_read_count_update(post, groups, last_read_post_number, user_id)
|
||||
return if groups.empty?
|
||||
post.publish_change_to_clients!(:read)
|
||||
opts = { readers_count: post.readers_count, reader_id: user_id }
|
||||
post.publish_change_to_clients!(:read, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@ -459,13 +459,6 @@ class PostSerializer < BasicPostSerializer
|
||||
can_review_topic?
|
||||
end
|
||||
|
||||
def readers_count
|
||||
read_count = object.reads - 1 # Exclude logged user
|
||||
read_count -= 1 unless yours
|
||||
|
||||
read_count < 0 ? 0 : read_count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_review_topic?
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ReviewableActionSerializer < ApplicationSerializer
|
||||
attributes :id, :icon, :label, :confirm_message, :description, :client_action
|
||||
attributes :id, :icon, :button_class, :label, :confirm_message, :description, :client_action
|
||||
|
||||
def label
|
||||
I18n.t(object.label)
|
||||
|
||||
@ -14,11 +14,7 @@ class ReviewableScoreExplanationSerializer < ApplicationSerializer
|
||||
)
|
||||
|
||||
def user_accuracy_bonus
|
||||
ReviewableScore.calc_user_accuracy_bonus(
|
||||
object.flags_agreed,
|
||||
object.flags_disagreed,
|
||||
object.flags_ignored
|
||||
)
|
||||
ReviewableScore.calc_user_accuracy_bonus(object.flags_agreed, object.flags_disagreed)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -6,7 +6,7 @@ class InlineUploads
|
||||
PLACEHOLDER = "__replace__"
|
||||
PATH_PLACEHOLDER = "__replace_path__"
|
||||
|
||||
UPLOAD_REGEXP_PATTERN = "/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-zA-Z0-9.]*)"
|
||||
UPLOAD_REGEXP_PATTERN = "/original/(\\dX/(?:\\h/)*\\h{40}[a-zA-Z0-9.]*)(\\?v=\\d+)?"
|
||||
private_constant :UPLOAD_REGEXP_PATTERN
|
||||
|
||||
def self.process(markdown, on_missing: nil)
|
||||
@ -26,7 +26,6 @@ class InlineUploads
|
||||
# Do nothing
|
||||
elsif !(node.children.count == 1 && (node.children[0].name != "img" && node.children[0].children.blank?)) &&
|
||||
!(node.name == "a" && node.children.count > 1 && !node_children_names(node).include?("img"))
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
@ -34,7 +33,7 @@ class InlineUploads
|
||||
if (actual_link = (node.attributes["href"]&.value || node.attributes["src"]&.value))
|
||||
link_occurences << { link: actual_link, is_valid: true }
|
||||
elsif node.name != "p"
|
||||
link_occurences << { link: actual_link, is_valid: false }
|
||||
link_occurences << { link: seen_link, is_valid: false }
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -64,7 +63,7 @@ class InlineUploads
|
||||
]
|
||||
|
||||
if Discourse.store.external?
|
||||
regexps << /((https?:)?#{SiteSetting.Upload.s3_base_url}#{UPLOAD_REGEXP_PATTERN})/
|
||||
regexps << /((?:https?:)?#{SiteSetting.Upload.s3_base_url}#{UPLOAD_REGEXP_PATTERN})/
|
||||
regexps << /(#{SiteSetting.Upload.s3_cdn_url}#{UPLOAD_REGEXP_PATTERN})/
|
||||
end
|
||||
|
||||
@ -72,8 +71,8 @@ class InlineUploads
|
||||
indexes = Set.new
|
||||
|
||||
markdown.scan(/(\n{2,}|\A)#{regexp}$/) do |match|
|
||||
if match[1].present?
|
||||
extension = match[1].split(".")[-1].downcase
|
||||
if match[1].present? && match[2].present?
|
||||
extension = match[2].split(".")[-1].downcase
|
||||
index = $~.offset(2)[0]
|
||||
indexes << index
|
||||
if FileHelper.supported_images.include?(extension)
|
||||
@ -87,36 +86,20 @@ class InlineUploads
|
||||
markdown.scan(/^#{regexp}(\s)/) do |match|
|
||||
if match[0].present?
|
||||
index = $~.offset(0)[0]
|
||||
next if indexes.include?(index)
|
||||
indexes << index
|
||||
|
||||
raw_matches << [
|
||||
match[0],
|
||||
match[0],
|
||||
+"#{Discourse.base_url}#{PATH_PLACEHOLDER}",
|
||||
$~.offset(0)[0]
|
||||
]
|
||||
next if !indexes.add?(index)
|
||||
raw_matches << [match[0], match[0], +"#{Discourse.base_url}#{PATH_PLACEHOLDER}", index]
|
||||
end
|
||||
end
|
||||
|
||||
markdown.scan(/\[[^\[\]]*\]: #{regexp}/) do |match|
|
||||
if match[0].present?
|
||||
index = $~.offset(1)[0]
|
||||
next if indexes.include?(index)
|
||||
indexes << index
|
||||
end
|
||||
indexes.add($~.offset(1)[0]) if match[0].present?
|
||||
end
|
||||
|
||||
markdown.scan(/(([\n\s\)\]\<])+)#{regexp}/) do |match|
|
||||
if matched_uploads(match[2]).present?
|
||||
next if indexes.include?($~.offset(3)[0])
|
||||
|
||||
raw_matches << [
|
||||
match[2],
|
||||
match[2],
|
||||
+"#{Discourse.base_url}#{PATH_PLACEHOLDER}",
|
||||
$~.offset(0)[0]
|
||||
]
|
||||
next if !indexes.add?($~.offset(3)[0])
|
||||
index = $~.offset(0)[0]
|
||||
raw_matches << [match[2], match[2], +"#{Discourse.base_url}#{PATH_PLACEHOLDER}", index]
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -160,7 +143,7 @@ class InlineUploads
|
||||
end
|
||||
end
|
||||
|
||||
markdown.scan(/(__([a-f0-9]{40})__)/) do |match|
|
||||
markdown.scan(/(__(\h{40})__)/) do |match|
|
||||
upload = Upload.find_by(sha1: match[1])
|
||||
markdown = markdown.sub(match[0], upload.short_path)
|
||||
end
|
||||
@ -182,7 +165,7 @@ class InlineUploads
|
||||
end
|
||||
|
||||
def self.match_bbcode_img(markdown, external_src: false)
|
||||
markdown.scan(/(\[img\]\s*([^\[\]\s]+)\s*\[\/img\])/) do |match|
|
||||
markdown.scan(/(\[img\]\s*([^\[\]\s]+)\s*\[\/img\])/i) do |match|
|
||||
if (matched_uploads(match[1]).present? && block_given?) || external_src
|
||||
yield(match[0], match[1], +"", $~.offset(0)[0])
|
||||
end
|
||||
@ -203,7 +186,7 @@ class InlineUploads
|
||||
end
|
||||
|
||||
def self.match_anchor(markdown, external_href: false)
|
||||
markdown.scan(/((<a[^<]+>)([^<\a>]*?)<\/a>)/) do |match|
|
||||
markdown.scan(/((<a[^<]+>)([^<\a>]*?)<\/a>)/i) do |match|
|
||||
node = Nokogiri::HTML::fragment(match[0]).children[0]
|
||||
href = node.attributes["href"]&.value
|
||||
|
||||
@ -219,8 +202,8 @@ class InlineUploads
|
||||
end
|
||||
|
||||
def self.match_img(markdown, external_src: false)
|
||||
markdown.scan(/(([ ]*)<(?!img)[^<>]+\/?>)?([\r\n]*)(([ ]*)<img ([^>\n]+)>([ ]*))([\r\n]*)/) do |match|
|
||||
node = Nokogiri::HTML::fragment(match[3].strip).children[0]
|
||||
markdown.scan(/(<(?!img)[^<>]+\/?>)?(\s*)(<img [^>\n]+>)/i) do |match|
|
||||
node = Nokogiri::HTML::fragment(match[2].strip).children[0]
|
||||
src = node.attributes["src"]&.value
|
||||
|
||||
if src && (matched_uploads(src).present? || external_src)
|
||||
@ -229,50 +212,17 @@ class InlineUploads
|
||||
height = node.attributes["height"]&.value.to_i
|
||||
title = node.attributes["title"]&.value
|
||||
text = "#{text}|#{width}x#{height}" if width > 0 && height > 0
|
||||
after_html_tag = match[0].present?
|
||||
spaces_before = match[1].present? ? match[1][/ +$/].size : 0
|
||||
replacement = +"#{" " * spaces_before}"
|
||||
|
||||
spaces_before =
|
||||
if after_html_tag && !match[0].end_with?("/>")
|
||||
(match[4].length > 0 ? match[4] : " ")
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
replacement = +"#{spaces_before}"
|
||||
|
||||
if after_html_tag && (num_newlines = match[2].length) <= 1
|
||||
replacement.prepend("\n" * (num_newlines == 0 ? 2 : 1))
|
||||
end
|
||||
|
||||
if after_html_tag && !match[0].end_with?("/>") && (num_newlines = match[7].length) <= 1
|
||||
replacement += ("\n" * (num_newlines == 0 ? 2 : 1))
|
||||
end
|
||||
|
||||
match[3].strip! if !after_html_tag
|
||||
|
||||
if (match[1].nil? || match[1].length < 4)
|
||||
if (match[4].nil? || match[4].length < 4)
|
||||
yield(match[3], src, replacement, $~.offset(0)[0]) if block_given?
|
||||
else
|
||||
yield(match[3], src, match[3].sub(src, PATH_PLACEHOLDER), $~.offset(0)[0]) if block_given?
|
||||
end
|
||||
else
|
||||
yield(match[3], src, match[3].sub(src, PATH_PLACEHOLDER), $~.offset(0)[0]) if block_given?
|
||||
end
|
||||
yield(match[2], src, replacement, $~.offset(0)[0]) if block_given?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.matched_uploads(node)
|
||||
matches = []
|
||||
|
||||
base_url = Discourse.base_url.sub(/https?:\/\//, "(https?://)")
|
||||
|
||||
if GlobalSetting.cdn_url
|
||||
cdn_url = GlobalSetting.cdn_url.sub(/https?:\/\//, "(https?://)")
|
||||
end
|
||||
|
||||
db = RailsMultisite::ConnectionManagement.current_db
|
||||
base_url = Discourse.base_url.sub(/https?:\/\//, "(https?://)")
|
||||
|
||||
regexps = [
|
||||
/(upload:\/\/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)/,
|
||||
@ -282,7 +232,12 @@ class InlineUploads
|
||||
/(#{base_url}\/uploads\/#{db}#{UPLOAD_REGEXP_PATTERN})/,
|
||||
]
|
||||
|
||||
regexps << /(#{cdn_url}\/uploads\/#{db}#{UPLOAD_REGEXP_PATTERN})/ if cdn_url
|
||||
if GlobalSetting.cdn_url && (cdn_url = GlobalSetting.cdn_url.sub(/https?:\/\//, "(https?://)"))
|
||||
regexps << /(#{cdn_url}\/uploads\/#{db}#{UPLOAD_REGEXP_PATTERN})/
|
||||
if GlobalSetting.relative_url_root.present?
|
||||
regexps << /(#{cdn_url}#{GlobalSetting.relative_url_root}\/uploads\/#{db}#{UPLOAD_REGEXP_PATTERN})/
|
||||
end
|
||||
end
|
||||
|
||||
if Discourse.store.external?
|
||||
if Rails.configuration.multisite
|
||||
@ -294,6 +249,7 @@ class InlineUploads
|
||||
end
|
||||
end
|
||||
|
||||
matches = []
|
||||
node = node.to_s
|
||||
|
||||
regexps.each do |regexp|
|
||||
|
||||
@ -4,7 +4,8 @@ class UserAuthenticator
|
||||
|
||||
def initialize(user, session, authenticator_finder = Users::OmniauthCallbacksController)
|
||||
@user = user
|
||||
@session = session[:authentication]
|
||||
@session = session
|
||||
@auth_session = session[:authentication]
|
||||
@authenticator_finder = authenticator_finder
|
||||
end
|
||||
|
||||
@ -15,7 +16,7 @@ class UserAuthenticator
|
||||
@user.password_required!
|
||||
end
|
||||
|
||||
@user.skip_email_validation = true if @session && @session[:skip_email_validation].present?
|
||||
@user.skip_email_validation = true if @auth_session && @auth_session[:skip_email_validation].present?
|
||||
end
|
||||
|
||||
def has_authenticator?
|
||||
@ -24,18 +25,18 @@ class UserAuthenticator
|
||||
|
||||
def finish
|
||||
if authenticator
|
||||
authenticator.after_create_account(@user, @session)
|
||||
authenticator.after_create_account(@user, @auth_session)
|
||||
confirm_email
|
||||
end
|
||||
@session = nil
|
||||
@session[:authentication] = @auth_session = nil if @auth_session
|
||||
end
|
||||
|
||||
def email_valid?
|
||||
@session && @session[:email_valid]
|
||||
@auth_session && @auth_session[:email_valid]
|
||||
end
|
||||
|
||||
def authenticated?
|
||||
@session && @session[:email]&.downcase == @user.email.downcase && @session[:email_valid].to_s == "true"
|
||||
@auth_session && @auth_session[:email]&.downcase == @user.email.downcase && @auth_session[:email_valid].to_s == "true"
|
||||
end
|
||||
|
||||
private
|
||||
@ -54,7 +55,7 @@ class UserAuthenticator
|
||||
end
|
||||
|
||||
def authenticator_name
|
||||
@session && @session[:authenticator_name]
|
||||
@auth_session && @auth_session[:authenticator_name]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -12,6 +12,6 @@
|
||||
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
|
||||
<%- end %>
|
||||
|
||||
<%- Discourse.find_plugin_css_assets(include_official: allow_plugins?, include_unofficial: allow_third_party_plugins?, mobile_view: mobile_view?).each do |file| %>
|
||||
<%- Discourse.find_plugin_css_assets(include_official: allow_plugins?, include_unofficial: allow_third_party_plugins?, mobile_view: mobile_view?, desktop_view: !mobile_view?).each do |file| %>
|
||||
<%= discourse_stylesheet_link_tag(file) %>
|
||||
<%- end %>
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
<div class='topic-list-item'>
|
||||
<%- if @template == "complete" %>
|
||||
<div class='main-link'>
|
||||
<div class="topic-details-flex">
|
||||
<div class='topic-details-column-1'>
|
||||
<div class="topic-column-wrapper">
|
||||
<div class='topic-column details-column'>
|
||||
<div class='topic-title-link'>
|
||||
<a target="_parent" href="<%= t.url %>" class="title raw-link raw-topic-link" data-topic-id="<%= t.id %>"><%= t.title %></a>
|
||||
</div>
|
||||
@ -38,7 +38,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='topic-details-column-2'>
|
||||
<div class='topic-column featured-image-column'>
|
||||
<%- if t.image_url.present? %>
|
||||
<div class='topic-featured-image'>
|
||||
<img src="<%= t.image_url %>">
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<html<%= raw @embeddable_css_class -%>>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover">
|
||||
<%= discourse_stylesheet_link_tag 'embed', theme_ids: nil %>
|
||||
<%- unless customization_disabled? %>
|
||||
<%= discourse_stylesheet_link_tag :embedded_theme %>
|
||||
@ -13,7 +13,7 @@
|
||||
<title><%= @topic_view.page_title %> - <%= SiteSetting.title %></title>
|
||||
<%- end %>
|
||||
|
||||
<meta id="data-embedded" data-referer="<%= request.referer %>">
|
||||
<meta id="data-embedded" data-referer="<%= @data_referer %>">
|
||||
<%= preload_script 'embed-application' %>
|
||||
|
||||
<%= yield :head %>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
<div class="dialog">
|
||||
<p>
|
||||
<%=t "login.auth_complete" %>
|
||||
<a href="<%= Discourse.base_url.html_safe %>?authComplete=true"><%= t("login.click_to_continue") %></a>
|
||||
<a href="<%= Discourse.base_url %>?authComplete=true"><%= t("login.click_to_continue") %></a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<div class="user-crawler">
|
||||
<img src='<%= UrlHelper.local_cdn_url(get_absolute_image_url(@user.small_avatar_url)) %>' alt='<%= @user.username %>' title='<%= @user.username %>' />
|
||||
<img src='<%= ::UrlHelper.local_cdn_url(get_absolute_image_url(@user.small_avatar_url)) %>' alt='<%= @user.username %>' title='<%= @user.username %>' />
|
||||
<h2 class='username'><%= @user.username %></h2>
|
||||
</div>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user