diff --git a/Gemfile b/Gemfile index 54797b5cf8c..45af6c03c35 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.79' +gem 'onebox', '1.8.82' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 48f6a2610ca..299c7f91ac8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -261,7 +261,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.79) + onebox (1.8.82) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -515,7 +515,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.79) + onebox (= 1.8.82) openid-redis-store pg pry-nav diff --git a/app/assets/javascripts/admin/models/flagged-post.js.es6 b/app/assets/javascripts/admin/models/flagged-post.js.es6 index fde2cdf01a4..13d69bf6650 100644 --- a/app/assets/javascripts/admin/models/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/models/flagged-post.js.es6 @@ -136,7 +136,9 @@ export default Post.extend({ label: I18n.t("yes_value"), class: "btn-danger", callback() { - Post.deleteMany(replies.map(r => r.id), { deferFlags: true }) + Post.deleteMany(replies.map(r => r.id), { + agreeWithFirstReplyFlag: false + }) .then(action) .then(resolve) .catch(error => { diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6 index 80a9fdc3014..ae47bab740e 100644 --- a/app/assets/javascripts/discourse/components/navigation-item.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6 @@ -47,7 +47,10 @@ export default Ember.Component.extend( this.set("hidden", false); } - buffer.push(``); + buffer.push( + `` + ); + if (content.get("hasIcon")) { buffer.push(""); } diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 8156df8e42b..52deb54fb28 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -902,7 +902,7 @@ export default Ember.Controller.extend({ composerModel.set("composeState", Composer.OPEN); composerModel.set("isWarning", false); - if (opts.usernames) { + if (opts.usernames && !this.get("model.targetUsernames")) { this.set("model.targetUsernames", opts.usernames); } diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 index 07d6cb83fc3..b710c5feb5d 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 @@ -103,6 +103,14 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { saveTimer() { + if (!this.get("topicTimer.updateTime")) { + this.flash( + I18n.t("topic.topic_status_update.time_frame_required"), + "alert-error" + ); + return; + } + this._setTimer( this.get("topicTimer.updateTime"), this.get("topicTimer.status_type") diff --git a/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 index de4f84143b8..c680a311cfa 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 @@ -5,6 +5,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; export default Ember.Controller.extend(PreferencesTabController, { saveAttrNames: [ "muted_usernames", + "ignored_usernames", "new_topic_duration_minutes", "auto_track_topics_after_msecs", "notification_level_when_replying", diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index a157abee6ac..1c4a6c460c8 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -24,7 +24,7 @@ export default Ember.Controller.extend({ this.set("searchTerm", ""); }, - @observes("searchTearm") + @observes("searchTerm") _searchTermChanged: debounce(function() { Invite.findInvitedBy( this.get("user"), @@ -90,7 +90,6 @@ export default Ember.Controller.extend({ Invite.rescindAll() .then(() => { this.set("rescindedAll", true); - this.get("model.invites").clear(); }) .catch(popupAjaxError); } diff --git a/app/assets/javascripts/discourse/lib/search.js.es6 b/app/assets/javascripts/discourse/lib/search.js.es6 index 32c226228d4..28380ddc93d 100644 --- a/app/assets/javascripts/discourse/lib/search.js.es6 +++ b/app/assets/javascripts/discourse/lib/search.js.es6 @@ -46,11 +46,26 @@ export function translateResults(results, opts) { results.groups = results.groups .map(group => { - const groupName = Handlebars.Utils.escapeExpression(group.name); + const name = Handlebars.Utils.escapeExpression(group.name); + const fullName = Handlebars.Utils.escapeExpression( + group.full_name || group.display_name + ); + const flairUrl = Ember.isEmpty(group.flair_url) + ? null + : Handlebars.Utils.escapeExpression(group.flair_url); + const flairColor = Handlebars.Utils.escapeExpression(group.flair_color); + const flairBgColor = Handlebars.Utils.escapeExpression( + group.flair_bg_color + ); + return { id: group.id, - name: groupName, - url: Discourse.getURL(`/g/${groupName}`) + flairUrl, + flairColor, + flairBgColor, + fullName, + name, + url: Discourse.getURL(`/g/${name}`) }; }) .compact(); @@ -72,10 +87,10 @@ export function translateResults(results, opts) { if (groupedSearchResult) { [ ["topic", "posts"], - ["category", "categories"], - ["tag", "tags"], ["user", "users"], - ["group", "groups"] + ["group", "groups"], + ["category", "categories"], + ["tag", "tags"] ].forEach(function(pair) { const type = pair[0]; const name = pair[1]; diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index c0371aa157e..e0d08665ef4 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -80,6 +80,7 @@ export function transformBasicPost(post) { expandablePost: false, replyCount: post.reply_count, locked: post.locked, + ignored: post.ignored, userCustomFields: post.user_custom_fields }; @@ -133,6 +134,13 @@ export default function transformPost( postAtts.topicUrl = topic.get("url"); postAtts.isSaving = post.isSaving; + if (post.post_notice_type) { + postAtts.postNoticeType = post.post_notice_type; + if (postAtts.postNoticeType === "returning") { + postAtts.postNoticeTime = new Date(post.post_notice_time); + } + } + const showPMMap = topic.archetype === "private_message" && post.post_number === 1; if (showPMMap) { diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 8da229cfb27..9202d581e05 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -378,10 +378,10 @@ Post.reopenClass({ }); }, - deleteMany(post_ids, { deferFlags = false } = {}) { + deleteMany(post_ids, { agreeWithFirstReplyFlag = true } = {}) { return ajax("/posts/destroy_many", { type: "DELETE", - data: { post_ids, defer_flags: deferFlags } + data: { post_ids, agree_with_first_reply_flag: agreeWithFirstReplyFlag } }); }, diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 18963d737cb..2e5fa5d2f7f 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -249,6 +249,7 @@ const User = RestModel.extend({ "custom_fields", "user_fields", "muted_usernames", + "ignored_usernames", "profile_background", "card_background", "muted_tags", diff --git a/app/assets/javascripts/discourse/routes/new-message.js.es6 b/app/assets/javascripts/discourse/routes/new-message.js.es6 index abdf17e24bd..70691518829 100644 --- a/app/assets/javascripts/discourse/routes/new-message.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-message.js.es6 @@ -48,6 +48,8 @@ export default Discourse.Route.extend({ } }) .catch(() => bootbox.alert(I18n.t("generic_error"))); + } else { + e.send("createNewMessageViaParams", null, params.title, params.body); } }); } else { diff --git a/app/assets/javascripts/discourse/templates/about.hbs b/app/assets/javascripts/discourse/templates/about.hbs index fb732e1700b..a0ef47a5c99 100644 --- a/app/assets/javascripts/discourse/templates/about.hbs +++ b/app/assets/javascripts/discourse/templates/about.hbs @@ -19,6 +19,11 @@

{{model.description}}

