From c4ece5cef5e718e8dfa84dcd1dcd7e96ce0acd58 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= <esanz-garcia@gitlab.com>
Date: Tue, 30 Jul 2024 21:30:36 +0200
Subject: [PATCH 1/3] Add bluesky field to the user profile

See https://atproto.com/specs/did

Changelog: changed

Authored-By: @SlickDomique
Co-Authored-By: @eduardosanz
---
 app/controllers/admin/users_controller.rb     |  7 +--
 app/controllers/profiles_controller.rb        | 23 +++++-----
 .../user_settings/profiles_controller.rb      | 23 +++++-----
 app/helpers/application_helper.rb             |  6 +++
 app/helpers/users_helper.rb                   |  6 +++
 app/models/user.rb                            |  1 +
 app/models/user_detail.rb                     | 15 +++++-
 .../user_settings/profiles/show.html.haml     |  3 ++
 app/views/users/_profile_sidebar.html.haml    |  6 ++-
 ...40730163326_add_bluesky_to_user_details.rb | 22 +++++++++
 db/schema_migrations/20240730163326           |  1 +
 db/structure.sql                              |  2 +
 doc/user/profile/index.md                     |  1 +
 locale/gitlab.pot                             |  3 ++
 .../user_settings/profiles_controller_spec.rb | 10 ++++
 spec/helpers/application_helper_spec.rb       | 15 ++++++
 spec/helpers/users_helper_spec.rb             | 22 +++++++++
 spec/models/user_detail_spec.rb               | 46 +++++++++++++++++++
 spec/models/user_spec.rb                      |  3 ++
 19 files changed, 188 insertions(+), 27 deletions(-)
 create mode 100644 db/migrate/20240730163326_add_bluesky_to_user_details.rb
 create mode 100644 db/schema_migrations/20240730163326

diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index fe6f06c96ce6a433..bc1114c5aef2dc3a 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -367,9 +367,10 @@ def allowed_user_params
       :access_level,
       :avatar,
       :bio,
+      :bluesky,
       :can_create_group,
-      :color_scheme_id,
       :color_mode_id,
+      :color_scheme_id,
       :discord,
       :email,
       :extern_uid,
@@ -381,7 +382,9 @@ def allowed_user_params
       :linkedin,
       :mastodon,
       :name,
+      :note,
       :password_expires_at,
+      :private_profile,
       :projects_limit,
       :provider,
       :remember_me,
@@ -390,8 +393,6 @@ def allowed_user_params
       :twitter,
       :username,
       :website_url,
-      :note,
-      :private_profile,
       { credit_card_validation_attributes: [:credit_card_validated_at] }
     ]
   end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 5fcda8e8be95b1c6..f89ef514ecc0214a 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -76,34 +76,35 @@ def username_param
 
   def user_params_attributes
     [
+      :achievements_enabled,
       :avatar,
       :bio,
+      :bluesky,
+      :commit_email,
       :discord,
       :email,
-      :role,
       :gitpod_enabled,
       :hide_no_password,
       :hide_no_ssh_key,
       :hide_project_limit,
+      :include_private_contributions,
+      :job_title,
       :linkedin,
       :location,
       :mastodon,
       :name,
-      :public_email,
-      :commit_email,
-      :skype,
-      :twitter,
-      :username,
-      :website_url,
       :organization,
       :private_profile,
-      :include_private_contributions,
-      :achievements_enabled,
-      :timezone,
-      :job_title,
       :pronouns,
       :pronunciation,
+      :public_email,
+      :role,
+      :skype,
+      :timezone,
+      :twitter,
+      :username,
       :validation_password,
+      :website_url,
       { status: [:emoji, :message, :availability, :clear_status_after] }
     ]
   end
