diff --git a/app/assets/javascripts/discourse/app/components/user-avatar-flair.js b/app/assets/javascripts/discourse/app/components/user-avatar-flair.js new file mode 100644 index 00000000000..21464bab2c7 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-avatar-flair.js @@ -0,0 +1,38 @@ +import MountWidget from "discourse/components/mount-widget"; +import { observes } from "discourse-common/utils/decorators"; +import autoGroupFlairForUser from "discourse/lib/avatar-flair"; + +export default MountWidget.extend({ + widget: "avatar-flair", + + @observes("user") + _rerender() { + this.queueRerender(); + }, + + buildArgs() { + if (!this.user) { + return; + } + + if (this.user.primary_group_flair_url) { + return { + primary_group_flair_url: this.user.primary_group_flair_url, + primary_group_flair_bg_color: this.user.primary_group_flair_bg_color, + primary_group_flair_color: this.user.primary_group_flair_color, + primary_group_name: this.user.primary_group_name, + }; + } else { + const autoFlairAttrs = autoGroupFlairForUser(this.site, this.user); + if (autoFlairAttrs) { + return { + primary_group_flair_url: autoFlairAttrs.primary_group_flair_url, + primary_group_flair_bg_color: + autoFlairAttrs.primary_group_flair_bg_color, + primary_group_flair_color: autoFlairAttrs.primary_group_flair_color, + primary_group_name: autoFlairAttrs.primary_group_name, + }; + } + } + }, +}); diff --git a/app/assets/javascripts/discourse/app/lib/avatar-flair.js b/app/assets/javascripts/discourse/app/lib/avatar-flair.js new file mode 100644 index 00000000000..5e6ee0e30c9 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/avatar-flair.js @@ -0,0 +1,66 @@ +let _autoGroupFlair, _noAutoFlair; + +export default function autoGroupFlairForUser(site, user) { + if (!_autoGroupFlair) { + initializeAutoGroupFlair(site); + } + + if (_noAutoFlair) { + // No automatic groups have flair. + return null; + } + + if (user.admin && _autoGroupFlair.admins) { + return _autoGroupFlair.admins; + } + + if (user.moderator && _autoGroupFlair.moderators) { + return _autoGroupFlair.moderators; + } + + if (_autoGroupFlair.staff && (user.admin || user.moderator)) { + return _autoGroupFlair.staff; + } + + let trustLevel = user.trust_level || user.trustLevel; + + if (trustLevel) { + for (let i = trustLevel; i >= 0; i--) { + if (_autoGroupFlair[`trust_level_${i}`]) { + return _autoGroupFlair[`trust_level_${i}`]; + } + } + } +} + +export function resetFlair() { + _autoGroupFlair = null; + _noAutoFlair = null; +} + +function initializeAutoGroupFlair(site) { + _autoGroupFlair = {}; + _noAutoFlair = true; + + [ + "admins", + "moderators", + "staff", + "trust_level_0", + "trust_level_1", + "trust_level_2", + "trust_level_3", + "trust_level_4", + ].forEach((groupName) => { + const group = site.groups.findBy("name", groupName); + if (group && group.flair_url) { + _noAutoFlair = false; + _autoGroupFlair[groupName] = { + primary_group_flair_url: group.flair_url, + primary_group_flair_bg_color: group.flair_bg_color, + primary_group_flair_color: group.flair_color, + primary_group_name: group.name.replace(/_/g, " "), + }; + } + }); +} diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js index 276f806b196..113a7a766a6 100644 --- a/app/assets/javascripts/discourse/app/lib/transform-post.js +++ b/app/assets/javascripts/discourse/app/lib/transform-post.js @@ -81,6 +81,7 @@ export function transformBasicPost(post) { userCustomFields: post.user_custom_fields, readCount: post.readers_count, canPublishPage: false, + trustLevel: post.trust_level, }; _additionalAttributes.forEach((a) => (postAtts[a] = post[a])); diff --git a/app/assets/javascripts/discourse/app/templates/components/groups-form-profile-fields.hbs b/app/assets/javascripts/discourse/app/templates/components/groups-form-profile-fields.hbs index 5be10c58309..e8ef91e4a28 100644 --- a/app/assets/javascripts/discourse/app/templates/components/groups-form-profile-fields.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/groups-form-profile-fields.hbs @@ -26,6 +26,12 @@ {{d-editor value=model.bio_raw class="group-form-bio input-xxlarge"}} +{{#if model.automatic}} +
+ {{group-flair-inputs model=model}} +
+{{/if}} + {{#if canEdit}} {{yield}} diff --git a/app/assets/javascripts/discourse/app/templates/components/latest-topic-list-item.hbs b/app/assets/javascripts/discourse/app/templates/components/latest-topic-list-item.hbs index f89043711a8..754d9fdd476 100644 --- a/app/assets/javascripts/discourse/app/templates/components/latest-topic-list-item.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/latest-topic-list-item.hbs @@ -2,6 +2,7 @@ {{#user-link user=topic.lastPosterUser}} {{avatar topic.lastPosterUser imageSize="large"}} {{/user-link}} + {{user-avatar-flair user=topic.lastPosterUser}} {{#if topic.lastPosterGroup}} {{avatar-flair flairURL=topic.lastPosterGroup.flair_url diff --git a/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs index b14298cd261..f6f6c475ca3 100644 --- a/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs @@ -27,13 +27,9 @@ {{else}} {{bound-avatar this.user "huge"}} {{/if}} - {{#if this.user.primary_group_name}} - {{avatar-flair - flairURL=this.user.primary_group_flair_url - flairBgColor=this.user.primary_group_flair_bg_color - flairColor=this.user.primary_group_flair_color - groupName=this.user.primary_group_name}} - {{/if}} + + {{user-avatar-flair user=this.user}} + {{plugin-outlet name="user-card-avatar-flair" args=(hash user=this.user) tagName="div"}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/user-info.hbs b/app/assets/javascripts/discourse/app/templates/components/user-info.hbs index 6de329c3a5a..07c80f41e39 100644 --- a/app/assets/javascripts/discourse/app/templates/components/user-info.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/user-info.hbs @@ -1,13 +1,7 @@
{{avatar @user imageSize="large"}} - {{#if @user.primary_group_name}} - {{avatar-flair - flairURL=@user.primary_group_flair_url - flairBgColor=@user.primary_group_flair_bg_color - flairColor=@user.primary_group_flair_color - groupName=@user.primary_group_name}} - {{/if}} + {{user-avatar-flair user=@user}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/user-profile-avatar.hbs b/app/assets/javascripts/discourse/app/templates/components/user-profile-avatar.hbs index f2965aabb09..e8b53b6b8d3 100644 --- a/app/assets/javascripts/discourse/app/templates/components/user-profile-avatar.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/user-profile-avatar.hbs @@ -1,11 +1,5 @@
{{bound-avatar @user "huge"}} - {{#if @user.primary_group_name}} - {{avatar-flair - flairURL=@user.primary_group_flair_url - flairBgColor=@user.primary_group_flair_bg_color - flairColor=@user.primary_group_flair_color - groupName=@user.primary_group_name}} - {{/if}} + {{user-avatar-flair user=@user}} {{plugin-outlet name="user-profile-avatar-flair" args=(hash model=@user) tagName="div"}}
diff --git a/app/assets/javascripts/discourse/app/widgets/post.js b/app/assets/javascripts/discourse/app/widgets/post.js index 47adaa8f5bc..9a02a73f2d0 100644 --- a/app/assets/javascripts/discourse/app/widgets/post.js +++ b/app/assets/javascripts/discourse/app/widgets/post.js @@ -20,6 +20,7 @@ import { postTransformCallbacks } from "discourse/widgets/post-stream"; import { prioritizeNameInUx } from "discourse/lib/settings"; import { relativeAgeMediumSpan } from "discourse/lib/formatter"; import { transformBasicPost } from "discourse/lib/transform-post"; +import autoGroupFlairForUser from "discourse/lib/avatar-flair"; function transformWithCallbacks(post) { let transformed = transformBasicPost(post); @@ -187,6 +188,11 @@ createWidget("post-avatar", { if (attrs.primary_group_flair_url || attrs.primary_group_flair_bg_color) { result.push(this.attach("avatar-flair", attrs)); + } else { + const autoFlairAttrs = autoGroupFlairForUser(this.site, attrs); + if (autoFlairAttrs) { + result.push(this.attach("avatar-flair", autoFlairAttrs)); + } } result.push(h("div.poster-avatar-extra")); diff --git a/app/assets/javascripts/discourse/app/widgets/topic-map.js b/app/assets/javascripts/discourse/app/widgets/topic-map.js index abde361f8bf..be9bf14d84e 100644 --- a/app/assets/javascripts/discourse/app/widgets/topic-map.js +++ b/app/assets/javascripts/discourse/app/widgets/topic-map.js @@ -4,6 +4,7 @@ import I18n from "I18n"; import { createWidget } from "discourse/widgets/widget"; import { h } from "virtual-dom"; import { replaceEmoji } from "discourse/widgets/emoji"; +import autoGroupFlairForUser from "discourse/lib/avatar-flair"; const LINKS_SHOWN = 5; @@ -61,6 +62,11 @@ createWidget("topic-participant", { if (attrs.primary_group_flair_url || attrs.primary_group_flair_bg_color) { linkContents.push(this.attach("avatar-flair", attrs)); + } else { + const autoFlairAttrs = autoGroupFlairForUser(this.site, attrs); + if (autoFlairAttrs) { + linkContents.push(this.attach("avatar-flair", autoFlairAttrs)); + } } return h( diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-avatar-flair-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-avatar-flair-test.js new file mode 100644 index 00000000000..7b81e20913e --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/user-avatar-flair-test.js @@ -0,0 +1,184 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { + discourseModule, + queryAll, +} from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import { resetFlair } from "discourse/lib/avatar-flair"; + +function setupSiteGroups(that) { + that.site.groups = [ + { + id: 1, + name: "admins", + flair_url: "fa-bars", + flair_bg_color: "CC000A", + flair_color: "FFFFFA", + }, + { + id: 2, + name: "staff", + flair_url: "fa-bars", + flair_bg_color: "CC0005", + flair_color: "FFFFF5", + }, + { + id: 3, + name: "trust_level_1", + flair_url: "fa-dice-one", + flair_bg_color: "CC0001", + flair_color: "FFFFF1", + }, + { + id: 4, + name: "trust_level_2", + flair_url: "fa-dice-two", + flair_bg_color: "CC0002", + flair_color: "FFFFF2", + }, + ]; +} + +discourseModule( + "Integration | Component | user-avatar-flair", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("avatar flair for admin user", { + template: hbs`{{user-avatar-flair user=args}}`, + beforeEach() { + resetFlair(); + this.set("args", { + admin: true, + moderator: false, + trust_level: 2, + }); + setupSiteGroups(this); + }, + afterEach() { + resetFlair(); + }, + test(assert) { + assert.ok(queryAll(".avatar-flair").length, "it has the tag"); + assert.ok(queryAll("svg.d-icon-bars").length, "it has the svg icon"); + assert.equal( + queryAll(".avatar-flair").attr("style"), + "background-color: #CC000A; color: #FFFFFA; ", + "it has styles" + ); + }, + }); + + componentTest("avatar flair for moderator user with fallback to staff", { + template: hbs`{{user-avatar-flair user=args}}`, + beforeEach() { + resetFlair(); + this.set("args", { + admin: false, + moderator: true, + trust_level: 2, + }); + setupSiteGroups(this); + }, + afterEach() { + resetFlair(); + }, + test(assert) { + assert.ok(queryAll(".avatar-flair").length, "it has the tag"); + assert.ok(queryAll("svg.d-icon-bars").length, "it has the svg icon"); + assert.equal( + queryAll(".avatar-flair").attr("style"), + "background-color: #CC0005; color: #FFFFF5; ", + "it has styles" + ); + }, + }); + + componentTest("avatar flair for trust level", { + template: hbs`{{user-avatar-flair user=args}}`, + beforeEach() { + resetFlair(); + this.set("args", { + admin: false, + moderator: false, + trust_level: 2, + }); + setupSiteGroups(this); + }, + afterEach() { + resetFlair(); + }, + test(assert) { + assert.ok(queryAll(".avatar-flair").length, "it has the tag"); + assert.ok( + queryAll("svg.d-icon-dice-two").length, + "it has the svg icon" + ); + assert.equal( + queryAll(".avatar-flair").attr("style"), + "background-color: #CC0002; color: #FFFFF2; ", + "it has styles" + ); + }, + }); + + componentTest("avatar flair for trust level with fallback", { + template: hbs`{{user-avatar-flair user=args}}`, + beforeEach() { + resetFlair(); + this.set("args", { + admin: false, + moderator: false, + trust_level: 3, + }); + setupSiteGroups(this); + }, + afterEach() { + resetFlair(); + }, + test(assert) { + assert.ok(queryAll(".avatar-flair").length, "it has the tag"); + assert.ok( + queryAll("svg.d-icon-dice-two").length, + "it has the svg icon" + ); + assert.equal( + queryAll(".avatar-flair").attr("style"), + "background-color: #CC0002; color: #FFFFF2; ", + "it has styles" + ); + }, + }); + + componentTest("avatar flair for primary group flair", { + template: hbs`{{user-avatar-flair user=args}}`, + beforeEach() { + resetFlair(); + this.set("args", { + admin: false, + moderator: false, + trust_level: 3, + primary_group_flair_url: "fa-times", + primary_group_flair_bg_color: "123456", + primary_group_flair_color: "B0B0B0", + primary_group_name: "Band Geeks", + }); + setupSiteGroups(this); + }, + afterEach() { + resetFlair(); + }, + test(assert) { + assert.ok(queryAll(".avatar-flair").length, "it has the tag"); + assert.ok(queryAll("svg.d-icon-times").length, "it has the svg icon"); + assert.equal( + queryAll(".avatar-flair").attr("style"), + "background-color: #123456; color: #B0B0B0; ", + "it has styles" + ); + }, + }); + } +); diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 0d39f9a8f9b..4239986dacb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -579,6 +579,10 @@ class GroupsController < ApplicationController messageable_level default_notification_level bio_raw + flair_icon + flair_upload_id + flair_bg_color + flair_color } else default_params = %i{ diff --git a/app/serializers/category_and_topic_lists_serializer.rb b/app/serializers/category_and_topic_lists_serializer.rb index edea6be090c..863164fb108 100644 --- a/app/serializers/category_and_topic_lists_serializer.rb +++ b/app/serializers/category_and_topic_lists_serializer.rb @@ -3,7 +3,7 @@ class CategoryAndTopicListsSerializer < ApplicationSerializer has_one :category_list, serializer: CategoryListSerializer, embed: :objects has_one :topic_list, serializer: TopicListSerializer, embed: :objects - has_many :users, serializer: BasicUserSerializer, embed: :objects + has_many :users, serializer: PosterSerializer, embed: :objects has_many :primary_groups, serializer: PrimaryGroupSerializer, embed: :objects def users diff --git a/app/serializers/concerns/user_primary_group_mixin.rb b/app/serializers/concerns/user_primary_group_mixin.rb index 78b921a1a93..184db2b0279 100644 --- a/app/serializers/concerns/user_primary_group_mixin.rb +++ b/app/serializers/concerns/user_primary_group_mixin.rb @@ -6,7 +6,10 @@ module UserPrimaryGroupMixin klass.attributes :primary_group_name, :primary_group_flair_url, :primary_group_flair_bg_color, - :primary_group_flair_color + :primary_group_flair_color, + :admin, + :moderator, + :trust_level end def primary_group_name @@ -41,4 +44,19 @@ module UserPrimaryGroupMixin object&.primary_group&.flair_color.present? end + def include_admin? + object&.admin + end + + def admin + true + end + + def include_moderator? + object&.moderator + end + + def moderator + true + end end diff --git a/app/serializers/poster_serializer.rb b/app/serializers/poster_serializer.rb new file mode 100644 index 00000000000..b578d739fd4 --- /dev/null +++ b/app/serializers/poster_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class PosterSerializer < BasicUserSerializer + include UserPrimaryGroupMixin +end diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 52493214498..e1061eab9be 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -62,7 +62,17 @@ class SiteSerializer < ApplicationSerializer def groups cache_anon_fragment("group_names") do - object.groups.order(:name).pluck(:id, :name).map { |id, name| { id: id, name: name } }.as_json + object.groups.order(:name) + .select(:id, :name, :flair_icon, :flair_upload_id, :flair_bg_color, :flair_color) + .map do |g| + { + id: g.id, + name: g.name, + flair_url: g.flair_url, + flair_bg_color: g.flair_bg_color, + flair_color: g.flair_color, + } + end.as_json end end diff --git a/app/serializers/topic_post_count_serializer.rb b/app/serializers/topic_post_count_serializer.rb index 8baae1e4d46..08d1c624f49 100644 --- a/app/serializers/topic_post_count_serializer.rb +++ b/app/serializers/topic_post_count_serializer.rb @@ -3,7 +3,8 @@ class TopicPostCountSerializer < BasicUserSerializer attributes :post_count, :primary_group_name, - :primary_group_flair_url, :primary_group_flair_color, :primary_group_flair_bg_color + :primary_group_flair_url, :primary_group_flair_color, :primary_group_flair_bg_color, + :admin, :moderator, :trust_level, def id object[:user].id @@ -34,4 +35,24 @@ class TopicPostCountSerializer < BasicUserSerializer object[:user]&.primary_group&.flair_color end + def include_admin? + object[:user].admin + end + + def admin + true + end + + def include_moderator? + object[:user].moderator + end + + def moderator + true + end + + def trust_level + object[:user].trust_level + end + end diff --git a/app/serializers/topic_poster_serializer.rb b/app/serializers/topic_poster_serializer.rb index 41c4b7ba807..6a6bc714f43 100644 --- a/app/serializers/topic_poster_serializer.rb +++ b/app/serializers/topic_poster_serializer.rb @@ -2,6 +2,7 @@ class TopicPosterSerializer < ApplicationSerializer attributes :extras, :description - has_one :user, serializer: BasicUserSerializer + + has_one :user, serializer: PosterSerializer has_one :primary_group, serializer: PrimaryGroupSerializer end diff --git a/lib/user_lookup.rb b/lib/user_lookup.rb index faced05ee0d..4f6feb7e7ce 100644 --- a/lib/user_lookup.rb +++ b/lib/user_lookup.rb @@ -18,7 +18,7 @@ class UserLookup private def self.lookup_columns - @user_lookup_columns ||= %i{id username name uploaded_avatar_id primary_group_id} + @user_lookup_columns ||= %i{id username name uploaded_avatar_id primary_group_id admin moderator trust_level} end def self.group_lookup_columns diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 8ff7c606bd8..110d9a11a6c 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -815,7 +815,9 @@ describe GroupsController do put "/groups/#{group.id}.json", params: { group: { + flair_bg_color: 'FFF', flair_color: 'BBB', + flair_icon: 'fa-adjust', name: 'testing', visibility_level: 1, mentionable_level: 1, @@ -829,7 +831,9 @@ describe GroupsController do expect(response.status).to eq(200) group.reload - expect(group.flair_color).to eq(nil) + expect(group.flair_bg_color).to eq('FFF') + expect(group.flair_color).to eq('BBB') + expect(group.flair_icon).to eq('fa-adjust') expect(group.name).to eq('admins') expect(group.visibility_level).to eq(1) expect(group.mentionable_level).to eq(1) @@ -916,6 +920,9 @@ describe GroupsController do put "/groups/#{group.id}.json", params: { group: { + flair_bg_color: 'FFF', + flair_color: 'BBB', + flair_icon: 'fa-adjust', mentionable_level: 1, messageable_level: 1, default_notification_level: 1 @@ -925,7 +932,9 @@ describe GroupsController do expect(response.status).to eq(200) group.reload - expect(group.flair_color).to eq(nil) + expect(group.flair_bg_color).to eq('FFF') + expect(group.flair_color).to eq('BBB') + expect(group.flair_icon).to eq('fa-adjust') expect(group.name).to eq('trust_level_4') expect(group.mentionable_level).to eq(1) expect(group.messageable_level).to eq(1)