+ {{plugin-outlet name="about-after-description" + connectorTagName='section' + tagName='' + args=(hash model=model)}} + {{#if model.admins}}

{{d-icon "users"}} {{i18n 'about.our_admins'}}

diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs index 4aeb37702a1..7be1e910609 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs @@ -1 +1 @@ -
{{title}} +{{title}} diff --git a/app/assets/javascripts/discourse/templates/mobile/components/categories-only.hbs b/app/assets/javascripts/discourse/templates/mobile/components/categories-only.hbs index 42b25ef1802..fda2b84c037 100644 --- a/app/assets/javascripts/discourse/templates/mobile/components/categories-only.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/components/categories-only.hbs @@ -41,11 +41,11 @@
{{number c.topics_all_time}}
{{i18n 'all_time'}}
{{#if c.pickMonth}} -
{{number c.topics_month}}
{{i18n 'month'}}
+
{{number c.topics_month}}
/ {{i18n 'month'}}
{{/if}} {{#if c.pickWeek}} -
{{number c.topics_week}}
{{i18n 'week'}}
+
{{number c.topics_week}}
/ {{i18n 'week'}}
{{/if}} diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index dcd09ac5892..5b4c000b3b5 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -81,19 +81,19 @@ {{else}} {{unbound invite.email}} {{format-date invite.created_at}} - + {{#if invite.expired}} - {{i18n 'user.invited.expired'}} -      +
{{i18n 'user.invited.expired'}}
{{/if}} {{#if invite.rescinded}} {{i18n 'user.invited.rescinded'}} {{else}} {{d-button icon="times" action=(action "rescind") actionParam=invite label="user.invited.rescind"}} {{/if}} -      + + {{#if invite.reinvited}} - {{i18n 'user.invited.reinvited'}} +
{{i18n 'user.invited.reinvited'}}
{{else}} {{d-button icon="sync" action=(action "reinvite") actionParam=invite label="user.invited.reinvite"}} {{/if}} diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index 5aa17d79c99..956dafd3583 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -13,6 +13,7 @@ import { formatUsername } from "discourse/lib/utilities"; import hbs from "discourse/widgets/hbs-compiler"; +import { relativeAge } from "discourse/lib/formatter"; function transformWithCallbacks(post) { let transformed = transformBasicPost(post); @@ -427,6 +428,29 @@ createWidget("post-contents", { } }); +createWidget("post-notice", { + tagName: "div.post-notice", + + html(attrs) { + let text, icon; + if (attrs.postNoticeType === "first") { + icon = "hands-helping"; + text = I18n.t("post.notice.first", { user: attrs.username }); + } else if (attrs.postNoticeType === "returning") { + icon = "far-smile"; + text = I18n.t("post.notice.return", { + user: attrs.username, + time: relativeAge(attrs.postNoticeTime, { + format: "tiny", + addAgo: true + }) + }); + } + + return h("p", [iconNode(icon), text]); + } +}); + createWidget("post-body", { tagName: "div.topic-body.clearfix", @@ -505,6 +529,10 @@ createWidget("post-article", { ); } + if (attrs.postNoticeType) { + rows.push(h("div.row", [this.attach("post-notice", attrs)])); + } + rows.push( h("div.row", [ this.attach("post-avatar", attrs), @@ -608,6 +636,9 @@ export default createWidget("post", { } else { classNames.push("regular"); } + if (attrs.ignored) { + classNames.push("post-ignored"); + } if (addPostClassesCallbacks) { for (let i = 0; i < addPostClassesCallbacks.length; i++) { let pluginClasses = addPostClassesCallbacks[i].call(this, attrs); diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 index 40b5ce38c02..1ce3119a75f 100644 --- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 @@ -152,14 +152,17 @@ export default createWidget("private-message-map", { } const result = [h(`div.participants${hideNamesClass}`, participants)]; + const controls = []; - const controls = [ - this.attach("button", { - action: "toggleEditing", - label: "private_message_info.edit", - className: "btn btn-default add-remove-participant-btn" - }) - ]; + if (attrs.canRemoveAllowedUsers || attrs.canRemoveSelfId) { + controls.push( + this.attach("button", { + action: "toggleEditing", + label: "private_message_info.edit", + className: "btn btn-default add-remove-participant-btn" + }) + ); + } if (attrs.canInvite && this.state.isEditing) { controls.push( @@ -171,7 +174,9 @@ export default createWidget("private-message-map", { ); } - result.push(h("div.controls", controls)); + if (controls.length) { + result.push(h("div.controls", controls)); + } return result; }, diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 index 69ecf7ace58..b6acdf85873 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -90,16 +90,48 @@ createSearchResult({ } }); +createSearchResult({ + type: "group", + linkField: "url", + builder(group) { + const fullName = escapeExpression(group.fullName); + const name = escapeExpression(group.name); + const groupNames = [h("span.name", fullName || name)]; + + if (fullName) { + groupNames.push(h("span.slug", name)); + } + + let avatarFlair; + if (group.flairUrl) { + avatarFlair = this.attach("avatar-flair", { + primary_group_flair_url: group.flairUrl, + primary_group_flair_bg_color: group.flairBgColor, + primary_group_flair_color: group.flairColor, + primary_group_name: name + }); + } else { + avatarFlair = iconNode("users"); + } + + const groupResultContents = [avatarFlair, h("div.group-names", groupNames)]; + + return h("div.group-result", groupResultContents); + } +}); + createSearchResult({ type: "user", linkField: "path", builder(u) { - const userTitles = [h("span.username", formatUsername(u.username))]; + const userTitles = []; if (u.name) { userTitles.push(h("span.name", u.name)); } + userTitles.push(h("span.username", formatUsername(u.username))); + const userResultContents = [ avatarImg("small", { template: u.avatar_template, @@ -112,21 +144,6 @@ createSearchResult({ } }); -createSearchResult({ - type: "group", - linkField: "url", - builder(group) { - const groupName = escapeExpression(group.name); - return h( - "span", - { - className: `group-${groupName} discourse-group` - }, - [iconNode("users"), h("span", groupName)] - ); - } -}); - createSearchResult({ type: "topic", linkField: "url", @@ -174,19 +191,12 @@ createWidget("search-menu-results", { const resultTypes = results.resultTypes || []; const mainResultsContent = []; - const classificationContents = []; - const otherContents = []; - const assignContainer = (type, node) => { - if (["topic"].includes(type)) { - mainResultsContent.push(node); - } else if (["category", "tag"].includes(type)) { - classificationContents.push(node); - } else { - otherContents.push(node); - } - }; + const usersAndGroups = []; + const categoriesAndTags = []; + const usersAndGroupsMore = []; + const categoriesAndTagsMore = []; - resultTypes.forEach(rt => { + const buildMoreNode = result => { const more = []; const moreArgs = { @@ -194,23 +204,45 @@ createWidget("search-menu-results", { contents: () => [I18n.t("more"), "..."] }; - if (rt.moreUrl) { + if (result.moreUrl) { more.push( - this.attach("link", $.extend(moreArgs, { href: rt.moreUrl })) + this.attach("link", $.extend(moreArgs, { href: result.moreUrl })) ); - } else if (rt.more) { + } else if (result.more) { more.push( this.attach( "link", $.extend(moreArgs, { action: "moreOfType", - actionParam: rt.type, + actionParam: result.type, className: "filter filter-type" }) ) ); } + if (more.length) { + return more; + } + }; + + const assignContainer = (result, node) => { + if (["topic"].includes(result.type)) { + mainResultsContent.push(node); + } + + if (["user", "group"].includes(result.type)) { + usersAndGroups.push(node); + usersAndGroupsMore.push(buildMoreNode(result)); + } + + if (["category", "tag"].includes(result.type)) { + categoriesAndTags.push(node); + categoriesAndTagsMore.push(buildMoreNode(result)); + } + }; + + resultTypes.forEach(rt => { const resultNodeContents = [ this.attach(rt.componentName, { searchContextEnabled: attrs.searchContextEnabled, @@ -220,14 +252,14 @@ createWidget("search-menu-results", { }) ]; - if (more.length) { - resultNodeContents.push(h("div.show-more", more)); + if (["topic"].includes(rt.type)) { + const more = buildMoreNode(rt); + if (more) { + resultNodeContents.push(h("div.show-more", more)); + } } - assignContainer( - rt.type, - h(`div.${rt.componentName}`, resultNodeContents) - ); + assignContainer(rt, h(`div.${rt.componentName}`, resultNodeContents)); }); const content = []; @@ -236,27 +268,25 @@ createWidget("search-menu-results", { content.push(h("div.main-results", mainResultsContent)); } - if (classificationContents.length || otherContents.length) { - const secondaryResultsContent = []; + if (usersAndGroups.length || categoriesAndTags.length) { + const secondaryResultsContents = []; - if (classificationContents.length) { - secondaryResultsContent.push( - h("div.classification-results", classificationContents) - ); + secondaryResultsContents.push(usersAndGroups); + secondaryResultsContents.push(usersAndGroupsMore); + + if (usersAndGroups.length && categoriesAndTags.length) { + secondaryResultsContents.push(h("div.separator")); } - if (otherContents.length) { - secondaryResultsContent.push(h("div.other-results", otherContents)); - } + secondaryResultsContents.push(categoriesAndTags); + secondaryResultsContents.push(categoriesAndTagsMore); - content.push( - h( - `div.secondary-results${ - mainResultsContent.length ? "" : ".no-main-results" - }`, - secondaryResultsContent - ) + const secondaryResults = h( + "div.secondary-results", + secondaryResultsContents ); + + content.push(secondaryResults); } return content; diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index ccc8eed9561..fbd4e40edbf 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -428,6 +428,11 @@ margin-bottom: 0; } + a.active { + background: $primary-medium; + color: $secondary; + } + a.blank:not(.active) { color: $primary-medium; } diff --git a/app/assets/stylesheets/common/base/search-menu.scss b/app/assets/stylesheets/common/base/search-menu.scss index a38e5b168ec..6da0cdf298c 100644 --- a/app/assets/stylesheets/common/base/search-menu.scss +++ b/app/assets/stylesheets/common/base/search-menu.scss @@ -76,26 +76,22 @@ flex-direction: column; flex: 1 1 auto; - .classification-results { - border-bottom: 1px solid $primary-low; + .separator { margin-bottom: 1em; - padding-bottom: 1em; - } - - .search-result-category { + margin-top: 1em; + height: 1px; + background: $primary-low; } .search-result-tag { - .list { - .item { - display: inline-flex; + .discourse-tag { + font-size: $font-down-1; + } + } - .widget-link.search-link { - display: inline; - font-size: $font-0; - padding: 5px; - } - } + .search-result-category { + .widget-link { + margin-bottom: 0; } } @@ -108,12 +104,71 @@ } } - .discourse-group { - display: inline-block; - word-break: break-all; + .group-result { + display: flex; + align-items: center; - .d-icon { - margin-right: s(1); + .d-icon, + .avatar-flair { + min-width: 25px; + margin-right: 0.5em; + + .d-icon { + margin-right: 0; + } + } + + .avatar-flair-image { + background-repeat: no-repeat; + background-size: 100% 100%; + min-height: 25px; + } + + .group-names { + display: flex; + flex-direction: column; + overflow: auto; + line-height: $line-height-medium; + + &:hover { + .name, + .slug { + color: $primary-high; + } + } + + .name, + .slug { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .name { + font-weight: 700; + } + + .slug { + font-size: $font-down-1; + color: $primary-high; + } + } + } + } + + .search-result-category, + .search-result-user, + .search-result-group, + .search-result-tag { + .list { + display: block; + + .item { + .widget-link.search-link { + flex: 1; + font-size: $font-0; + padding: 5px; + } } } } @@ -145,29 +200,17 @@ .username { color: dark-light-choose($primary-high, $secondary-low); - font-size: $font-0; - font-weight: 700; + font-size: $font-down-1; } .name { color: dark-light-choose($primary-high, $secondary-low); - font-size: $font-down-1; + font-size: $font-0; + font-weight: 700; } } } } - - &.no-main-results .search-result-user { - .user-titles { - flex-direction: row; - align-items: center; - - .name { - margin: 0 0 0 0.25em; - font-size: $font-0; - } - } - } } .show-more { diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index e17493e8830..246b4e28d8c 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -214,6 +214,10 @@ aside.quote { margin: -2px; } +.post-ignored { + font-style: italic; +} + .post-action { .undo-action, .act-action { @@ -353,7 +357,10 @@ aside.quote { display: flex; flex-wrap: wrap; align-items: center; - margin-bottom: 0.5em; + + & + .controls { + margin-top: 0.5em; + } &.hide-names .user { .username, @@ -857,3 +864,22 @@ a.mention-group { margin-bottom: 1em; } } + +.post-notice { + background-color: $tertiary-low; + border-top: 1px solid $primary-low; + color: $primary; + padding: 1em; + max-width: calc( + #{$topic-body-width} + #{$topic-avatar-width} - #{$topic-body-width-padding} + + 3px + ); + + p { + margin: 0; + } + + .d-icon { + margin-right: 1em; + } +} diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 75b797e261b..622493ee0d8 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -48,8 +48,7 @@ } } - &.active > a, - > a.active { + a.active { color: $secondary; background-color: $quaternary; diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index 073c8ded119..86e16bbb2f5 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -203,8 +203,24 @@ border: 1px solid $primary-low; } +.d-editor-preview img { + padding-bottom: 1.4em; + &.emoji, + &.avatar, + &.site-icon { + padding-bottom: 0; + } +} + .d-editor-preview .image-wrapper { position: relative; + display: inline-block; + padding-bottom: 1.4em; + + img { + padding-bottom: 0; + } + &:hover { .button-wrapper { opacity: 0.9; @@ -212,21 +228,22 @@ } .button-wrapper { opacity: 0; - background: $secondary; position: absolute; transition: all 0.25s; display: flex; align-items: center; - bottom: 0.75em; - left: 0.75em; - box-shadow: shadow("dropdown"); + bottom: 0; + left: 0; .separator { - color: $primary-low; + color: $primary-low-mid; } .scale-btn { color: $tertiary; - padding: 0.2em 0.6em; + padding: 0 0.4em; + &:first-of-type { + padding-left: 0; + } &.active { font-weight: bold; diff --git a/app/assets/stylesheets/common/select-kit/select-kit.scss b/app/assets/stylesheets/common/select-kit/select-kit.scss index 87497e62c86..c0d86c737b5 100644 --- a/app/assets/stylesheets/common/select-kit/select-kit.scss +++ b/app/assets/stylesheets/common/select-kit/select-kit.scss @@ -24,8 +24,6 @@ z-index: z("dropdown"); .select-kit-body { - -webkit-animation: fadein 0.25s; - animation: fadein 0.25s; display: flex; flex-direction: column; left: 0; diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 4a59ef97e00..484efd885e5 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -47,60 +47,69 @@ section.post-menu-area { nav.post-controls { padding: 0; .like-button { + // Like button wrapper display: inline-flex; - .like-count { - color: dark-light-choose($primary-low-mid, $secondary-high); - } - .widget-button { - background: none; - } + color: $primary-low-mid; + margin-right: 0.15em; &:hover { - background: $primary-low; - .like-count { + // Like button wrapper on hover + button { + background: $primary-low; color: $primary-medium; + } + } + button { + margin-left: 0; + margin-right: 0; + &.my-likes { + // Like count on my posts + .d-icon { + color: $primary-low-mid; + padding-left: 0.45em; + } + } + &.like { + // Like button with 0 likes + &.d-hover { + background: $love-low; + .d-icon { + color: $love; + } + } + } + &.has-like { + // Like button after I've liked + .d-icon { + color: $love; + } + &.d-hover { + background: $primary-low; + .d-icon { + color: $primary-medium; + } + } + } + &[disabled] { + // Disabled like button + cursor: not-allowed; + } + &.like-count { + // Like count button + &:not(.my-likes) { + padding-right: 0; + } &.d-hover { color: $primary; } - } - .d-hover { - background: none; - } - .d-icon { - color: $love; + + .toggle-like { + // Like button when like count is present + padding-left: 0.45em; + &.d-hover { + background: $primary-low; + } + } } } - &:active { - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4); - .widget-button { - box-shadow: none; - } - } - .like { - &:focus { - background: none; - } - } - .like-count { - font-size: $font-up-1; - margin-left: 0; - .d-icon { - padding-left: 10px; - color: dark-light-choose($primary-low-mid, $secondary-high); - } - &.my-likes { - margin-right: -2px; - } - &.regular-likes { - margin-right: -12px; - } - } - .toggle-like { - padding: 8px 8px; - margin-left: 2px; - } - } - .highlight-action { - color: dark-light-choose($primary-medium, $secondary-high); } a, button { @@ -186,23 +195,6 @@ nav.post-controls { color: $secondary; } } - &.like.d-hover, - &.like:focus { - color: $love; - background: $love-low; - .d-icon { - color: $love; - } - } - &.has-like .d-icon { - color: $love; - } - &.has-like[disabled]:hover { - background: transparent; - } - &.has-like[disabled]:active { - box-shadow: none; - } &.bookmark { padding: 8px 11px; &.bookmarked .d-icon { diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index f04ae567021..e03a940bc6d 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -115,6 +115,11 @@ .user-invite-list { width: 100%; margin-top: 15px; + tr { + td { + padding: 0.667em; + } + } } .user-invite-search { diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss index 7928653602a..caa03554998 100644 --- a/app/assets/stylesheets/mobile/modal.scss +++ b/app/assets/stylesheets/mobile/modal.scss @@ -66,7 +66,7 @@ form { margin-top: 20px; - input[type="text"] { + input:not(.filter-input)[type="text"] { box-sizing: border-box; width: 100%; } diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 69e58dbeee0..831e47b0ddc 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -475,3 +475,7 @@ span.highlighted { margin-bottom: 0; } } + +.post-notice { + margin-bottom: 1em; +} diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index f36fa25a16e..aa9eef412f7 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -113,12 +113,15 @@ class Admin::EmailTemplatesController < Admin::AdminController def update_key(key, value) old_value = I18n.t(key) - translation_override = TranslationOverride.upsert!(I18n.locale, key, value) + + unless old_value.is_a?(Hash) + translation_override = TranslationOverride.upsert!(I18n.locale, key, value) + end { key: key, old_value: old_value, - error_messages: translation_override.errors.full_messages + error_messages: translation_override&.errors&.full_messages } end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 884f67ec8f7..30e06c2f65e 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -143,7 +143,7 @@ class InvitesController < ApplicationController def rescind_all_invites guardian.ensure_can_rescind_all_invites!(current_user) - Invite.rescind_all_invites_from(current_user) + Invite.rescind_all_expired_invites_from(current_user) render body: nil end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 6820ed87de9..8317bb665c8 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -336,7 +336,7 @@ class PostsController < ApplicationController def destroy_many params.require(:post_ids) - defer_flags = params[:defer_flags] || false + agree_with_first_reply_flag = (params[:agree_with_first_reply_flag] || true).to_s == "true" posts = Post.where(id: post_ids_including_replies) raise Discourse::InvalidParameters.new(:post_ids) if posts.blank? @@ -345,7 +345,9 @@ class PostsController < ApplicationController posts.each { |p| guardian.ensure_can_delete!(p) } Post.transaction do - posts.each { |p| PostDestroyer.new(current_user, p, defer_flags: defer_flags).destroy } + posts.each_with_index do |p, i| + PostDestroyer.new(current_user, p, defer_flags: !(agree_with_first_reply_flag && i == 0)).destroy + end end render body: nil diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index de1032355d4..9be7ef9b809 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1226,6 +1226,7 @@ class UsersController < ApplicationController :title, :date_of_birth, :muted_usernames, + :ignored_usernames, :theme_ids, :locale, :bio_raw, diff --git a/app/jobs/base.rb b/app/jobs/base.rb index 515a3c8ab10..746f325c094 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -17,63 +17,74 @@ module Jobs class Base class JobInstrumenter - def initialize(job_class:, opts:, db:) + def initialize(job_class:, opts:, db:, jid:) return unless enabled? - @data = {} + self.class.mutex.synchronize do + @data = {} - @data["hostname"] = `hostname`.strip # Hostname - @data["pid"] = Process.pid # Pid - @data["database"] = db # DB name - multisite db name it ran on - @data["job_name"] = job_class.name # Job Name - eg: Jobs::AboutStats - @data["job_type"] = job_class.try(:scheduled?) ? "scheduled" : "regular" # Job Type - either s for scheduled or r for regular - @data["opts"] = opts.to_json # Params - json encoded params for the job + @data["hostname"] = `hostname`.strip # Hostname + @data["pid"] = Process.pid # Pid + @data["database"] = db # DB name - multisite db name it ran on + @data["job_id"] = jid # Job unique ID + @data["job_name"] = job_class.name # Job Name - eg: Jobs::AboutStats + @data["job_type"] = job_class.try(:scheduled?) ? "scheduled" : "regular" # Job Type - either s for scheduled or r for regular + @data["opts"] = opts.to_json # Params - json encoded params for the job - @data["status"] = 'pending' - @start_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC) + if ENV["DISCOURSE_LOG_SIDEKIQ_INTERVAL"] + @data["status"] = "starting" + write_to_log + end - self.class.ensure_interval_logging! - @@active_jobs ||= [] - @@active_jobs << self + @data["status"] = "pending" + @start_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC) - MethodProfiler.ensure_discourse_instrumentation! - MethodProfiler.start + self.class.ensure_interval_logging! + @@active_jobs ||= [] + @@active_jobs << self + + MethodProfiler.ensure_discourse_instrumentation! + MethodProfiler.start + end end def stop(exception:) return unless enabled? + self.class.mutex.synchronize do + profile = MethodProfiler.stop - profile = MethodProfiler.stop + @@active_jobs.delete(self) - @@active_jobs.delete(self) + @data["duration"] = profile[:total_duration] # Duration - length in seconds it took to run + @data["sql_duration"] = profile.dig(:sql, :duration) || 0 # Sql Duration (s) + @data["sql_calls"] = profile.dig(:sql, :calls) || 0 # Sql Statements - how many statements ran + @data["redis_duration"] = profile.dig(:redis, :duration) || 0 # Redis Duration (s) + @data["redis_calls"] = profile.dig(:redis, :calls) || 0 # Redis commands + @data["net_duration"] = profile.dig(:net, :duration) || 0 # Redis Duration (s) + @data["net_calls"] = profile.dig(:net, :calls) || 0 # Redis commands - @data["duration"] = profile[:total_duration] # Duration - length in seconds it took to run - @data["sql_duration"] = profile.dig(:sql, :duration) || 0 # Sql Duration (s) - @data["sql_calls"] = profile.dig(:sql, :calls) || 0 # Sql Statements - how many statements ran - @data["redis_duration"] = profile.dig(:redis, :duration) || 0 # Redis Duration (s) - @data["redis_calls"] = profile.dig(:redis, :calls) || 0 # Redis commands - @data["net_duration"] = profile.dig(:net, :duration) || 0 # Redis Duration (s) - @data["net_calls"] = profile.dig(:net, :calls) || 0 # Redis commands + if exception.present? + @data["exception"] = exception # Exception - if job fails a json encoded exception + @data["status"] = 'failed' + else + @data["status"] = 'success' # Status - fail, success, pending + end - if exception.present? - @data["exception"] = exception # Exception - if job fails a json encoded exception - @data["status"] = 'failed' - else - @data["status"] = 'success' # Status - fail, success, pending + write_to_log end - - write_to_log end def self.raw_log(message) - @@logger ||= Logger.new("#{Rails.root}/log/sidekiq.log") + @@logger ||= begin + f = File.open "#{Rails.root}/log/sidekiq.log", "a" + f.sync = true + Logger.new f + end @@log_queue ||= Queue.new - unless @log_thread&.alive? - @@log_thread = Thread.new do - begin - loop { @@logger << @@log_queue.pop } - rescue Exception => e - Discourse.warn_exception(e, message: "Sidekiq logging thread terminated unexpectedly") - end + @@log_thread ||= Thread.new do + begin + loop { @@logger << @@log_queue.pop } + rescue Exception => e + Discourse.warn_exception(e, message: "Sidekiq logging thread terminated unexpectedly") end end @@log_queue.push(message) @@ -94,14 +105,21 @@ module Jobs ENV["DISCOURSE_LOG_SIDEKIQ"] == "1" end + def self.mutex + @@mutex ||= Mutex.new + end + def self.ensure_interval_logging! interval = ENV["DISCOURSE_LOG_SIDEKIQ_INTERVAL"] return if !interval + interval = interval.to_i @@interval_thread ||= Thread.new do begin loop do - sleep interval.to_i - @@active_jobs.each { |j| j.write_to_log if j.current_duration > interval } + sleep interval + mutex.synchronize do + @@active_jobs.each { |j| j.write_to_log if j.current_duration > interval } + end end rescue Exception => e Discourse.warn_exception(e, message: "Sidekiq interval logging thread terminated unexpectedly") @@ -183,7 +201,7 @@ module Jobs exception = {} RailsMultisite::ConnectionManagement.with_connection(db) do - job_instrumenter = JobInstrumenter.new(job_class: self.class, opts: opts, db: db) + job_instrumenter = JobInstrumenter.new(job_class: self.class, opts: opts, db: db, jid: jid) begin I18n.locale = SiteSetting.default_locale || "en" I18n.ensure_all_loaded! diff --git a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb index d6a4a16d56c..a14db0bbadc 100644 --- a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb +++ b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb @@ -24,7 +24,7 @@ module Jobs BadgeGranter.grant(badge, user) SystemMessage.new(user).create('new_user_of_the_month', - month_year: Time.now.strftime("%B %Y"), + month_year: I18n.l(Time.now, format: :no_day), url: "#{Discourse.base_url}/badges" ) end diff --git a/app/models/google_user_info.rb b/app/models/google_user_info.rb deleted file mode 100644 index 343fe9945fd..00000000000 --- a/app/models/google_user_info.rb +++ /dev/null @@ -1,27 +0,0 @@ -class GoogleUserInfo < ActiveRecord::Base - belongs_to :user -end - -# == Schema Information -# -# Table name: google_user_infos -# -# id :integer not null, primary key -# user_id :integer not null -# google_user_id :string not null -# first_name :string -# last_name :string -# email :string -# gender :string -# name :string -# link :string -# profile_link :string -# picture :string -# created_at :datetime not null -# updated_at :datetime not null -# -# Indexes -# -# index_google_user_infos_on_google_user_id (google_user_id) UNIQUE -# index_google_user_infos_on_user_id (user_id) UNIQUE -# diff --git a/app/models/group.rb b/app/models/group.rb index 0f2959048a3..65bc2c7fd49 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -290,7 +290,7 @@ class Group < ActiveRecord::Base # way to have the membership in a table case name when :everyone - group.visibility_level = Group.visibility_levels[:owners] + group.visibility_level = Group.visibility_levels[:staff] group.save! return group when :moderators diff --git a/app/models/invite.rb b/app/models/invite.rb index b2012405365..6e022424381 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -226,8 +226,9 @@ class Invite < ActiveRecord::Base end end - def self.rescind_all_invites_from(user) - Invite.where('invites.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ?', user.id).find_each do |invite| + def self.rescind_all_expired_invites_from(user) + Invite.where('invites.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ? AND invites.created_at < ?', + user.id, SiteSetting.invite_expiry_days.days.ago).find_each do |invite| invite.trash!(user) end end diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index fbcc5621d0f..74a45ad97b6 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -407,8 +407,8 @@ class OptimizedImage < ActiveRecord::Base # just ditch the optimized image if there was any errors optimized_image.destroy ensure - file&.unlink file&.close + file&.unlink if file&.respond_to?(:unlink) end end end diff --git a/app/models/post.rb b/app/models/post.rb index 00a4db52fa0..73f52ecb6eb 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -194,6 +194,7 @@ class Post < ActiveRecord::Base def recover! super update_flagged_posts_count + delete_post_notices recover_public_post_actions TopicLink.extract_from(self) QuotedPost.extract_from(self) @@ -381,6 +382,11 @@ class Post < ActiveRecord::Base PostAction.update_flagged_posts_count end + def delete_post_notices + self.custom_fields.delete("post_notice_type") + self.custom_fields.delete("post_notice_time") + end + def recover_public_post_actions PostAction.publics .with_deleted diff --git a/app/models/s3_region_site_setting.rb b/app/models/s3_region_site_setting.rb index d931a14b203..9f195d4a908 100644 --- a/app/models/s3_region_site_setting.rb +++ b/app/models/s3_region_site_setting.rb @@ -17,15 +17,18 @@ class S3RegionSiteSetting < EnumSiteSetting 'ap-south-1', 'ap-southeast-1', 'ap-southeast-2', + 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 'eu-central-1', + 'eu-north-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1', 'us-east-2', + 'us-gov-east-1', 'us-gov-west-1', 'us-west-1', 'us-west-2', diff --git a/app/models/topic.rb b/app/models/topic.rb index 419a06357ee..028b6fc7a55 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -527,10 +527,10 @@ class Topic < ActiveRecord::Base end # Atomically creates the next post number - def self.next_post_number(topic_id, reply = false, whisper = false) + def self.next_post_number(topic_id, opts = {}) highest = DB.query_single("SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?", topic_id).first.to_i - if whisper + if opts[:whisper] result = DB.query_single(<<~SQL, highest, topic_id) UPDATE topics @@ -543,13 +543,15 @@ class Topic < ActiveRecord::Base else - reply_sql = reply ? ", reply_count = reply_count + 1" : "" + reply_sql = opts[:reply] ? ", reply_count = reply_count + 1" : "" + posts_sql = opts[:post] ? ", posts_count = posts_count + 1" : "" result = DB.query_single(<<~SQL, highest: highest, topic_id: topic_id) UPDATE topics SET highest_staff_post_number = :highest + 1, - highest_post_number = :highest + 1#{reply_sql}, - posts_count = posts_count + 1 + highest_post_number = :highest + 1 + #{reply_sql} + #{posts_sql} WHERE id = :topic_id RETURNING highest_post_number SQL @@ -585,6 +587,43 @@ class Topic < ActiveRecord::Base posts_count = Y.posts_count FROM X, Y WHERE + topics.archetype <> 'private_message' AND + X.topic_id = topics.id AND + Y.topic_id = topics.id AND ( + topics.highest_staff_post_number <> X.highest_post_number OR + topics.highest_post_number <> Y.highest_post_number OR + topics.last_posted_at <> Y.last_posted_at OR + topics.posts_count <> Y.posts_count + ) + SQL + + DB.exec <<~SQL + WITH + X as ( + SELECT topic_id, + COALESCE(MAX(post_number), 0) highest_post_number + FROM posts + WHERE deleted_at IS NULL + GROUP BY topic_id + ), + Y as ( + SELECT topic_id, + coalesce(MAX(post_number), 0) highest_post_number, + count(*) posts_count, + max(created_at) last_posted_at + FROM posts + WHERE deleted_at IS NULL AND post_type <> 3 AND post_type <> 4 + GROUP BY topic_id + ) + UPDATE topics + SET + highest_staff_post_number = X.highest_post_number, + highest_post_number = Y.highest_post_number, + last_posted_at = Y.last_posted_at, + posts_count = Y.posts_count + FROM X, Y + WHERE + topics.archetype = 'private_message' AND X.topic_id = topics.id AND Y.topic_id = topics.id AND ( topics.highest_staff_post_number <> X.highest_post_number OR @@ -597,32 +636,39 @@ class Topic < ActiveRecord::Base # If a post is deleted we have to update our highest post counters def self.reset_highest(topic_id) + archetype = Topic.where(id: topic_id).pluck(:archetype).first + + # ignore small_action replies for private messages + post_type = archetype == Archetype.private_message ? " AND post_type <> #{Post.types[:small_action]}" : '' + result = DB.query_single(<<~SQL, topic_id: topic_id) UPDATE topics SET - highest_staff_post_number = ( + highest_staff_post_number = ( SELECT COALESCE(MAX(post_number), 0) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL ), - highest_post_number = ( + highest_post_number = ( SELECT COALESCE(MAX(post_number), 0) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL AND post_type <> 4 + #{post_type} ), posts_count = ( SELECT count(*) FROM posts WHERE deleted_at IS NULL AND topic_id = :topic_id AND post_type <> 4 + #{post_type} ), - last_posted_at = ( SELECT MAX(created_at) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL AND post_type <> 4 + #{post_type} ) WHERE id = :topic_id RETURNING highest_post_number diff --git a/app/models/user.rb b/app/models/user.rb index 6c9e3be075d..97c8440a9a0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -66,7 +66,6 @@ class User < ActiveRecord::Base has_one :user_avatar, dependent: :destroy has_many :user_associated_accounts, dependent: :destroy has_one :github_user_info, dependent: :destroy - has_one :google_user_info, dependent: :destroy has_many :oauth2_user_infos, dependent: :destroy has_one :instagram_user_info, dependent: :destroy has_many :user_second_factors, dependent: :destroy diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index b2fe649156c..5d46756ad47 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -65,10 +65,12 @@ class WebHook < ActiveRecord::Base end end - def self.enqueue_topic_hooks(event, topic) + def self.enqueue_topic_hooks(event, topic, payload = nil) if active_web_hooks('topic').exists? && topic.present? - topic_view = TopicView.new(topic.id, Discourse.system_user) - payload = WebHook.generate_payload(:topic, topic_view, WebHookTopicViewSerializer) + payload ||= begin + topic_view = TopicView.new(topic.id, Discourse.system_user) + WebHook.generate_payload(:topic, topic_view, WebHookTopicViewSerializer) + end WebHook.enqueue_hooks(:topic, event, id: topic.id, @@ -79,9 +81,9 @@ class WebHook < ActiveRecord::Base end end - def self.enqueue_post_hooks(event, post) + def self.enqueue_post_hooks(event, post, payload = nil) if active_web_hooks('post').exists? && post.present? - payload = WebHook.generate_payload(:post, post) + payload ||= WebHook.generate_payload(:post, post) WebHook.enqueue_hooks(:post, event, id: post.id, diff --git a/app/serializers/basic_post_serializer.rb b/app/serializers/basic_post_serializer.rb index bcc75f776e1..014421decfb 100644 --- a/app/serializers/basic_post_serializer.rb +++ b/app/serializers/basic_post_serializer.rb @@ -6,7 +6,8 @@ class BasicPostSerializer < ApplicationSerializer :avatar_template, :created_at, :cooked, - :cooked_hidden + :cooked_hidden, + :ignored def name object.user && object.user.name @@ -35,11 +36,18 @@ class BasicPostSerializer < ApplicationSerializer else I18n.t('flagging.user_must_edit') end + elsif ignored + I18n.t('ignored.hidden_content') else object.filter_quotes(@parent_post) end end + def ignored + object.is_first_post? && IgnoredUser.where(user_id: scope.current_user&.id, + ignored_user_id: object.user_id).present? + end + def include_name? SiteSetting.enable_names? end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 42686fae37a..916e2cdb6ea 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -70,6 +70,8 @@ class PostSerializer < BasicPostSerializer :is_auto_generated, :action_code, :action_code_who, + :post_notice_type, + :post_notice_time, :last_wiki_edit, :locked, :excerpt @@ -363,6 +365,22 @@ class PostSerializer < BasicPostSerializer include_action_code? && action_code_who.present? end + def post_notice_type + post_custom_fields["post_notice_type"] + end + + def include_post_notice_type? + post_notice_type.present? + end + + def post_notice_time + post_custom_fields["post_notice_time"] + end + + def include_post_notice_time? + post_notice_time.present? + end + def locked true end diff --git a/app/services/user_anonymizer.rb b/app/services/user_anonymizer.rb index ae480252b55..dfeb941d8db 100644 --- a/app/services/user_anonymizer.rb +++ b/app/services/user_anonymizer.rb @@ -53,7 +53,6 @@ class UserAnonymizer end @user.user_avatar.try(:destroy) - @user.google_user_info.try(:destroy) @user.github_user_info.try(:destroy) @user.single_sign_on_record.try(:destroy) @user.oauth2_user_infos.try(:destroy_all) diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 52df88fdab2..261634a6f5e 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -128,6 +128,10 @@ class UserUpdater update_muted_users(attributes[:muted_usernames]) end + if attributes.key?(:ignored_usernames) + update_ignored_users(attributes[:ignored_usernames]) + end + name_changed = user.name_changed? if (saved = (!save_options || user.user_option.save) && user_profile.save && user.save) && (name_changed && old_user_name.casecmp(attributes.fetch(:name)) != 0) @@ -157,13 +161,27 @@ class UserUpdater INSERT into muted_users(user_id, muted_user_id, created_at, updated_at) SELECT :user_id, id, :now, :now FROM users - WHERE - id in (:desired_ids) AND - id NOT IN ( - SELECT muted_user_id - FROM muted_users - WHERE user_id = :user_id - ) + WHERE id in (:desired_ids) + ON CONFLICT DO NOTHING + SQL + end + end + + def update_ignored_users(usernames) + usernames ||= "" + desired_ids = User.where(username: usernames.split(",")).pluck(:id) + if desired_ids.empty? + IgnoredUser.where(user_id: user.id).destroy_all + else + IgnoredUser.where('user_id = ? AND ignored_user_id not in (?)', user.id, desired_ids).destroy_all + + # SQL is easier here than figuring out how to do the same in AR + DB.exec(<<~SQL, now: Time.now, user_id: user.id, desired_ids: desired_ids) + INSERT into ignored_users(user_id, ignored_user_id, created_at, updated_at) + SELECT :user_id, id, :now, :now + FROM users + WHERE id in (:desired_ids) + ON CONFLICT DO NOTHING SQL end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f6a9b5613d7..a01460ce174 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -17,6 +17,8 @@ <%- end %> + <%= build_plugin_html 'server:before-script-load' %> + <%= preload_script "locales/#{I18n.locale}" %> <%= preload_script "ember_jquery" %> <%= preload_script "preload-store" %> diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e2dfc29843d..bb1eb3917a7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -192,15 +192,18 @@ en: ap_south_1: "Asia Pacific (Mumbai)" ap_southeast_1: "Asia Pacific (Singapore)" ap_southeast_2: "Asia Pacific (Sydney)" + ca_central_1: "Canada (Central)" cn_north_1: "China (Beijing)" cn_northwest_1: "China (Ningxia)" eu_central_1: "EU (Frankfurt)" + eu_north_1: "EU (Stockholm)" eu_west_1: "EU (Ireland)" eu_west_2: "EU (London)" eu_west_3: "EU (Paris)" - sa_east_1: "South America (Sao Paulo)" + sa_east_1: "South America (São Paulo)" us_east_1: "US East (N. Virginia)" us_east_2: "US East (Ohio)" + us_gov_east_1: "AWS GovCloud (US-East)" us_gov_west_1: "AWS GovCloud (US)" us_west_1: "US West (N. California)" us_west_2: "US West (Oregon)" @@ -1000,9 +1003,9 @@ en: expired: "This invite has expired." rescind: "Remove" rescinded: "Invite removed" - rescind_all: "Remove all Invites" - rescinded_all: "All Invites removed!" - rescind_all_confirm: "Are you sure you want to remove all invites?" + rescind_all: "Remove all Expired Invites" + rescinded_all: "All Expired Invites removed!" + rescind_all_confirm: "Are you sure you want to remove all expired invites?" reinvite: "Resend Invite" reinvite_all: "Resend all Invites" reinvite_all_confirm: "Are you sure you want to resend all invites?" @@ -1809,6 +1812,7 @@ en: when: "When:" public_timer_types: Topic Timers private_timer_types: User Topic Timers + time_frame_required: Please select a time frame auto_update_input: none: "Select a timeframe" later_today: "Later today" @@ -2145,6 +2149,10 @@ en: one: "view 1 hidden reply" other: "view {{count}} hidden replies" + notice: + first: "This is the first time {{user}} has posted — let's welcome them to our community!" + return: "It's been a while since we've seen {{user}} — their last post was in {{time}}." + unread: "Post is unread" has_replies: one: "{{count}} Reply" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d1013cc4589..300697c5952 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -25,14 +25,16 @@ en: datetime_formats: &datetime_formats formats: - # Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime short: "%m-%d-%Y" - # Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime short_no_year: "%B %-d" - # Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime date_only: "%B %-d, %Y" - # Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime long: "%B %-d, %Y, %l:%M%P" + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime + no_day: "%B %Y" date: # Do not remove the brackets and commas and do not translate the first month name. It should be "null". month_names: @@ -870,6 +872,9 @@ en: you_must_edit: '

Your post was flagged by the community. Please see your messages.

' user_must_edit: "

This post was flagged by the community and is temporarily hidden.

" + ignored: + hidden_content: '

Hidden content

' + archetypes: regular: title: "Regular Topic" @@ -1896,6 +1901,8 @@ en: max_allowed_message_recipients: "Maximum recipients allowed in a message." watched_words_regular_expressions: "Watched words are regular expressions." + returning_users_days: "How many days should pass before a user is considered to be returning." + default_email_digest_frequency: "How often users receive summary emails by default." default_include_tl0_in_digests: "Include posts from new users in summary emails by default. Users can change this in their preferences." default_email_personal_messages: "Send an email when someone messages the user by default." diff --git a/config/site_settings.yml b/config/site_settings.yml index 2acedd3ec46..8bd22447f95 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -807,6 +807,8 @@ posting: default: false client: true shadowed_by_global: true + returning_users_days: + default: 60 email: email_time_window_mins: @@ -1251,7 +1253,7 @@ security: list_type: compact blacklisted_crawler_user_agents: type: list - default: "mauibot|semrushbot|ahrefsbot" + default: "mauibot|semrushbot|ahrefsbot|blexbot" list_type: compact slow_down_crawler_user_agents: type: list diff --git a/db/migrate/20190306154335_migrate_google_user_info.rb b/db/migrate/20190306154335_migrate_google_user_info.rb new file mode 100644 index 00000000000..a3a9c0e1c37 --- /dev/null +++ b/db/migrate/20190306154335_migrate_google_user_info.rb @@ -0,0 +1,27 @@ +class MigrateGoogleUserInfo < ActiveRecord::Migration[5.2] + def up + execute <<~SQL + INSERT INTO user_associated_accounts ( + provider_name, + provider_uid, + user_id, + info, + last_used, + created_at, + updated_at + ) SELECT + 'google_oauth2', + google_user_id, + user_id, + json_build_object('email', email, 'first_name', first_name, 'last_name', last_name, 'name', name), + updated_at, + created_at, + updated_at + FROM google_user_infos + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index 297fa50976b..118d0812af5 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -1,5 +1,4 @@ -class Auth::GoogleOAuth2Authenticator < Auth::Authenticator - +class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator def name "google_oauth2" end @@ -8,77 +7,10 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator SiteSetting.enable_google_oauth2_logins end - def description_for_user(user) - info = GoogleUserInfo.find_by(user_id: user.id) - info&.email || info&.name || "" - end - - def can_revoke? - true - end - - def revoke(user, skip_remote: false) - info = GoogleUserInfo.find_by(user_id: user.id) - raise Discourse::NotFound if info.nil? - - # We get a temporary token from google upon login but do not need it, and do not store it. - # Therefore we do not have any way to revoke the token automatically on google's end - - info.destroy! - true - end - - def can_connect_existing_user? - true - end - - def after_authenticate(auth_hash, existing_account: nil) - session_info = parse_hash(auth_hash) - google_hash = session_info[:google] - - result = ::Auth::Result.new - result.email = session_info[:email] - result.email_valid = session_info[:email_valid] - result.name = session_info[:name] - - result.extra_data = google_hash - - user_info = ::GoogleUserInfo.find_by(google_user_id: google_hash[:google_user_id]) - - if existing_account && (user_info.nil? || existing_account.id != user_info.user_id) - user_info.destroy! if user_info - result.user = existing_account - user_info = GoogleUserInfo.create!({ user_id: result.user.id }.merge(google_hash)) - else - result.user = user_info&.user - end - - if !result.user && !result.email.blank? && result.email_valid - result.user = User.find_by_email(result.email) - if result.user - # we've matched an existing user to this login attempt... - if result.user.google_user_info && result.user.google_user_info.google_user_id != google_hash[:google_user_id] - # but the user has changed the google account used to log in... - if result.user.google_user_info.email != google_hash[:email] - # the user changed their email, go ahead and scrub the old record - result.user.google_user_info.destroy! - else - # same email address but different account? likely a takeover scenario - result.failed = true - result.failed_reason = I18n.t('errors.conflicting_google_user_id') - return result - end - end - ::GoogleUserInfo.create({ user_id: result.user.id }.merge(google_hash)) - end - end - - result - end - - def after_create_account(user, auth) - data = auth[:extra_data] - GoogleUserInfo.create({ user_id: user.id }.merge(data)) + def primary_email_verified?(auth_token) + # note, emails that come back from google via omniauth are always valid + # this protects against future regressions + auth_token[:extra][:raw_info][:email_verified] end def register_middleware(omniauth) @@ -95,37 +27,8 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") end - }, - skip_jwt: true + } } - # jwt encoding is causing auth to fail in quite a few conditions - # skipping omniauth.provider :google_oauth2, options end - - protected - - def parse_hash(hash) - extra = hash[:extra][:raw_info] - - h = {} - - h[:email] = hash[:info][:email] - h[:name] = hash[:info][:name] - h[:email_valid] = extra[:email_verified] - - h[:google] = { - google_user_id: hash[:uid] || extra[:sub], - email: extra[:email], - first_name: extra[:given_name], - last_name: extra[:family_name], - gender: extra[:gender], - name: extra[:name], - link: extra[:hd], - profile_link: extra[:profile], - picture: extra[:picture] - } - - h - end end diff --git a/lib/auth/managed_authenticator.rb b/lib/auth/managed_authenticator.rb index a56989c587d..1d21453a371 100644 --- a/lib/auth/managed_authenticator.rb +++ b/lib/auth/managed_authenticator.rb @@ -10,6 +10,12 @@ class Auth::ManagedAuthenticator < Auth::Authenticator true end + def primary_email_verified?(auth_token) + # Omniauth providers should only provide verified emails in the :info hash. + # This method allows additional checks to be added + true + end + def can_revoke? true end @@ -35,7 +41,11 @@ class Auth::ManagedAuthenticator < Auth::Authenticator end # Matching an account by email - if match_by_email && association.user.nil? && (user = User.find_by_email(auth_token.dig(:info, :email))) + if primary_email_verified?(auth_token) && + match_by_email && + association.user.nil? && + (user = User.find_by_email(auth_token.dig(:info, :email))) + UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end @@ -60,7 +70,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result.email = info[:email] result.name = "#{info[:first_name]} #{info[:last_name]}" result.username = info[:nickname] - result.email_valid = true if result.email + result.email_valid = primary_email_verified?(auth_token) if result.email result.extra_data = { provider: auth_token[:provider], uid: auth_token[:uid] diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index f382df5ea36..57870cfab95 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -29,6 +29,7 @@ module BackupRestore @client_id = opts[:client_id] @filename = opts[:filename] @publish_to_message_bus = opts[:publish_to_message_bus] || false + @disable_emails = opts.fetch(:disable_emails, true) ensure_restore_is_enabled ensure_no_operation_is_running @@ -402,9 +403,11 @@ module BackupRestore log "Reloading site settings..." SiteSetting.refresh! - log "Disabling outgoing emails for non-staff users..." - user = User.find_by_email(@user_info[:email]) || Discourse.system_user - SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + if @disable_emails + log "Disabling outgoing emails for non-staff users..." + user = User.find_by_email(@user_info[:email]) || Discourse.system_user + SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + end end def clear_emoji_cache diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 736626194d7..c7f706d05c2 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -899,6 +899,22 @@ module Email create_post_with_attachments(options) end + def notification_level_for(body) + # since we are stripping save all this work on long replies + return nil if body.length > 40 + + body = body.strip.downcase + case body + when "mute" + NotificationLevels.topic_levels[:muted] + when "track" + NotificationLevels.topic_levels[:tracking] + when "watch" + NotificationLevels.topic_levels[:watching] + else nil + end + end + def create_reply(options = {}) raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed? raise BouncedEmailError if options[:bounce] && options[:topic].archetype != Archetype.private_message @@ -908,6 +924,8 @@ module Email if post_action_type = post_action_for(options[:raw]) create_post_action(options[:user], options[:post], post_action_type) + elsif notification_level = notification_level_for(options[:raw]) + TopicUser.change(options[:user].id, options[:post].topic_id, notification_level: notification_level) else raise TopicClosedError if options[:topic].closed? options[:topic_id] = options[:topic].id diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 21fba9804ef..377d11e5508 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -86,7 +86,7 @@ module JsLocaleHelper end def self.load_translations_merged(*locales) - locales = locales.compact + locales = locales.uniq.compact @loaded_merges ||= {} @loaded_merges[locales.join('-')] ||= begin all_translations = {} diff --git a/lib/post_creator.rb b/lib/post_creator.rb index d8bee4047ca..2dd11e649db 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -165,6 +165,7 @@ class PostCreator transaction do build_post_stats create_topic + create_post_notice save_post extract_links track_topic @@ -247,7 +248,11 @@ class PostCreator post.word_count = post.raw.scan(/[[:word:]]+/).size whisper = post.post_type == Post.types[:whisper] - post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?, whisper) + increase_posts_count = !post.topic&.private_message? || post.post_type != Post.types[:small_action] + post.post_number ||= Topic.next_post_number(post.topic_id, + reply: post.reply_to_post_number.present?, + whisper: whisper, + post: increase_posts_count) cooking_options = post.cooking_options || {} cooking_options[:topic_id] = post.topic_id @@ -508,6 +513,21 @@ class PostCreator @user.update_attributes(last_posted_at: @post.created_at) end + def create_post_notice + last_post_time = Post.where(user_id: @user.id) + .order(created_at: :desc) + .limit(1) + .pluck(:created_at) + .first + + if !last_post_time + @post.custom_fields["post_notice_type"] = "first" + elsif SiteSetting.returning_users_days > 0 && last_post_time < SiteSetting.returning_users_days.days.ago + @post.custom_fields["post_notice_type"] = "returning" + @post.custom_fields["post_notice_time"] = last_post_time.iso8601 + end + end + def publish return if @opts[:import_mode] || @post.post_number == 1 @post.publish_change_to_clients! :created diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index cf36b72d6f9..16e91f4c586 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -61,19 +61,11 @@ class PostDestroyer mark_for_deletion(delete_removed_posts_after) end DiscourseEvent.trigger(:post_destroyed, @post, @opts, @user) - WebHook.enqueue_hooks(:post, :post_destroyed, - id: @post.id, - category_id: @post&.topic&.category_id, - payload: payload - ) if WebHook.active_web_hooks(:post).exists? + WebHook.enqueue_post_hooks(:post_destroyed, @post, payload) if @post.is_first_post? && @post.topic DiscourseEvent.trigger(:topic_destroyed, @post.topic, @user) - WebHook.enqueue_hooks(:topic, :topic_destroyed, - id: topic.id, - category_id: topic&.category_id, - payload: topic_payload - ) if WebHook.active_web_hooks(:topic).exists? + WebHook.enqueue_topic_hooks(:topic_destroyed, @post.topic, topic_payload) end end @@ -147,7 +139,7 @@ class PostDestroyer update_user_counts TopicUser.update_post_action_cache(post_id: @post.id) DB.after_commit do - if @opts[:defer_flags].to_s == "true" + if @opts[:defer_flags] defer_flags else agree_with_flags diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index e3a89fe4611..d4d8ce56f87 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -230,10 +230,11 @@ module PrettyText return title unless SiteSetting.enable_emoji? set = SiteSetting.emoji_set.inspect + custom = Emoji.custom.map { |e| [e.name, e.url] }.to_h.to_json protect do v8.eval(<<~JS) __paths = #{paths_json}; - __performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set} }); + __performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set}, customEmoji: #{custom} }); JS end end diff --git a/lib/search.rb b/lib/search.rb index 56ebc31a431..3ffbda872fb 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -687,7 +687,7 @@ class Search def groups_search groups = Group .visible_groups(@guardian.user, "name ASC", include_everyone: false) - .where("groups.name ILIKE ?", "%#{@term}%") + .where("name ILIKE :term OR full_name ILIKE :term", term: "%#{@term}%") groups.each { |group| @results.add(group) } end diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb index 49ff26d8e70..19d9676120f 100644 --- a/lib/stylesheet/importer.rb +++ b/lib/stylesheet/importer.rb @@ -144,9 +144,8 @@ COMMENT end def to_scss_variable(name, value) - escaped = value.to_s.gsub('"', "\\22") - escaped.gsub!("\n", "\\A") - "$#{name}: unquote(\"#{escaped}\");\n" + escaped = SassC::Script::Value::String.quote(value, sass: true) + "$#{name}: unquote(#{escaped});\n" end def imports(asset, parent_path) diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb index df3259fdbd1..f6946b2e325 100644 --- a/lib/svg_sprite/svg_sprite.rb +++ b/lib/svg_sprite/svg_sprite.rb @@ -118,6 +118,7 @@ module SvgSprite "globe", "globe-americas", "hand-point-right", + "hands-helping", "heading", "heart", "home", @@ -224,10 +225,10 @@ module SvgSprite icons = all_icons(theme_ids) doc = File.open("#{Rails.root}/vendor/assets/svg-icons/fontawesome/solid.svg") { |f| Nokogiri::XML(f) } - fa_license = doc.at('//comment()').text svg_subset = """ """.dup diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index 0e2c6525dfe..2815f4440c5 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -58,7 +58,7 @@ task 'assets:precompile:css' => 'environment' do STDERR.puts "Compiling css for #{db} #{Time.zone.now}" begin Stylesheet::Manager.precompile_css - rescue PG::UndefinedColumn => e + rescue PG::UndefinedColumn, ActiveModel::MissingAttributeError => e STDERR.puts "#{e.class} #{e.message}: #{e.backtrace.join("\n")}" STDERR.puts "Skipping precompilation of CSS cause schema is old, you are precompiling prior to running migrations." end diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 3bc5879669a..1ca1d3e2be3 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -495,102 +495,6 @@ def list_missing_uploads(skip_optimized: false) Discourse.store.list_missing_uploads(skip_optimized: skip_optimized) end -################################################################################ -# Recover from tombstone # -################################################################################ - -task "uploads:recover_from_tombstone" => :environment do - if ENV["RAILS_DB"] - recover_from_tombstone - else - RailsMultisite::ConnectionManagement.each_connection { recover_from_tombstone } - end -end - -def recover_from_tombstone - if Discourse.store.external? - puts "This task only works for internal storages." - return - end - - begin - previous_image_size = SiteSetting.max_image_size_kb - previous_attachment_size = SiteSetting.max_attachment_size_kb - previous_extensions = SiteSetting.authorized_extensions - - SiteSetting.max_image_size_kb = 10 * 1024 - SiteSetting.max_attachment_size_kb = 10 * 1024 - SiteSetting.authorized_extensions = "*" - - current_db = RailsMultisite::ConnectionManagement.current_db - public_path = Rails.root.join("public") - paths = Dir.glob(File.join(public_path, 'uploads', 'tombstone', current_db, '**', '*.*')) - max = paths.size - - paths.each_with_index do |path, index| - filename = File.basename(path) - printf("%9d / %d (%5.1f%%)\n", (index + 1), max, (((index + 1).to_f / max.to_f) * 100).round(1)) - - Post.where("raw LIKE ?", "%#{filename}%").find_each do |post| - doc = Nokogiri::HTML::fragment(post.raw) - updated = false - - image_urls = doc.css("img[src]").map { |img| img["src"] } - attachment_urls = doc.css("a.attachment[href]").map { |a| a["href"] } - - (image_urls + attachment_urls).each do |url| - next if !url.start_with?("/uploads/") - next if Upload.exists?(url: url) - - puts "Restoring #{path}..." - tombstone_path = File.join(public_path, 'uploads', 'tombstone', url.gsub(/^\/uploads\//, "")) - - if File.exists?(tombstone_path) - File.open(tombstone_path) do |file| - new_upload = UploadCreator.new(file, File.basename(url)).create_for(Discourse::SYSTEM_USER_ID) - - if new_upload.persisted? - puts "Restored into #{new_upload.url}" - DbHelper.remap(url, new_upload.url) - updated = true - else - puts "Failed to create upload for #{url}: #{new_upload.errors.full_messages}." - end - end - else - puts "Failed to find file (#{tombstone_path}) in tombstone." - end - end - - post.rebake! if updated - end - - sha1 = File.basename(filename, File.extname(filename)) - short_url = "upload://#{Base62.encode(sha1.hex)}" - - Post.where("raw LIKE ?", "%#{short_url}%").find_each do |post| - puts "Restoring #{path}..." - - File.open(path) do |file| - new_upload = UploadCreator.new(file, filename).create_for(Discourse::SYSTEM_USER_ID) - - if new_upload.persisted? - puts "Restored into #{new_upload.short_url}" - DbHelper.remap(short_url, new_upload.short_url) if short_url != new_upload.short_url - post.rebake! - else - puts "Failed to create upload for #{filename}: #{new_upload.errors.full_messages}." - end - end - end - end - ensure - SiteSetting.max_image_size_kb = previous_image_size - SiteSetting.max_attachment_size_kb = previous_attachment_size - SiteSetting.authorized_extensions = previous_extensions - end -end - ################################################################################ # regenerate_missing_optimized # ################################################################################ @@ -795,6 +699,10 @@ task "uploads:fix_incorrect_extensions" => :environment do UploadFixer.fix_all_extensions end +task "uploads:recover_from_tombstone" => :environment do + Rake::Task["uploads:recover"].invoke +end + task "uploads:recover" => :environment do require_dependency "upload_recovery" diff --git a/lib/topic_view.rb b/lib/topic_view.rb index c40f82ff0b5..4f8c0ac3668 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -18,7 +18,7 @@ class TopicView end def self.default_post_custom_fields - @default_post_custom_fields ||= ["action_code_who"] + @default_post_custom_fields ||= ["action_code_who", "post_notice_type", "post_notice_time"] end def self.post_custom_fields_whitelisters diff --git a/package.json b/package.json index 65923e79e74..2e05f64c6b0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Discourse", "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-free": "5.5.0", + "@fortawesome/fontawesome-free": "5.7.2", "ace-builds": "1.4.2", "bootbox": "3.2.0", "chart.js": "2.7.3", diff --git a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 index 39d4d16049b..f3ae85f855b 100644 --- a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 +++ b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 @@ -420,7 +420,9 @@ createWidget("discourse-poll-buttons", { const castVotesDisabled = !attrs.canCastVotes; contents.push( this.attach("button", { - className: `btn cast-votes ${castVotesDisabled ? "" : "btn-primary"}`, + className: `btn cast-votes ${ + castVotesDisabled ? "btn-default" : "btn-primary" + }`, label: "poll.cast-votes.label", title: "poll.cast-votes.title", disabled: castVotesDisabled, @@ -433,7 +435,7 @@ createWidget("discourse-poll-buttons", { if (attrs.showResults || hideResultsDisabled) { contents.push( this.attach("button", { - className: "btn toggle-results", + className: "btn btn-default toggle-results", label: "poll.hide-results.label", title: "poll.hide-results.title", icon: "far-eye-slash", @@ -449,7 +451,7 @@ createWidget("discourse-poll-buttons", { } else { contents.push( this.attach("button", { - className: "btn toggle-results", + className: "btn btn-default toggle-results", label: "poll.show-results.label", title: "poll.show-results.title", icon: "far-eye", @@ -492,7 +494,7 @@ createWidget("discourse-poll-buttons", { if (!attrs.isAutomaticallyClosed) { contents.push( this.attach("button", { - className: "btn toggle-status", + className: "btn btn-default toggle-status", label: "poll.open.label", title: "poll.open.title", icon: "unlock-alt", diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss index 63f671d9eeb..c66bb0f1d8a 100644 --- a/plugins/poll/assets/stylesheets/common/poll.scss +++ b/plugins/poll/assets/stylesheets/common/poll.scss @@ -59,6 +59,10 @@ div.poll { margin: 0.25em 0; color: $primary-medium; } + .info-text + .info-text, + button + .info-text { + margin-left: 0.5em; + } } .poll-voters:not(:empty) { diff --git a/plugins/poll/config/locales/client.en.yml b/plugins/poll/config/locales/client.en.yml index a44a852cb6a..3197397895f 100644 --- a/plugins/poll/config/locales/client.en.yml +++ b/plugins/poll/config/locales/client.en.yml @@ -72,8 +72,8 @@ en: confirm: "Are you sure you want to close this poll?" automatic_close: - closes_in: "closes in %{timeLeft}" - age: "closed %{age}" + closes_in: "Closes in %{timeLeft}." + age: "Closed %{age}" error_while_toggling_status: "Sorry, there was an error toggling the status of this poll." error_while_casting_votes: "Sorry, there was an error casting your votes." diff --git a/script/bulk_import/discourse_merger.rb b/script/bulk_import/discourse_merger.rb index 2ad24990f29..cb85bc31e9b 100644 --- a/script/bulk_import/discourse_merger.rb +++ b/script/bulk_import/discourse_merger.rb @@ -151,7 +151,7 @@ class BulkImport::DiscourseMerger < BulkImport::Base copy_model(c, skip_if_merged: true, is_a_user_model: true, skip_processing: true) end - [UserAssociatedAccount, GithubUserInfo, GoogleUserInfo, Oauth2UserInfo, + [UserAssociatedAccount, GithubUserInfo, Oauth2UserInfo, SingleSignOnRecord, EmailChangeRequest ].each do |c| copy_model(c, skip_if_merged: true, is_a_user_model: true) @@ -628,11 +628,6 @@ class BulkImport::DiscourseMerger < BulkImport::Base r end - def process_google_user_info(r) - return nil if GoogleUserInfo.where(google_user_id: r['google_user_id']).exists? - r - end - def process_oauth2_user_info(r) return nil if Oauth2UserInfo.where(uid: r['uid'], provider: r['provider']).exists? r diff --git a/script/discourse b/script/discourse index 205b9be545b..3b830db810c 100755 --- a/script/discourse +++ b/script/discourse @@ -106,6 +106,7 @@ class DiscourseCLI < Thor end desc "restore", "Restore a Discourse backup" + option :disable_emails, type: :boolean, default: true def restore(filename = nil) if File.exist?('/usr/local/bin/discourse') @@ -132,7 +133,11 @@ class DiscourseCLI < Thor begin puts "Starting restore: #{filename}" - restorer = BackupRestore::Restorer.new(Discourse.system_user.id, filename: filename) + restorer = BackupRestore::Restorer.new( + Discourse.system_user.id, + filename: filename, + disable_emails: options[:disable_emails] + ) restorer.run puts 'Restore done.' rescue BackupRestore::FilenameMissingError diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index b6bb1762cec..39d20568467 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -563,7 +563,7 @@ class ImportScripts::Base post_creator = PostCreator.new(user, opts) post = post_creator.create post_create_action.try(:call, post) if post - post ? post : post_creator.errors.full_messages + post && post_creator.errors.empty? ? post : post_creator.errors.full_messages end def create_upload(user_id, path, source_filename) diff --git a/script/import_scripts/nodebb/nodebb.rb b/script/import_scripts/nodebb/nodebb.rb index 3e5c616a5d6..45078de4239 100644 --- a/script/import_scripts/nodebb/nodebb.rb +++ b/script/import_scripts/nodebb/nodebb.rb @@ -144,6 +144,7 @@ class ImportScripts::NodeBB < ImportScripts::Base suspended_till: suspended_till, primary_group_id: group_id_from_imported_group_id(user["groupTitle"]), created_at: user["joindate"], + active: true, custom_fields: { import_pass: user["password"] }, @@ -197,13 +198,14 @@ class ImportScripts::NodeBB < ImportScripts::Base upload = UploadCreator.new(file, filename).create_for(imported_user.id) else - # remove "/assets/uploads/" from attachment + # remove "/assets/uploads/" and "/uploads" from attachment picture = picture.gsub("/assets/uploads", "") + picture = picture.gsub("/uploads", "") filepath = File.join(ATTACHMENT_DIR, picture) filename = File.basename(picture) unless File.exists?(filepath) - puts "Avatar file doesn't exist: #{filename}" + puts "Avatar file doesn't exist: #{filepath}" return nil end @@ -256,13 +258,14 @@ class ImportScripts::NodeBB < ImportScripts::Base upload = UploadCreator.new(file, filename).create_for(imported_user.id) else - # remove "/assets/uploads/" from attachment + # remove "/assets/uploads/" and "/uploads" from attachment picture = picture.gsub("/assets/uploads", "") + picture = picture.gsub("/uploads", "") filepath = File.join(ATTACHMENT_DIR, picture) filename = File.basename(picture) unless File.exists?(filepath) - puts "Background file doesn't exist: #{filename}" + puts "Background file doesn't exist: #{filepath}" return nil end @@ -509,13 +512,6 @@ class ImportScripts::NodeBB < ImportScripts::Base end end - # @username with dash to underscore - raw = raw.gsub(/@([a-zA-Z0-9-]+)/) do - username = $1 - - username.gsub('-', '_') - end - raw end end diff --git a/spec/components/auth/google_oauth2_authenticator_spec.rb b/spec/components/auth/google_oauth2_authenticator_spec.rb index 17fb1082cec..49bc007201f 100644 --- a/spec/components/auth/google_oauth2_authenticator_spec.rb +++ b/spec/components/auth/google_oauth2_authenticator_spec.rb @@ -10,6 +10,7 @@ describe Auth::GoogleOAuth2Authenticator do user = Fabricate(:user) hash = { + provider: "google_oauth2", uid: "123456789", info: { name: "John Doe", @@ -35,6 +36,7 @@ describe Auth::GoogleOAuth2Authenticator do user = Fabricate(:user) hash = { + provider: "google_oauth2", uid: "123456789", info: { name: "John Doe", @@ -59,9 +61,10 @@ describe Auth::GoogleOAuth2Authenticator do user1 = Fabricate(:user) user2 = Fabricate(:user) - GoogleUserInfo.create!(user_id: user1.id, google_user_id: 100) + UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user1.id, provider_uid: 100) hash = { + provider: "google_oauth2", uid: "100", info: { name: "John Doe", @@ -79,14 +82,17 @@ describe Auth::GoogleOAuth2Authenticator do result = authenticator.after_authenticate(hash, existing_account: user2) expect(result.user.id).to eq(user2.id) - expect(GoogleUserInfo.exists?(user_id: user1.id)).to eq(false) - expect(GoogleUserInfo.exists?(user_id: user2.id)).to eq(true) + expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(false) + expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true) end it 'can create a proper result for non existing users' do hash = { + provider: "google_oauth2", uid: "123456789", info: { + first_name: "Jane", + last_name: "Doe", name: "Jane Doe", email: "jane.doe@the.google.com" }, @@ -103,7 +109,7 @@ describe Auth::GoogleOAuth2Authenticator do result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) - expect(result.extra_data[:name]).to eq("Jane Doe") + expect(result.name).to eq("Jane Doe") end end @@ -116,7 +122,7 @@ describe Auth::GoogleOAuth2Authenticator do end it 'revokes correctly' do - GoogleUserInfo.create!(user_id: user.id, google_user_id: 12345, email: 'someuser@somedomain.tld') + UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user.id, provider_uid: 12345) expect(authenticator.can_revoke?).to eq(true) expect(authenticator.revoke(user)).to eq(true) expect(authenticator.description_for_user(user)).to eq("") diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index f243065e490..a87629e00f6 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -251,6 +251,10 @@ describe Email::Receiver do ) end + let :topic_user do + TopicUser.find_by(topic_id: topic.id, user_id: user.id) + end + it "uses MD5 of 'mail_string' there is no message_id" do mail_string = email(:missing_message_id) expect { Email::Receiver.new(mail_string).process! }.to change { IncomingEmail.count } @@ -285,14 +289,34 @@ describe Email::Receiver do expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicNotFoundError) end - it "raises a TopicClosedError when the topic was closed" do - topic.update_columns(closed: true) - expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError) - end + context "a closed topic" do - it "does not raise TopicClosedError when performing a like action" do - topic.update_columns(closed: true) - expect { process(:like) }.to change(PostAction, :count) + before do + topic.update_columns(closed: true) + end + + it "raises a TopicClosedError when the topic was closed" do + expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError) + end + + it "Can watch topics via the watch command" do + # TODO support other locales as well, the tricky thing is that these string live in + # client.yml not on server yml so it is a bit tricky to find + + topic.update_columns(closed: true) + process(:watch) + expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:watching]) + end + + it "Can mute topics via the mute command" do + process(:mute) + expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:muted]) + end + + it "can track a topic via the track command" do + process(:track) + expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:tracking]) + end end it "raises an InvalidPost when there was an error while creating the post" do diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 8774c6402e9..86ebf54ba1c 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -776,6 +776,28 @@ describe PostCreator do expect(post.topic.topic_allowed_users.where(user_id: admin2.id).count).to eq(0) end + + it 'does not increase posts count for small actions' do + topic = Fabricate(:private_message_topic, user: Fabricate(:user)) + + Fabricate(:post, topic: topic) + + 1.upto(3) do |i| + user = Fabricate(:user) + topic.invite(topic.user, user.username) + topic.reload + expect(topic.posts_count).to eq(1) + expect(topic.posts.where(post_type: Post.types[:small_action]).count).to eq(i) + end + + Fabricate(:post, topic: topic) + Topic.reset_highest(topic.id) + expect(topic.reload.posts_count).to eq(2) + + Fabricate(:post, topic: topic) + Topic.reset_all_highest! + expect(topic.reload.posts_count).to eq(3) + end end context "warnings" do @@ -1238,4 +1260,32 @@ describe PostCreator do end end end + + context "#create_post_notice" do + let(:user) { Fabricate(:user) } + let(:new_user) { Fabricate(:user) } + let(:returning_user) { Fabricate(:user) } + + it "generates post notices" do + # new users + post = PostCreator.create(new_user, title: "one of my first topics", raw: "one of my first posts") + expect(post.custom_fields["post_notice_type"]).to eq("first") + post = PostCreator.create(new_user, title: "another one of my first topics", raw: "another one of my first posts") + expect(post.custom_fields["post_notice_type"]).to eq(nil) + + # returning users + SiteSetting.returning_users_days = 30 + old_post = Fabricate(:post, user: returning_user, created_at: 31.days.ago) + post = PostCreator.create(returning_user, title: "this is a returning topic", raw: "this is a post") + expect(post.custom_fields["post_notice_type"]).to eq("returning") + expect(post.custom_fields["post_notice_time"]).to eq(old_post.created_at.iso8601) + end + + it "does not generate post notices" do + Fabricate(:post, user: user, created_at: 3.days.ago) + post = PostCreator.create(user, title: "this is another topic", raw: "this is my another post") + expect(post.custom_fields["post_notice_type"]).to eq(nil) + expect(post.custom_fields["post_notice_time"]).to eq(nil) + end + end end diff --git a/spec/components/validators/max_emojis_validator_spec.rb b/spec/components/validators/max_emojis_validator_spec.rb index 671caaea999..1242a32a4e8 100644 --- a/spec/components/validators/max_emojis_validator_spec.rb +++ b/spec/components/validators/max_emojis_validator_spec.rb @@ -14,7 +14,9 @@ describe MaxEmojisValidator do shared_examples "validating any topic title" do it 'adds an error when emoji count is greater than SiteSetting.max_emojis_in_title' do SiteSetting.max_emojis_in_title = 3 - record.title = '🧐 Lots of emojis here 🎃 :joy: :)' + CustomEmoji.create!(name: 'trout', upload: Fabricate(:upload)) + Emoji.clear_cache + record.title = '🧐 Lots of emojis here 🎃 :trout: :)' validate expect(record.errors[:title][0]).to eq(I18n.t("errors.messages.max_emojis", max_emojis_count: 3)) diff --git a/spec/fixtures/emails/mute.eml b/spec/fixtures/emails/mute.eml new file mode 100644 index 00000000000..62dea407517 --- /dev/null +++ b/spec/fixtures/emails/mute.eml @@ -0,0 +1,10 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <13@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +mute diff --git a/spec/fixtures/emails/track.eml b/spec/fixtures/emails/track.eml new file mode 100644 index 00000000000..a7d7ad10dfb --- /dev/null +++ b/spec/fixtures/emails/track.eml @@ -0,0 +1,10 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <13@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +track diff --git a/spec/fixtures/emails/watch.eml b/spec/fixtures/emails/watch.eml new file mode 100644 index 00000000000..446ff77b0d7 --- /dev/null +++ b/spec/fixtures/emails/watch.eml @@ -0,0 +1,10 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <13@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +watch diff --git a/spec/jobs/invalidate_inactive_admins_spec.rb b/spec/jobs/invalidate_inactive_admins_spec.rb index 6fd0f309c40..572120f9052 100644 --- a/spec/jobs/invalidate_inactive_admins_spec.rb +++ b/spec/jobs/invalidate_inactive_admins_spec.rb @@ -38,13 +38,13 @@ describe Jobs::InvalidateInactiveAdmins do before do GithubUserInfo.create!(user_id: not_seen_admin.id, screen_name: 'bob', github_user_id: 100) UserOpenId.create!(url: 'https://me.yahoo.com/id/123' , user_id: not_seen_admin.id, email: 'bob@example.com', active: true) - GoogleUserInfo.create!(user_id: not_seen_admin.id, google_user_id: 100, email: 'bob@example.com') + UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: not_seen_admin.id, provider_uid: 100, info: { email: "bob@google.account.com" }) end it 'removes the social logins' do subject expect(GithubUserInfo.where(user_id: not_seen_admin.id).exists?).to eq(false) - expect(GoogleUserInfo.where(user_id: not_seen_admin.id).exists?).to eq(false) + expect(UserAssociatedAccount.where(user_id: not_seen_admin.id).exists?).to eq(false) expect(UserOpenId.where(user_id: not_seen_admin.id).exists?).to eq(false) end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 8419e89976c..c70570411b5 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -221,9 +221,9 @@ describe Group do end describe '.refresh_automatic_group!' do - it "makes sure the everyone group is not visible" do + it "makes sure the everyone group is not visible except to staff" do g = Group.refresh_automatic_group!(:everyone) - expect(g.visibility_level).to eq(Group.visibility_levels[:owners]) + expect(g.visibility_level).to eq(Group.visibility_levels[:staff]) end it "ensures that the moderators group is messageable by all" do diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 15ebea3f32d..bdf7a02faa4 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -477,16 +477,21 @@ describe Invite do end - describe '.rescind_all_invites_from' do - it 'removes all invites sent by a user' do + describe '.rescind_all_expired_invites_from' do + it 'removes all expired invites sent by a user' do + SiteSetting.invite_expiry_days = 1 user = Fabricate(:user) invite_1 = Fabricate(:invite, invited_by: user) invite_2 = Fabricate(:invite, invited_by: user) - Invite.rescind_all_invites_from(user) + expired_invite = Fabricate(:invite, invited_by: user) + expired_invite.update!(created_at: 2.days.ago) + Invite.rescind_all_expired_invites_from(user) invite_1.reload invite_2.reload - expect(invite_1.deleted_at).to be_present - expect(invite_2.deleted_at).to be_present + expired_invite.reload + expect(invite_1.deleted_at).to eq(nil) + expect(invite_2.deleted_at).to eq(nil) + expect(expired_invite.deleted_at).to be_present end end end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 9ddfccfb3fd..de5c367d19b 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -134,6 +134,29 @@ describe Post do end end + context 'a post with notices' do + let(:post) { + post = Fabricate(:post, post_args) + post.custom_fields["post_notice_type"] = "returning" + post.custom_fields["post_notice_time"] = 1.day.ago + post + } + + before do + post.trash! + post.reload + end + + describe 'recovery' do + it 'deletes notices' do + post.recover! + + expect(post.custom_fields).not_to have_key("post_notice_type") + expect(post.custom_fields).not_to have_key("post_notice_time") + end + end + end + end describe 'flagging helpers' do diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 0b4a332c9a7..3b0b442e697 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -310,6 +310,18 @@ HTML scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id) expect(scss).to include("font-size:30px") + + # Escapes correctly. If not, compiling this would throw an exception + setting.value = <<~MULTILINE + \#{$fakeinterpolatedvariable} + andanothervalue 'withquotes'; margin: 0; + MULTILINE + + theme.set_field(target: :common, name: :scss, value: 'body {font-size: quote($font-size)}') + theme.save! + + scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id) + expect(scss).to include('font-size:"#{$fakeinterpolatedvariable}\a andanothervalue \'withquotes\'; margin: 0;\a"') end it "allows values to be used in JS" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0bbea094a75..a1fbdcc0192 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -428,7 +428,7 @@ describe User do UserAssociatedAccount.create(user_id: user.id, provider_name: "twitter", provider_uid: "1", info: { nickname: "sam" }) UserAssociatedAccount.create(user_id: user.id, provider_name: "facebook", provider_uid: "1234", info: { email: "test@example.com" }) UserAssociatedAccount.create(user_id: user.id, provider_name: "instagram", provider_uid: "examplel123123", info: { nickname: "sam" }) - GoogleUserInfo.create(user_id: user.id, email: "sam@sam.com", google_user_id: 1) + UserAssociatedAccount.create(user_id: user.id, provider_name: "google_oauth2", provider_uid: "1", info: { email: "sam@sam.com" }) GithubUserInfo.create(user_id: user.id, screen_name: "sam", github_user_id: 1) user.reload diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb index 89beb7042d0..3156c35777f 100644 --- a/spec/models/web_hook_spec.rb +++ b/spec/models/web_hook_spec.rb @@ -257,6 +257,33 @@ describe WebHook do expect(payload["id"]).to eq(post.topic.id) end + it 'should enqueue the destroyed hooks with tag filter for post events' do + tag = Fabricate(:tag) + Fabricate(:web_hook, tags: [tag]) + + post = PostCreator.create!(user, + raw: 'post', + topic_id: topic.id, + reply_to_post_number: 1, + skip_validations: true + ) + + topic.tags = [tag] + topic.save! + + Jobs::EmitWebHookEvent.jobs.clear + PostDestroyer.new(user, post).destroy + + job = Jobs::EmitWebHookEvent.new + job.expects(:web_hook_request).times(2) + + args = Jobs::EmitWebHookEvent.jobs[1]["args"].first + job.execute(args.with_indifferent_access) + + args = Jobs::EmitWebHookEvent.jobs[2]["args"].first + job.execute(args.with_indifferent_access) + end + it 'should enqueue the right hooks for user events' do Fabricate(:user_web_hook, active: true) diff --git a/spec/requests/admin/email_templates_controller_spec.rb b/spec/requests/admin/email_templates_controller_spec.rb index 150112d3541..b0d3a0a0550 100644 --- a/spec/requests/admin/email_templates_controller_spec.rb +++ b/spec/requests/admin/email_templates_controller_spec.rb @@ -214,6 +214,21 @@ RSpec.describe Admin::EmailTemplatesController do end end + context "when subject has plural keys" do + it "doesn't update the subject" do + old_subject = I18n.t('system_messages.pending_users_reminder.subject_template') + expect(old_subject).to be_a(Hash) + + put '/admin/customize/email_templates/system_messages.pending_users_reminder', params: { + email_template: { subject: '', body: 'Lorem ipsum' } + }, headers: headers + + expect(response.status).to eq(200) + + expect(I18n.t('system_messages.pending_users_reminder.subject_template')).to eq(old_subject) + expect(I18n.t('system_messages.pending_users_reminder.text_body_template')).to eq('Lorem ipsum') + end + end end end diff --git a/spec/requests/omniauth_callbacks_controller_spec.rb b/spec/requests/omniauth_callbacks_controller_spec.rb index d4b424344e7..8296ac3362e 100644 --- a/spec/requests/omniauth_callbacks_controller_spec.rb +++ b/spec/requests/omniauth_callbacks_controller_spec.rb @@ -96,7 +96,9 @@ RSpec.describe Users::OmniauthCallbacksController do uid: '123545', info: OmniAuth::AuthHash::InfoHash.new( email: email, - name: 'Some name' + name: 'Some name', + first_name: "Some", + last_name: "name" ), extra: { raw_info: OmniAuth::AuthHash.new( @@ -107,7 +109,7 @@ RSpec.describe Users::OmniauthCallbacksController do gender: 'male', name: "Some name Huh", ) - }, + } ) Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] @@ -262,7 +264,7 @@ RSpec.describe Users::OmniauthCallbacksController do @sso.return_sso_url = "http://somewhere.over.rainbow/sso" cookies[:sso_payload] = @sso.payload - GoogleUserInfo.create!(google_user_id: '12345', user: user) + UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user) OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( provider: 'google_oauth2', @@ -299,7 +301,7 @@ RSpec.describe Users::OmniauthCallbacksController do context 'when user has not verified his email' do before do - GoogleUserInfo.create!(google_user_id: '12345', user: user) + UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user) user.update!(active: false) OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( @@ -341,8 +343,8 @@ RSpec.describe Users::OmniauthCallbacksController do context 'when attempting reconnect' do let(:user2) { Fabricate(:user) } before do - GoogleUserInfo.create!(google_user_id: '12345', user: user) - GoogleUserInfo.create!(google_user_id: '123456', user: user2) + UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user) + UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '123456', user: user2) OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( provider: 'google_oauth2', @@ -385,7 +387,7 @@ RSpec.describe Users::OmniauthCallbacksController do get "/auth/google_oauth2/callback.json" expect(response.status).to eq(200) expect(session[:current_user_id]).to eq(user2.id) - expect(GoogleUserInfo.count).to eq(2) + expect(UserAssociatedAccount.count).to eq(2) end it 'should reconnect if parameter supplied' do @@ -402,7 +404,7 @@ RSpec.describe Users::OmniauthCallbacksController do expect(session[:auth_reconnect]).to eq(nil) # Disconnect - GoogleUserInfo.find_by(user_id: user.id).destroy + UserAssociatedAccount.find_by(user_id: user.id).destroy # Reconnect flow: get "/auth/google_oauth2?reconnect=true" @@ -414,7 +416,7 @@ RSpec.describe Users::OmniauthCallbacksController do expect(response.status).to eq(200) expect(JSON.parse(response.body)["authenticated"]).to eq(true) expect(session[:current_user_id]).to eq(user.id) - expect(GoogleUserInfo.count).to eq(1) + expect(UserAssociatedAccount.count).to eq(1) end end diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb index 6c400cd1392..0829f3ee0ec 100644 --- a/spec/requests/posts_controller_spec.rb +++ b/spec/requests/posts_controller_spec.rb @@ -248,15 +248,22 @@ describe PostsController do let(:moderator) { Fabricate(:moderator) } before do + sign_in(moderator) PostAction.act(moderator, post1, PostActionType.types[:off_topic]) PostAction.act(moderator, post2, PostActionType.types[:off_topic]) Jobs::SendSystemMessage.clear end - it "defers the posts" do - sign_in(moderator) + it "defers the child posts by default" do expect(PostAction.flagged_posts_count).to eq(2) - delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id], defer_flags: true } + delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id] } + expect(Jobs::SendSystemMessage.jobs.size).to eq(1) + expect(PostAction.flagged_posts_count).to eq(0) + end + + it "can defer all posts based on `agree_with_first_reply_flag` param" do + expect(PostAction.flagged_posts_count).to eq(2) + delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id], agree_with_first_reply_flag: false } expect(Jobs::SendSystemMessage.jobs.size).to eq(0) expect(PostAction.flagged_posts_count).to eq(0) end diff --git a/spec/services/user_anonymizer_spec.rb b/spec/services/user_anonymizer_spec.rb index f1b396032e9..87a652925e7 100644 --- a/spec/services/user_anonymizer_spec.rb +++ b/spec/services/user_anonymizer_spec.rb @@ -190,7 +190,6 @@ describe UserAnonymizer do end it "removes external auth assocations" do - user.google_user_info = GoogleUserInfo.create(user_id: user.id, google_user_id: "google@gmail.com") user.github_user_info = GithubUserInfo.create(user_id: user.id, screen_name: "example", github_user_id: "examplel123123") user.user_associated_accounts = [UserAssociatedAccount.create(user_id: user.id, provider_uid: "example", provider_name: "facebook")] user.single_sign_on_record = SingleSignOnRecord.create(user_id: user.id, external_id: "example", last_payload: "looks good") @@ -198,7 +197,6 @@ describe UserAnonymizer do UserOpenId.create(user_id: user.id, email: user.email, url: "http://example.com/openid", active: true) make_anonymous user.reload - expect(user.google_user_info).to eq(nil) expect(user.github_user_info).to eq(nil) expect(user.user_associated_accounts).to be_empty expect(user.single_sign_on_record).to eq(nil) diff --git a/spec/services/user_merger_spec.rb b/spec/services/user_merger_spec.rb index 304bb0cdb64..86025a5ef4e 100644 --- a/spec/services/user_merger_spec.rb +++ b/spec/services/user_merger_spec.rb @@ -978,7 +978,6 @@ describe UserMerger do it "deletes external auth infos of source user" do UserAssociatedAccount.create(user_id: source_user.id, provider_name: "facebook", provider_uid: "1234") GithubUserInfo.create(user_id: source_user.id, screen_name: "example", github_user_id: "examplel123123") - GoogleUserInfo.create(user_id: source_user.id, google_user_id: "google@gmail.com") Oauth2UserInfo.create(user_id: source_user.id, uid: "example", provider: "example") SingleSignOnRecord.create(user_id: source_user.id, external_id: "example", last_payload: "looks good") UserOpenId.create(user_id: source_user.id, email: source_user.email, url: "http://example.com/openid", active: true) @@ -987,7 +986,6 @@ describe UserMerger do expect(UserAssociatedAccount.where(user_id: source_user.id).count).to eq(0) expect(GithubUserInfo.where(user_id: source_user.id).count).to eq(0) - expect(GoogleUserInfo.where(user_id: source_user.id).count).to eq(0) expect(Oauth2UserInfo.where(user_id: source_user.id).count).to eq(0) expect(SingleSignOnRecord.where(user_id: source_user.id).count).to eq(0) expect(UserOpenId.where(user_id: source_user.id).count).to eq(0) diff --git a/spec/services/user_updater_spec.rb b/spec/services/user_updater_spec.rb index 609099ad4af..d4a10da977b 100644 --- a/spec/services/user_updater_spec.rb +++ b/spec/services/user_updater_spec.rb @@ -22,7 +22,27 @@ describe UserUpdater do expect(MutedUser.where(user_id: u2.id).count).to eq 2 expect(MutedUser.where(user_id: u1.id).count).to eq 2 expect(MutedUser.where(user_id: u3.id).count).to eq 0 + end + end + describe '#update_ignored_users' do + it 'updates ignored users' do + u1 = Fabricate(:user) + u2 = Fabricate(:user) + u3 = Fabricate(:user) + + updater = UserUpdater.new(u1, u1) + updater.update_ignored_users("#{u2.username},#{u3.username}") + + updater = UserUpdater.new(u2, u2) + updater.update_ignored_users("#{u3.username},#{u1.username}") + + updater = UserUpdater.new(u3, u3) + updater.update_ignored_users("") + + expect(IgnoredUser.where(user_id: u2.id).count).to eq 2 + expect(IgnoredUser.where(user_id: u1.id).count).to eq 2 + expect(IgnoredUser.where(user_id: u3.id).count).to eq 0 end end diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6 index 9ad9d448ccc..fcbff948dc7 100644 --- a/test/javascripts/acceptance/composer-test.js.es6 +++ b/test/javascripts/acceptance/composer-test.js.es6 @@ -600,6 +600,24 @@ QUnit.test("Checks for existing draft", async assert => { toggleCheckDraftPopup(false); }); +QUnit.test("Loading draft also replaces the recipients", async assert => { + toggleCheckDraftPopup(true); + + // prettier-ignore + server.get("/draft.json", () => { // eslint-disable-line no-undef + return [ 200, { "Content-Type": "application/json" }, { + "draft":"{\"reply\":\"hello\",\"action\":\"privateMessage\",\"title\":\"hello\",\"categoryId\":null,\"archetypeId\":\"private_message\",\"metaData\":null,\"usernames\":\"codinghorror\",\"composerTime\":9159,\"typingTime\":2500}", + "draft_sequence":0 + } ]; + }); + + await visit("/u/charlie"); + await click("button.compose-pm"); + await click(".modal .btn-default"); + + assert.equal(find(".users-input .item:eq(0)").text(), "codinghorror"); +}); + const assertImageResized = (assert, uploads) => { assert.equal( find(".d-editor-input").val(), diff --git a/test/javascripts/fixtures/user_fixtures.js.es6 b/test/javascripts/fixtures/user_fixtures.js.es6 index 51df0412233..203f3a76410 100644 --- a/test/javascripts/fixtures/user_fixtures.js.es6 +++ b/test/javascripts/fixtures/user_fixtures.js.es6 @@ -2276,5 +2276,334 @@ export default { } ] } + }, + "/u/charlie.json": { + user_badges: [ + { + id: 17, + granted_at: "2019-03-06T19:08:28.230Z", + count: 1, + badge_id: 3, + user_id: 5, + granted_by_id: -1 + } + ], + badges: [ + { + id: 3, + name: "Regular", + description: + '\u003ca href="https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/"\u003eGranted\u003c/a\u003e recategorize, rename, followed links, wiki, more likes', + grant_count: 3, + allow_title: true, + multiple_grant: false, + icon: "fa-user", + image: null, + listable: true, + enabled: true, + badge_grouping_id: 4, + system: true, + slug: "regular", + manually_grantable: false, + badge_type_id: 2 + } + ], + badge_types: [{ id: 2, name: "Silver", sort_order: 8 }], + users: [ + { + id: 5, + username: "charlie", + name: null, + avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png", + moderator: false, + admin: false + }, + { + id: -1, + username: "system", + name: "system", + avatar_template: "/user_avatar/localhost/system/{size}/2_2.png", + moderator: true, + admin: true + } + ], + user: { + id: 5, + username: "charlie", + name: null, + avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png", + last_posted_at: null, + last_seen_at: null, + created_at: "2019-03-06T19:06:20.340Z", + can_edit: true, + can_edit_username: true, + can_edit_email: true, + can_edit_name: true, + ignored: false, + can_ignore_user: false, + can_send_private_messages: true, + can_send_private_message_to_user: true, + trust_level: 3, + moderator: false, + admin: false, + title: null, + uploaded_avatar_id: null, + badge_count: 3, + has_title_badges: true, + custom_fields: {}, + pending_count: 0, + profile_view_count: 1, + time_read: 0, + recent_time_read: 0, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + staged: false, + second_factor_enabled: false, + post_count: 0, + can_be_deleted: true, + can_delete_all_posts: true, + locale: null, + muted_category_ids: [], + watched_tags: [], + watching_first_post_tags: [], + tracked_tags: [], + muted_tags: [], + tracked_category_ids: [], + watched_category_ids: [], + watched_first_post_category_ids: [], + system_avatar_upload_id: null, + system_avatar_template: + "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png", + muted_usernames: [], + ignored_usernames: [], + mailing_list_posts_per_day: 0, + can_change_bio: true, + user_api_keys: null, + user_auth_tokens: [], + user_auth_token_logs: [], + invited_by: null, + groups: [ + { + id: 10, + automatic: true, + name: "trust_level_0", + display_name: "trust_level_0", + user_count: 14, + mentionable_level: 0, + messageable_level: 0, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 3, + membership_request_template: null + }, + { + id: 11, + automatic: true, + name: "trust_level_1", + display_name: "trust_level_1", + user_count: 9, + mentionable_level: 0, + messageable_level: 0, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 3, + membership_request_template: null + }, + { + id: 12, + automatic: true, + name: "trust_level_2", + display_name: "trust_level_2", + user_count: 6, + mentionable_level: 0, + messageable_level: 0, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 3, + membership_request_template: null + }, + { + id: 13, + automatic: true, + name: "trust_level_3", + display_name: "trust_level_3", + user_count: 3, + mentionable_level: 0, + messageable_level: 0, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 3, + membership_request_template: null + } + ], + group_users: [ + { group_id: 10, user_id: 5, notification_level: 3 }, + { group_id: 11, user_id: 5, notification_level: 3 }, + { group_id: 12, user_id: 5, notification_level: 3 }, + { group_id: 13, user_id: 5, notification_level: 3 } + ], + featured_user_badge_ids: [17], + user_option: { + user_id: 5, + email_always: false, + mailing_list_mode: false, + mailing_list_mode_frequency: 1, + email_digests: true, + email_private_messages: true, + email_direct: true, + external_links_in_new_tab: false, + dynamic_favicon: false, + enable_quoting: true, + disable_jump_reply: false, + digest_after_minutes: 10080, + automatically_unpin_topics: true, + auto_track_topics_after_msecs: 240000, + notification_level_when_replying: 2, + new_topic_duration_minutes: 2880, + email_previous_replies: 2, + email_in_reply_to: true, + like_notification_frequency: 1, + include_tl0_in_digests: false, + theme_ids: [2], + theme_key_seq: 0, + allow_private_messages: true, + homepage_id: null, + hide_profile_and_presence: false, + text_size: "normal", + text_size_seq: 0 + } + } + }, + "/u/charlie/summary.json": { + topics: [], + badges: [ + { + id: 3, + name: "Regular", + description: + '\u003ca href="https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/"\u003eGranted\u003c/a\u003e recategorize, rename, followed links, wiki, more likes', + grant_count: 3, + allow_title: true, + multiple_grant: false, + icon: "fa-user", + image: null, + listable: true, + enabled: true, + badge_grouping_id: 4, + system: true, + slug: "regular", + manually_grantable: false, + badge_type_id: 2 + } + ], + badge_types: [{ id: 2, name: "Silver", sort_order: 8 }], + users: [ + { + id: 5, + username: "charlie", + name: null, + avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png", + moderator: false, + admin: false + }, + { + id: -1, + username: "system", + name: "system", + avatar_template: "/user_avatar/localhost/system/{size}/2_2.png", + moderator: true, + admin: true + } + ], + user_summary: { + likes_given: 0, + likes_received: 0, + topics_entered: 0, + posts_read_count: 0, + days_visited: 0, + topic_count: 0, + post_count: 0, + time_read: 0, + recent_time_read: 0, + topic_ids: [], + replies: [], + links: [], + most_liked_by_users: [], + most_liked_users: [], + most_replied_to_users: [], + badges: [ + { + id: 17, + granted_at: "2019-03-06T19:08:28.230Z", + count: 1, + badge_id: 3, + user_id: 5, + granted_by_id: -1 + } + ], + top_categories: [] + } } }; diff --git a/test/javascripts/widgets/post-test.js.es6 b/test/javascripts/widgets/post-test.js.es6 index 12b5726ff14..5dc1b8ad50d 100644 --- a/test/javascripts/widgets/post-test.js.es6 +++ b/test/javascripts/widgets/post-test.js.es6 @@ -852,3 +852,22 @@ widgetTest("pm map", { assert.equal(find(".private-message-map .user").length, 1); } }); + +widgetTest("post notice", { + template: '{{mount-widget widget="post" args=args}}', + beforeEach() { + this.set("args", { + postNoticeType: "returning", + postNoticeTime: new Date("2010-01-01 12:00:00 UTC"), + username: "codinghorror" + }); + }, + test(assert) { + assert.equal( + find(".post-notice") + .text() + .trim(), + I18n.t("post.notice.return", { user: "codinghorror", time: "Jan '10" }) + ); + } +}); diff --git a/vendor/assets/svg-icons/fontawesome/brands.svg b/vendor/assets/svg-icons/fontawesome/brands.svg index ecf73b20ed6..1c46f9a9a31 100644 --- a/vendor/assets/svg-icons/fontawesome/brands.svg +++ b/vendor/assets/svg-icons/fontawesome/brands.svg @@ -1,8 +1,4 @@ - 500px @@ -14,16 +10,20 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Accusoft - + - + Acquisitions Incorporated - + App.net + + Adobe + + Adversal @@ -50,7 +50,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Amilia - + Android @@ -88,10 +88,18 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Apple Pay + + Artstation + + Asymmetrik, Ltd. + + Atlassian + + Audible @@ -128,9 +136,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL BIMobject - + Bitbucket - + Bitcoin @@ -176,13 +184,17 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL BuySellAds + + Canadian Maple Leaf + + Amazon Pay Credit Card American Express Credit Card - + Apple Pay Credit Card @@ -194,7 +206,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Discover Credit Card - + JCB Credit Card @@ -210,7 +222,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Stripe Credit Card - + Visa Credit Card @@ -220,6 +232,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Centercode + + Centos + + Chrome @@ -244,6 +260,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Codie Pie + + Confluence + + Connect Develop @@ -254,7 +274,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL cPanel - + Creative Commons @@ -285,8 +305,8 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL - Creative Commons Public Domain Alternate - + Alternate Creative Commons Public Domain + Creative Commons Remix @@ -334,15 +354,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL D&D Beyond - + DashCube - Delicious Logo - + Delicious + deploy.dog @@ -360,13 +380,21 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL deviantART + + DHL + + + + Diaspora + + Digg Logo Digital Ocean - + Discord @@ -386,7 +414,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Draft2digital - + Dribbble @@ -414,7 +442,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL eBay - + Edge Browser @@ -430,7 +458,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Ember - + Galactic Empire @@ -442,7 +470,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Erlang - + Ethereum @@ -462,7 +490,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Facebook F - + Facebook Messenger @@ -476,9 +504,21 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Fantasy Flight-games + + FedEx + + + + Fedora + + + + Figma + + Firefox - + First Order @@ -510,7 +550,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Font Awesome - + Font Awesome Flag @@ -522,7 +562,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Fonticons - + Fonticons Fi @@ -534,7 +574,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Fort Awesome - + Forumbee @@ -602,7 +642,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL GitLab - + Gitter @@ -666,11 +706,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Gripfire, Inc. - + Grunt - + Gulp @@ -690,7 +730,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hips - + HireAHelper @@ -698,7 +738,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hooli - + Hornbill @@ -722,16 +762,24 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL IMDB - + Instagram + + Intercom + + Internet-explorer + + InVision + + ioxhost @@ -746,19 +794,23 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Java - + Jedi Order - + Jenkis + + Jira + + Joget - + Joomla Logo @@ -776,9 +828,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL jsFiddle - + Kaggle - + Keybase @@ -838,7 +890,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Linux - + lyft @@ -848,9 +900,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Magento - + Mailchimp - + Mandalorian @@ -858,7 +910,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Markdown - + Mastodon @@ -890,7 +942,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Megaport - + + + + Mendeley + Microsoft @@ -978,7 +1034,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Optin Monster - + Open Source Initiative @@ -1034,7 +1090,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Pied Piper Logo - + Pied Piper-hat @@ -1070,7 +1126,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Python - + QQ @@ -1078,7 +1134,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL QuinScape - + Quora @@ -1088,17 +1144,21 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL R Project + + Raspberry Pi + + Ravelry React - + ReactEurope - + ReadMe @@ -1124,6 +1184,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL reddit Square + + Redhat + + Renren @@ -1158,7 +1222,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Sass - + SCHLIX @@ -1186,7 +1250,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Shirts in Bulk - + Shopware @@ -1204,6 +1268,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Sith + + Sketch + + skyatlas @@ -1214,7 +1282,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Slack Logo - + Slack Hashtag @@ -1222,7 +1290,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Slideshare - + Snapchat @@ -1240,6 +1308,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL SoundCloud + + Sourcetree + + Speakap @@ -1258,11 +1330,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Stack Overflow - + StayLinked - + Steam @@ -1278,19 +1350,19 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Sticker Mule - + Strava - + Stripe - + Stripe S - + Studio Vinari @@ -1312,9 +1384,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Supple + + Suse + + TeamSpeak - + Telegram @@ -1328,9 +1404,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Tencent Weibo - + The Red Yeti - + Themeco @@ -1342,15 +1418,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Think Peaks - + Trade Federation - + Trello - + TripAdvisor @@ -1378,28 +1454,40 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Typo3 - + Uber + + Ubuntu + + UIkit Uniregistry - + Untappd + + UPS + + USB + + United States Postal Service + + us-Sunnah Foundation @@ -1548,9 +1636,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Yandex International - + + Yarn + + + Yelp - + Yoast diff --git a/vendor/assets/svg-icons/fontawesome/regular.svg b/vendor/assets/svg-icons/fontawesome/regular.svg index 8a77d05c68b..c9b33b62427 100644 --- a/vendor/assets/svg-icons/fontawesome/regular.svg +++ b/vendor/assets/svg-icons/fontawesome/regular.svg @@ -1,8 +1,4 @@ - Address Book @@ -106,7 +102,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Clipboard - + Clock @@ -174,11 +170,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Eye - + - + Eye Slash - + File @@ -190,7 +186,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Archive File - + Audio File @@ -198,7 +194,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Code File - + Excel File @@ -238,7 +234,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Folder Open - + Font Awesome Full Logo @@ -586,7 +582,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Trash - + User diff --git a/vendor/assets/svg-icons/fontawesome/solid.svg b/vendor/assets/svg-icons/fontawesome/solid.svg index 4654dee7e14..aa652f5d0c2 100644 --- a/vendor/assets/svg-icons/fontawesome/solid.svg +++ b/vendor/assets/svg-icons/fontawesome/solid.svg @@ -1,8 +1,4 @@ - + + Baby + + + + Baby Carriage + + Backspace @@ -204,6 +208,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL backward + + Bacon + + Balance Scale @@ -288,6 +296,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Binoculars + + Biohazard + + Birthday Cake @@ -304,6 +316,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Blind + + Blog + + bold @@ -330,7 +346,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Book of the Dead - + + + + Medical Book + Book Open @@ -354,7 +374,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Box Open - + Boxes @@ -368,6 +388,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Brain + + Bread Slice + + Briefcase @@ -430,12 +454,16 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Calendar - + Calendar Check + + Calendar with Day Focus + + Calendar Minus @@ -448,6 +476,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Calendar Times + + Calendar with Week Focus + + camera @@ -460,6 +492,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Campground + + Candy Cane + + Cannabis @@ -520,6 +556,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Caret Up + + Carrot + + Shopping Cart Arrow Down @@ -528,6 +568,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Add to Shopping Cart + + Cash Register + + Cat @@ -538,7 +582,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Chair - + Chalkboard @@ -584,9 +628,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Check Square + + Cheese + + Chess - + Chess Bishop @@ -594,7 +642,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Chess Board - + Chess King @@ -668,6 +716,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL City + + Medical Clinic + + Clipboard @@ -750,7 +802,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL cog - + cogs @@ -780,6 +832,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Comment Dots + + Alternate Medical Chat + + Comment Slash @@ -804,6 +860,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Compress + + Alternate Compress Arrows + + Concierge Bell @@ -856,6 +916,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Crown + + Crutch + + Cube @@ -1010,12 +1074,20 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Drumstick with Bite Taken Out - + Dumbbell + + Dumpster + + + + Dumpster Fire + + Dungeon @@ -1024,6 +1096,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Edit + + Egg + + eject @@ -1060,6 +1136,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL eraser + + Ethernet + + Euro Sign @@ -1098,15 +1178,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Eye - + Eye Dropper - + Eye Slash - + fast-backward @@ -1146,7 +1226,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Archive File - + Audio File @@ -1174,7 +1254,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL File Export - + Image File @@ -1182,7 +1262,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL File Import - + File Invoice @@ -1250,7 +1330,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL fire - + + + + Alternate Fire + fire-extinguisher @@ -1274,7 +1358,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL flag-checkered - + United States of America Flag @@ -1306,7 +1390,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL font - + Font Awesome Full Logo @@ -1366,7 +1450,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL gift - + + + + Gifts + + + + Glass Cheers + Martini Glass @@ -1376,6 +1468,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Glass Martini + + Glass Whiskey + + Glasses @@ -1396,13 +1492,17 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Globe with Asia shown + + Globe with Europe shown + + Golf Ball Gopuram - + Graduation Cap @@ -1476,14 +1576,30 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Grip Horizontal + + Grip Lines + + + + Grip Lines Vertical + + Grip Vertical + + Guitar + + H Square + + Hamburger + + Hammer @@ -1508,6 +1624,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Lizard (Hand) + + Hand with Middle Finger Raised + + Paper (Hand) @@ -1538,7 +1658,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Rock (Hand) - + Scissors (Hand) @@ -1546,7 +1666,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Spock (Hand) - + Hands @@ -1564,6 +1684,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hanukiah + + Hard Hat + + Hashtag @@ -1600,6 +1724,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Heart + + Heart Broken + + Heartbeat @@ -1628,14 +1756,22 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hockey Puck + + Holly Berry + + home - + Horse + + Horse Head + + hospital @@ -1652,6 +1788,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hot Tub + + Hot Dog + + Hotel @@ -1684,6 +1824,14 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL I Beam Cursor + + Ice Cream + + + + Icicles + + Identification Badge @@ -1696,6 +1844,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Identification Card + + Igloo + + Image @@ -1782,7 +1934,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Language - + Laptop @@ -1792,6 +1944,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Laptop Code + + Laptop Medical + + Grinning Face With Big Eyes @@ -2048,6 +2204,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Minus Square + + Mitten + + Mobile Phone @@ -2098,7 +2258,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Motorcycle - + Mountain @@ -2108,6 +2268,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Mouse Pointer + + Mug Hot + + Music @@ -2156,6 +2320,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Outdent + + Pager + + Paint Brush @@ -2252,6 +2420,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL People Carry + + Hot Pepper + + Percent @@ -2288,8 +2460,12 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Pills + + Pizza Slice + + - Place Of Worship + Place of Worship @@ -2428,6 +2604,14 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Quran + + Radiation + + + + Alternate Radiation + + Rainbow @@ -2468,6 +2652,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Republican + + Restroom + + Retweet @@ -2490,7 +2678,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL rocket - + Route @@ -2540,6 +2728,14 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Loudly Crying Face + + Satellite + + + + Satellite Dish + + Save @@ -2556,6 +2752,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Scroll + + Sd Card + + Search @@ -2610,7 +2810,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Shield - + Ship @@ -2664,14 +2864,30 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL signal - + Signature - + + + + SIM Card + Sitemap + + Skating + + + + Skiing + + + + Skiing Nordic + + Skull @@ -2684,6 +2900,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Slash + + Sleigh + + Horizontal Sliders @@ -2712,10 +2932,26 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Smoking Ban + + SMS + + + + Snowboarding + + Snowflake + + Snowman + + + + Snowplow + + Socks @@ -2730,11 +2966,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Sort Alpha Down - + Sort Alpha Up - + Sort Amount Down @@ -2793,7 +3029,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL - Square Root Alternate + Alternate Square Root @@ -2878,7 +3114,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL subscript - + Subway @@ -2898,7 +3134,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL superscript - + Hushed Face @@ -2992,6 +3228,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Low Temperature + + Tenge + + Terminal @@ -3086,7 +3326,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Toggle On - + + + + Toilet + Toilet Paper @@ -3096,13 +3340,17 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Toolbox + + Tools + + Tooth - + Torah - + Torii Gate @@ -3114,7 +3362,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Trademark - + Traffic Light @@ -3124,6 +3372,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Train + + Tram + + Transgender @@ -3134,11 +3386,19 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Trash - + Alternate Trash - + + + + Trash Restore + + + + Alternative Trash Restore + Tree @@ -3277,7 +3537,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL - user-md + Doctor @@ -3288,6 +3548,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL User Ninja + + Nurse + + User Plus @@ -3462,7 +3726,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Won Sign - + Wrench @@ -3474,7 +3738,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Yen Sign - + Yin Yang diff --git a/yarn.lock b/yarn.lock index 4aac87712db..8e7bf1979b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,10 +87,10 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@fortawesome/fontawesome-free@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.5.0.tgz#0c6c53823d04457ae669cd19567b8a21dbb4fcfd" - integrity sha512-p4lu0jfj5QN013ddArh99r3OXZ/fp9rbovs62LfaO70OMBsAXxtNd0lAq/97fitrscR0fqfd+/a5KNcp6Sh/0A== +"@fortawesome/fontawesome-free@5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.7.2.tgz#1498c3eb78ee7c78c5488418707de90aaf58d5d7" + integrity sha512-Ha4HshKdCVKgu4TVCtG8XyPPYdzTzNW4/fvPnn+LT7AosRABryhlRv4cc4+o84dgpvVJN9reN7jo/c+nYujFug== "@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.3.0": version "1.3.0"