diff --git a/app/controllers/user_settings/profiles_controller.rb b/app/controllers/user_settings/profiles_controller.rb
index 55d074eddbc2a975..df8da1e8bbea8e05 100644
--- a/app/controllers/user_settings/profiles_controller.rb
+++ b/app/controllers/user_settings/profiles_controller.rb
@@ -39,34 +39,35 @@ def user
 
     def user_params_attributes
       [
+        :achievements_enabled,
         :avatar,
         :bio,
+        :bluesky,
+        :commit_email,
         :discord,
         :email,
-        :role,
         :gitpod_enabled,
         :hide_no_password,
         :hide_no_ssh_key,
         :hide_project_limit,
+        :include_private_contributions,
+        :job_title,
         :linkedin,
         :location,
         :mastodon,
         :name,
-        :public_email,
-        :commit_email,
-        :skype,
-        :twitter,
-        :username,
-        :website_url,
         :organization,
         :private_profile,
-        :include_private_contributions,
-        :achievements_enabled,
-        :timezone,
-        :job_title,
         :pronouns,
         :pronunciation,
+        :public_email,
+        :role,
+        :skype,
+        :timezone,
+        :twitter,
+        :username,
         :validation_password,
+        :website_url,
         { status: [:emoji, :message, :availability, :clear_status_after] }
       ]
     end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 73f515e92eafdc1d..17a80fcde7f8f8e3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -379,6 +379,12 @@ def discord_url(user)
     "https://discord.com/users/#{user.discord}"
   end
 
+  def bluesky_url(user)
+    return '' if user.bluesky.blank?
+
+    external_redirect_path(url: "https://bsky.app/profile/#{user.bluesky}")
+  end
+
   def mastodon_url(user)
     return '' if user.mastodon.blank?
 
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index d9ae9368513f5246..70ea4db49dd4481a 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -195,6 +195,12 @@ def admin_user_actions_data_attributes(user)
     }
   end
 
+  def has_contact_info?(user)
+    contact_fields = %i[bluesky discord linkedin mastodon skype twitter website_url]
+    has_contact = contact_fields.any? { |field| user.public_send(field).present? }  # rubocop:disable GitlabSecurity/PublicSend
+    has_contact || display_public_email?(user)
+  end
+
   def display_public_email?(user)
     user.public_email.present?
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index e39f8c8cf6e78f37..3e9434e11ff9bfc6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -443,6 +443,7 @@ def update_tracked_fields!(request)
   delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
   delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
   delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
+  delegate :bluesky, :bluesky=, to: :user_detail, allow_nil: true
   delegate :mastodon, :mastodon=, to: :user_detail, allow_nil: true
   delegate :linkedin, :linkedin=, to: :user_detail, allow_nil: true
   delegate :twitter, :twitter=, to: :user_detail, allow_nil: true
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 8c2c6ec3bcdab4e8..0bc229d991859c77 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -17,6 +17,14 @@ class UserDetail < MainClusterwide::ApplicationRecord
 
   DEFAULT_FIELD_LENGTH = 500
 
+  # specification for bluesky identifier https://web.plc.directory/spec/v0.1/did-plc
+  BLUESKY_VALIDATION_REGEX = /
+    \A            # beginning of string
+    did:plc:      # beginning of bluesky id
+    [a-z0-9]{24}  # 24 characters of word or digit
+    \z            # end of string
+  /x
+
   MASTODON_VALIDATION_REGEX = /
     \A            # beginning of string
     @?\b          # optional leading at
@@ -33,6 +41,10 @@ class UserDetail < MainClusterwide::ApplicationRecord
   validate :discord_format
   validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
   validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+  validates :bluesky,
+    allow_blank: true,
+    format: { with: UserDetail::BLUESKY_VALIDATION_REGEX,
+              message: proc { s_('Profiles|must contain only a bluesky did:plc identifier.') } }
   validates :mastodon, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
   validate :mastodon_format
   validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
@@ -47,7 +59,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
   enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
 
   def sanitize_attrs
-    %i[discord linkedin mastodon skype twitter website_url].each do |attr|
+    %i[bluesky discord linkedin mastodon skype twitter website_url].each do |attr|
       value = self[attr]
       self[attr] = Sanitize.clean(value) if value.present?
     end
@@ -60,6 +72,7 @@ def sanitize_attrs
   private
 
   def prevent_nil_fields
+    self.bluesky = '' if bluesky.nil?
     self.bio = '' if bio.nil?
     self.discord = '' if discord.nil?
     self.linkedin = '' if linkedin.nil?
