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