diff --git a/app/views/user_settings/profiles/show.html.haml b/app/views/user_settings/profiles/show.html.haml
index e90c5db4ff46ada6..e30c38ae76bb938c 100644
--- a/app/views/user_settings/profiles/show.html.haml
+++ b/app/views/user_settings/profiles/show.html.haml
@@ -124,6 +124,9 @@
           allow_empty: true}
         %small.form-text.gl-text-secondary
           = external_accounts_docs_link
+      .form-group.gl-form-group
+        = f.label :bluesky
+        = f.text_field :bluesky, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: "did:plc:ewvi7nxzyoun6zhxrhs64oiz"
       .form-group.gl-form-group
         = f.label :mastodon
         = f.text_field :mastodon, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: "@robin@example.com"
diff --git a/app/views/users/_profile_sidebar.html.haml b/app/views/users/_profile_sidebar.html.haml
index cbee8a5d2318d6da..860c50c59d839edd 100644
--- a/app/views/users/_profile_sidebar.html.haml
+++ b/app/views/users/_profile_sidebar.html.haml
@@ -47,7 +47,7 @@
                 = sprite_icon('calendar', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                 %span= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) }
 
-            - if @user.website_url.present? || display_public_email?(@user) || @user.skype.present? || @user.linkedin.present? || @user.twitter.present? || @user.mastodon.present? || @user.discord.present?
+            - if has_contact_info?(@user)
               .gl-text-gray-900
                 %h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|Contact')
                 - if @user.website_url.present?
@@ -70,6 +70,10 @@
                   .gl-display-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('x', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to @user.twitter, twitter_url(@user), class: 'gl-text-gray-900', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow'
+                - if @user.bluesky.present?
+                  .gl-display-flex.gl-gap-2.gl-mb-2
+                    = sprite_icon('at', css_class: 'gl-fill-gray-500 gl-mt-1 flex-shrink-0')
+                    = link_to @user.bluesky, bluesky_url(@user), class: 'gl-text-gray-900', title: "Bluesky", target: '_blank', rel: 'noopener noreferrer nofollow'
                 - if @user.mastodon.present?
                   .gl-display-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('mastodon', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
diff --git a/db/migrate/20240730163326_add_bluesky_to_user_details.rb b/db/migrate/20240730163326_add_bluesky_to_user_details.rb
new file mode 100644
index 0000000000000000..ff5d77b07babe668
--- /dev/null
+++ b/db/migrate/20240730163326_add_bluesky_to_user_details.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddBlueskyToUserDetails < Gitlab::Database::Migration[2.2]
+  disable_ddl_transaction!
+  milestone '17.3'
+
+  USER_DETAILS_FIELD_LIMIT = 256
+
+  def up
+    with_lock_retries do
+      add_column :user_details, :bluesky, :text, default: '', null: false, if_not_exists: true
+    end
+
+    add_text_limit :user_details, :bluesky, USER_DETAILS_FIELD_LIMIT
+  end
+
+  def down
+    with_lock_retries do
+      remove_column :user_details, :bluesky
+    end
+  end
+end
diff --git a/db/schema_migrations/20240730163326 b/db/schema_migrations/20240730163326
new file mode 100644
index 0000000000000000..a0ff08ef2b4bf2ed
--- /dev/null
+++ b/db/schema_migrations/20240730163326
@@ -0,0 +1 @@
+df9c3f57784f53f2966970e883d56abe6bc816c4cb1fe61886a924271270d6e3
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 023cfb2464f82dc4..e74fb4793f6d8fb4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18690,6 +18690,8 @@ CREATE TABLE user_details (
     mastodon text DEFAULT ''::text NOT NULL,
     project_authorizations_recalculated_at timestamp with time zone DEFAULT '2010-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
     onboarding_status jsonb DEFAULT '{}'::jsonb NOT NULL,
+    bluesky text DEFAULT ''::text NOT NULL,
+    CONSTRAINT check_18a53381cd CHECK ((char_length(bluesky) <= 256)),
     CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
     CONSTRAINT check_444573ee52 CHECK ((char_length(skype) <= 500)),
     CONSTRAINT check_466a25be35 CHECK ((char_length(twitter) <= 500)),
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index b5fe52a43f9f05da..2c9aa1a9944de051 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -184,6 +184,7 @@ To add links to other accounts:
 1. In the **Main settings** section, add your:
    - Discord [user ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-).
    - LinkedIn profile name.
+   - Bluesky [did:plc identifier](https://atproto.com/specs/did). To find your identifier, [resolve your user handle](https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=USER_HANDLE).
    - Mastodon username.
    - Skype username.
    - X (formerly Twitter) @username.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b4ad2eb14e5f13ed..dc12febf032fa510 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -41015,6 +41015,9 @@ msgstr ""
 msgid "Profiles|https://website.com"
 msgstr ""
 
+msgid "Profiles|must contain only a bluesky did:plc identifier."
+msgstr ""
+
 msgid "Profiles|username"
 msgstr ""
 
diff --git a/spec/controllers/user_settings/profiles_controller_spec.rb b/spec/controllers/user_settings/profiles_controller_spec.rb
index 706612296acbf0f9..ffb1d8a6a6e404f4 100644
--- a/spec/controllers/user_settings/profiles_controller_spec.rb
+++ b/spec/controllers/user_settings/profiles_controller_spec.rb
@@ -132,6 +132,16 @@
       expect(response).to have_gitlab_http_status(:found)
     end
 
+    it 'allows updating user specified bluesky did identifier', :aggregate_failures do
+      bluesky_did_id = 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'
+      sign_in(user)
+
+      put :update, params: { user: { bluesky: bluesky_did_id } }
+
+      expect(user.reload.bluesky).to eq(bluesky_did_id)
+      expect(response).to have_gitlab_http_status(:found)
+    end
+
     it 'allows updating user specified mastodon username', :aggregate_failures do
       mastodon_username = '@robin@example.com'
       sign_in(user)
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index d1d04fb5548b979b..49ba36a3e4c79016 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -668,6 +668,21 @@ def stub_controller_method(method_name, value)
       end
     end
 
+    context 'when bluesky is set' do
+      let_it_be(:user) { build(:user) }
+      let(:bluesky) { bluesky_url(user) }
+
+      it 'returns an empty string if bluesky did id is not set' do
+        expect(bluesky).to eq('')
+      end
+
+      it 'returns bluesky url when bluesky did id is set' do
+        user.bluesky = 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'
+
+        expect(bluesky).to eq(external_redirect_path(url: 'https://bsky.app/profile/did:plc:ewvi7nxzyoun6zhxrhs64oiz'))
+      end
+    end
+
     context 'when mastodon is set' do
       let_it_be(:user) { build(:user) }
       let(:mastodon) { mastodon_url(user) }
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index fc5a912f4334e05a..5d9d7d52c74b103b 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -11,6 +11,28 @@ def filter_ee_badges(badges)
     badges.reject { |badge| badge[:text] == 'Is using seat' }
   end
 
+  describe 'has_contact_info?' do
+    subject { helper.has_contact_info?(user) }
+
+    context 'when user has skype profile' do
+      let_it_be(:user) { create(:user, bluesky: 'did:plc:ewvi7nxzyoun6zhxrhs64oiz') }
+
+      it { is_expected.to be true }
+    end
+
+    context 'when user has public email' do
+      let_it_be(:user) { create(:user, :public_email) }
+
+      it { is_expected.to be true }
+    end
+
+    context 'when user public email is blank' do
+      let_it_be(:user) { create(:user, public_email: '') }
+
+      it { is_expected.to be false }
+    end
+  end
+
   describe 'display_public_email?' do
     let_it_be(:user) { create(:user, :public_email) }
 
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index a8835a0dc2044194..f7f0aef2e4a01b33 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -159,6 +159,50 @@
       end
     end
 
+    describe '#bluesky' do
+      context 'when bluesky is set' do
+        let_it_be(:user_detail) { build(:user_detail) }
+
+        let(:value) { 'did:plc:ewvi7nxzyoun6zhxrhs64oiz' }
+
+        before do
+          user_detail.bluesky = value
+        end
+
+        it 'accepts a valid bluesky did id' do
+          expect(user_detail).to be_valid
+        end
+
+        shared_examples 'throws an error' do
+          it do
+            expect(user_detail).not_to be_valid
+            expect(user_detail.errors.full_messages)
+              .to match_array([_('Bluesky must contain only a bluesky did:plc identifier.')])
+          end
+        end
+
+        context 'when bluesky is set to a wrong format' do
+          context 'when bluesky did:plc is too long' do
+            let(:value) { 'a' * 33 }
+
+            it_behaves_like 'throws an error'
+          end
+
+          context 'when bluesky did:plc is wrong' do
+            let(:value) { 'did:plc:ewvi7nxzyoun6zhxrhs64OIZ' }
+
+            it_behaves_like 'throws an error'
+          end
+
+          context 'when bluesky other bluesky did: formats are used' do
+            let(:value) { 'did:web:example.com' }
+
+            it_behaves_like 'throws an error'
+          end
+        end
+      end
+    end
+
     describe '#mastodon' do
       it { is_expected.to validate_length_of(:mastodon).is_at_most(500) }
 
@@ -219,6 +263,7 @@
         discord: '1234567890123456789',
         linkedin: 'linkedin',
         location: 'location',
+        bluesky: 'did:plc:ewvi7nxzyoun6zhxrhs64oiz',
         mastodon: '@robin@example.com',
         organization: 'organization',
         skype: 'skype',
@@ -240,6 +285,7 @@
     it_behaves_like 'prevents `nil` value', :discord
     it_behaves_like 'prevents `nil` value', :linkedin
     it_behaves_like 'prevents `nil` value', :location
+    it_behaves_like 'prevents `nil` value', :bluesky
     it_behaves_like 'prevents `nil` value', :mastodon
     it_behaves_like 'prevents `nil` value', :organization
     it_behaves_like 'prevents `nil` value', :skype
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index bcd6208c855f7d2c..fc367ef44a743ad5 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -129,6 +129,9 @@
     it { is_expected.to delegate_method(:linkedin).to(:user_detail).allow_nil }
     it { is_expected.to delegate_method(:linkedin=).to(:user_detail).with_arguments(:args).allow_nil }
 
+    it { is_expected.to delegate_method(:bluesky).to(:user_detail).allow_nil }
+    it { is_expected.to delegate_method(:bluesky=).to(:user_detail).with_arguments(:args).allow_nil }
+
     it { is_expected.to delegate_method(:mastodon).to(:user_detail).allow_nil }
     it { is_expected.to delegate_method(:mastodon=).to(:user_detail).with_arguments(:args).allow_nil }
 
-- 
GitLab


From 2bb01a42d16e835848915bd001a866a07b79b09f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= <esanz-garcia@gitlab.com>
Date: Wed, 31 Jul 2024 08:33:06 +0200
Subject: [PATCH 2/3] Replace legacy CSS rule for Tailwind equivalent

gl-display-flex -> gl-flex
---
 app/views/users/_profile_sidebar.html.haml | 30 +++++++++++-----------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/app/views/users/_profile_sidebar.html.haml b/app/views/users/_profile_sidebar.html.haml
index 860c50c59d839edd..226edc1daabff1c3 100644
--- a/app/views/users/_profile_sidebar.html.haml
+++ b/app/views/users/_profile_sidebar.html.haml
@@ -3,11 +3,11 @@
     .gl-align-top.gl-text-left.gl-max-w-80.gl-wrap-anywhere
       .user-info
         - if @user.confirmed?
-          .gl-display-flex.gl-gap-4.gl-flex-direction-column
+          .gl-flex.gl-gap-4.gl-flex-direction-column
             - if @user.pronouns.present? || @user.pronunciation.present? || @user.bio.present?
               %div
                 %h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|About')
-                .gl-display-flex.gl-gap-2.gl-flex-direction-column
+                .gl-flex.gl-gap-2.gl-flex-direction-column
                   - if @user.pronouns.present? || @user.pronunciation.present?
                     .gl-mb-2
                       - if @user.pronunciation.present?
@@ -29,21 +29,21 @@
             %div{ itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' }
               %h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|Info')
               - if work_information(@user).present?
-                .gl-display-flex.gl-gap-2.gl-mb-2
+                .gl-flex.gl-gap-2.gl-mb-2
                   = sprite_icon('work', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                   %span
                     = work_information(@user, with_schema_markup: true)
               - if @user.location.present?
-                .gl-display-flex.gl-gap-2.gl-mb-2
+                .gl-flex.gl-gap-2.gl-mb-2
                   = sprite_icon('location', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                   %span{ itemprop: 'addressLocality' }
                     = @user.location
               - if user_local_time.present?
-                .gl-display-flex.gl-gap-2.gl-mb-2{ data: { testid: 'user-local-time' } }
+                .gl-flex.gl-gap-2.gl-mb-2{ data: { testid: 'user-local-time' } }
                   = sprite_icon('clock', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                   %span
                     = user_local_time
-              .gl-display-flex.gl-gap-2.gl-mb-2
+              .gl-flex.gl-gap-2.gl-mb-2
                 = sprite_icon('calendar', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                 %span= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) }
 
@@ -51,34 +51,34 @@
               .gl-text-gray-900
                 %h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|Contact')
                 - if @user.website_url.present?
-                  .gl-display-flex.gl-gap-2.gl-mb-2
+                  .gl-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('earth', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to @user.short_website_url, @user.full_website_url, class: 'gl-text-gray-900', target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url'
                 - if display_public_email?(@user)
-                  .gl-display-flex.gl-gap-2.gl-mb-2
+                  .gl-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('mail', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to @user.public_email, "mailto:#{@user.public_email}", class: 'gl-text-gray-900', itemprop: 'email'
                 - if @user.skype.present?
-                  .gl-display-flex.gl-gap-2.gl-mb-2
+                  .gl-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('skype', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to @user.skype, "skype:#{@user.skype}", class: 'gl-text-gray-900', title: "Skype"
                 - if @user.linkedin.present?
-                  .gl-display-flex.gl-gap-2.gl-mb-2
+                  .gl-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('linkedin', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to linkedin_name(@user), linkedin_url(@user), class: 'gl-text-gray-900', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow'
                 - if @user.twitter.present?
-                  .gl-display-flex.gl-gap-2.gl-mb-2
+                  .gl-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('x', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to @user.twitter, twitter_url(@user), class: 'gl-text-gray-900', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow'
                 - if @user.bluesky.present?
-                  .gl-display-flex.gl-gap-2.gl-mb-2
-                    = sprite_icon('at', css_class: 'gl-fill-gray-500 gl-mt-1 flex-shrink-0')
+                  .gl-flex.gl-gap-2.gl-mb-2
+                    = sprite_icon('at', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to @user.bluesky, bluesky_url(@user), class: 'gl-text-gray-900', title: "Bluesky", target: '_blank', rel: 'noopener noreferrer nofollow'
                 - if @user.mastodon.present?
-                  .gl-display-flex.gl-gap-2.gl-mb-2
+                  .gl-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('mastodon', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to @user.mastodon, mastodon_url(@user), class: 'gl-text-gray-900', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow'
                 - if @user.discord.present?
-                  .gl-display-flex.gl-gap-2.gl-mb-2
+                  .gl-flex.gl-gap-2.gl-mb-2
                     = sprite_icon('discord', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
                     = link_to @user.discord, discord_url(@user), class: 'gl-text-gray-900', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow'
-- 
GitLab


From 3c9e6e3df5339610ced34ccd2090b0a482ab125a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= <esanz-garcia@gitlab.com>
Date: Wed, 31 Jul 2024 08:37:13 +0200
Subject: [PATCH 3/3] Adding rubocop comment

---
 app/helpers/users_helper.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 70ea4db49dd4481a..3715dbbc3bc8a9b9 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -197,7 +197,7 @@ def admin_user_actions_data_attributes(user)
 
   def has_contact_info?(user)
     contact_fields = %i[bluesky discord linkedin mastodon skype twitter website_url]
-    has_contact = contact_fields.any? { |field| user.public_send(field).present? }  # rubocop:disable GitlabSecurity/PublicSend
+    has_contact = contact_fields.any? { |field| user.public_send(field).present? }  # rubocop:disable GitlabSecurity/PublicSend -- fields are controlled, it is safe.
     has_contact || display_public_email?(user)
   end
 
-- 
GitLab