From ba2104b9cafdad1a91fb8c6d5d77e9e0f9f34b75 Mon Sep 17 00:00:00 2001
From: Raimund Hook <rhook@gitlab.com>
Date: Thu, 28 Sep 2023 14:46:14 +0100
Subject: [PATCH 1/7] Add support for Mastodon as user social

This change adds support for adding a link to a Mastodon social network
user

Signed-off-by: Raimund Hook <rhook@gitlab.com>
Changelog: added
---
 .../stylesheets/framework/variables.scss      |  1 +
 .../stylesheets/page_bundles/profile.scss     |  4 ++++
 app/controllers/admin/users_controller.rb     |  3 ++-
 app/controllers/profiles_controller.rb        |  3 ++-
 app/helpers/application_helper.rb             |  8 +++++++
 app/models/user.rb                            |  1 +
 app/models/user_detail.rb                     | 11 ++++++++-
 app/views/profiles/show.html.haml             |  3 +++
 app/views/users/show.html.haml                |  4 ++++
 ...0927124202_add_mastodon_to_user_details.rb | 23 +++++++++++++++++++
 db/schema_migrations/20230927124202           |  1 +
 db/structure.sql                              |  2 ++
 doc/user/profile/index.md                     |  1 +
 locale/gitlab.pot                             |  6 +++++
 spec/controllers/profiles_controller_spec.rb  | 10 ++++++++
 spec/helpers/application_helper_spec.rb       | 15 ++++++++++++
 spec/models/user_detail_spec.rb               | 23 +++++++++++++++++++
 spec/models/user_spec.rb                      |  3 +++
 18 files changed, 119 insertions(+), 3 deletions(-)
 create mode 100644 db/migrate/20230927124202_add_mastodon_to_user_details.rb
 create mode 100644 db/schema_migrations/20230927124202

diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index a4bb39e0764d235f..31d45ad3a28559f6 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -655,6 +655,7 @@ $status-icon-size: 22px;
 */
 $discord: #5865f2;
 $linkedin: #2867b2;
+$mastodon: #6364ff;
 $skype: #0078d7;
 $twitter: #1d9bf0;
 
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index dbe82f583d1e898a..6b31c6678323f90d 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -242,6 +242,10 @@
   color: $discord;
 }
 
+.mastodon-icon {
+  color: $mastodon;
+}
+
 .key-created-at {
   line-height: 42px;
 }
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 1f05e4e7b21c267a..50e0c5cc5ffedabd 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -342,6 +342,7 @@ def allowed_user_params
       :bio,
       :can_create_group,
       :color_scheme_id,
+      :discord,
       :email,
       :extern_uid,
       :external,
@@ -350,6 +351,7 @@ def allowed_user_params
       :hide_no_ssh_key,
       :key_id,
       :linkedin,
+      :mastodon,
       :name,
       :password_expires_at,
       :projects_limit,
@@ -358,7 +360,6 @@ def allowed_user_params
       :skype,
       :theme_id,
       :twitter,
-      :discord,
       :username,
       :website_url,
       :note,
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index da15b393e6c9bade..cb29f0f35392ca02 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -111,6 +111,7 @@ def user_params_attributes
     [
       :avatar,
       :bio,
+      :discord,
       :email,
       :role,
       :gitpod_enabled,
@@ -119,12 +120,12 @@ def user_params_attributes
       :hide_project_limit,
       :linkedin,
       :location,
+      :mastodon,
       :name,
       :public_email,
       :commit_email,
       :skype,
       :twitter,
-      :discord,
       :username,
       :website_url,
       :organization,
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 579373539558c01b..60c77e14a5554319 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -371,6 +371,14 @@ def discord_url(user)
     "https://discord.com/users/#{user.discord}"
   end
 
+  def mastodon_url(user)
+    return '' if user.mastodon.blank?
+
+    url = user.mastodon.match %r{\A@?\b([\w\d.%+-]+)@([\w\d.-]+\.\w{2,})\b\z}
+
+    "https://#{url[2]}/@#{url[1]}"
+  end
+
   def collapsed_sidebar?
     cookies["sidebar_collapsed"] == "true"
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4034677509f82500..74a09c966f725ad2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -417,6 +417,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 :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
   delegate :skype, :skype=, to: :user_detail, allow_nil: true
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 9ac814eebdabd31e..fcf10adae6f67bed 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -21,6 +21,8 @@ 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 :mastodon, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+  validate :mastodon_format
   validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
   validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
   validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
@@ -32,7 +34,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
   enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
 
   def sanitize_attrs
-    %i[discord linkedin skype twitter website_url].each do |attr|
+    %i[discord linkedin mastodon skype twitter website_url].each do |attr|
       value = self[attr]
       self[attr] = Sanitize.clean(value) if value.present?
     end
@@ -49,6 +51,7 @@ def prevent_nil_fields
     self.discord = '' if discord.nil?
     self.linkedin = '' if linkedin.nil?
     self.location = '' if location.nil?
+    self.mastodon = '' if mastodon.nil?
     self.organization = '' if organization.nil?
     self.skype = '' if skype.nil?
     self.twitter = '' if twitter.nil?
@@ -62,4 +65,10 @@ def discord_format
   errors.add(:discord, _('must contain only a discord user ID.'))
 end
 
+def mastodon_format
+  return if mastodon.blank? || mastodon =~ %r{\A@?\b([\w\d.%+-]+)@([\w\d.-]+\.\w{2,})\b\z}
+
+  errors.add(:mastodon, _('must contain only a mastodon username.'))
+end
+
 UserDetail.prepend_mod_with('UserDetail')
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 4da48771ba341a5c..6ff7583e736d384f 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -122,6 +122,9 @@
           allow_empty: true}
         %small.form-text.text-gl-muted
           = external_accounts_docs_link
+      .form-group.gl-form-group
+        = f.label :mastodon
+        = f.text_field :mastodon, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@robin@example.com")
 
       .form-group.gl-form-group
         = f.label :website_url, s_('Profiles|Website url')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 0881c5bba5430a50..13c5f73d96024bd1 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -111,6 +111,10 @@
                   = render 'middle_dot_divider', breakpoint: 'sm' do
                     = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
                       = sprite_icon('discord', css_class: 'discord-icon')
+                - if @user.mastodon.present?
+                  = render 'middle_dot_divider', breakpoint: 'sm' do
+                    = link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do
+                      = sprite_icon('mastodon', css_class: 'mastodon-icon')
                 - if @user.website_url.present?
                   = render 'middle_dot_divider', stacking: true do
                     - if Feature.enabled?(:security_auto_fix) && @user.bot?
diff --git a/db/migrate/20230927124202_add_mastodon_to_user_details.rb b/db/migrate/20230927124202_add_mastodon_to_user_details.rb
new file mode 100644
index 0000000000000000..b23fd21837954be2
--- /dev/null
+++ b/db/migrate/20230927124202_add_mastodon_to_user_details.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class AddMastodonToUserDetails < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  USER_DETAILS_FIELD_LIMIT = 500
+
+  def up
+    with_lock_retries do
+      add_column :user_details, :mastodon, :text, default: '', null: false
+    end
+
+    add_text_limit :user_details, :mastodon, USER_DETAILS_FIELD_LIMIT
+  end
+
+  def down
+    remove_text_limit :user_details, :mastodon
+
+    with_lock_retries do
+      remove_column :user_details, :mastodon
+    end
+  end
+end
diff --git a/db/schema_migrations/20230927124202 b/db/schema_migrations/20230927124202
new file mode 100644
index 0000000000000000..a4089994e9799c7c
--- /dev/null
+++ b/db/schema_migrations/20230927124202
@@ -0,0 +1 @@
+652375e6b7318fe85b4b23eac3cce88618136341cee7721522adacbe52a52c66
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index d7d5d469d9eb12a7..6a9cdb508214152b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -24088,6 +24088,7 @@ CREATE TABLE user_details (
     enterprise_group_id bigint,
     enterprise_group_associated_at timestamp with time zone,
     email_reset_offered_at timestamp with time zone,
+    mastodon text DEFAULT ''::text NOT NULL,
     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)),
@@ -24099,6 +24100,7 @@ CREATE TABLE user_details (
     CONSTRAINT check_8a7fcf8a60 CHECK ((char_length(location) <= 500)),
     CONSTRAINT check_a73b398c60 CHECK ((char_length(phone) <= 50)),
     CONSTRAINT check_eeeaf8d4f0 CHECK ((char_length(pronouns) <= 50)),
+    CONSTRAINT check_f1a8a05b9a CHECK ((char_length(mastodon) <= 500)),
     CONSTRAINT check_f932ed37db CHECK ((char_length(pronunciation) <= 255))
 );
 
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 6536a992292e4fe0..76b454f164381d2c 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -138,6 +138,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.
+   - Mastodon username.
    - Skype username.
    - Twitter @username.
 
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f4e362f05d15f8a2..ca11ea53c0668c86 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -36207,6 +36207,9 @@ msgstr ""
 msgid "Profiles|%{provider} Active"
 msgstr ""
 
+msgid "Profiles|@robin@example.com"
+msgstr ""
+
 msgid "Profiles|@username"
 msgstr ""
 
@@ -57148,6 +57151,9 @@ msgstr ""
 msgid "must contain only a discord user ID."
 msgstr ""
 
+msgid "must contain only a mastodon username."
+msgstr ""
+
 msgid "must have a repository"
 msgstr ""
 
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 2bcb47f97ab00a25..4f350ddf1ef4da4a 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -128,6 +128,16 @@
       expect(user.reload.discord).to eq(discord_user_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)
+
+      put :update, params: { user: { mastodon: mastodon_username } }
+
+      expect(user.reload.mastodon).to eq(mastodon_username)
+      expect(response).to have_gitlab_http_status(:found)
+    end
   end
 
   describe 'GET audit_log' do
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 7cf64c6e049befd7..3e95cb25b1208f9c 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -637,6 +637,21 @@ def stub_controller_method(method_name, value)
         expect(discord).to eq('https://discord.com/users/1234567890123456789')
       end
     end
+
+    context 'when mastodon is set' do
+      let_it_be(:user) { build(:user) }
+      let(:mastodon) { mastodon_url(user) }
+
+      it 'returns an empty string if mastodon username is not set' do
+        expect(mastodon).to eq('')
+      end
+
+      it 'returns mastodon url when mastodon username is set' do
+        user.mastodon = '@robin@example.com'
+
+        expect(mastodon).to eq('https://example.com/@robin')
+      end
+    end
   end
 
   describe '#gitlab_ui_form_for' do
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 428fd5470c3127e3..58f4322f21f1dc24 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -59,6 +59,27 @@
       end
     end
 
+    describe '#mastodon' do
+      it { is_expected.to validate_length_of(:mastodon).is_at_most(500) }
+
+      context 'when discord is set' do
+        let_it_be(:user_detail) { create(:user_detail) }
+
+        it 'accepts a valid mastodon username' do
+          user_detail.mastodon = '@robin@example.com'
+
+          expect(user_detail).to be_valid
+        end
+
+        it 'throws an error when mastodon username format is wrong' do
+          user_detail.mastodon = '@robin'
+
+          expect(user_detail).not_to be_valid
+          expect(user_detail.errors.full_messages).to match_array([_('Mastodon must contain only a mastodon username.')])
+        end
+      end
+    end
+
     describe '#location' do
       it { is_expected.to validate_length_of(:location).is_at_most(500) }
     end
@@ -97,6 +118,7 @@
         discord: '1234567890123456789',
         linkedin: 'linkedin',
         location: 'location',
+        mastodon: '@robin@example.com',
         organization: 'organization',
         skype: 'skype',
         twitter: 'twitter',
@@ -117,6 +139,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', :mastodon
     it_behaves_like 'prevents `nil` value', :organization
     it_behaves_like 'prevents `nil` value', :skype
     it_behaves_like 'prevents `nil` value', :twitter
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 947d83badf636531..c9da1a31c8686e51 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -113,6 +113,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(:mastodon).to(:user_detail).allow_nil }
+    it { is_expected.to delegate_method(:mastodon=).to(:user_detail).with_arguments(:args).allow_nil }
+
     it { is_expected.to delegate_method(:twitter).to(:user_detail).allow_nil }
     it { is_expected.to delegate_method(:twitter=).to(:user_detail).with_arguments(:args).allow_nil }
 
-- 
GitLab


From 2fac622ff3567e815f9d5b27372f5d9fcfe96f33 Mon Sep 17 00:00:00 2001
From: Raimund Hook <rhook@gitlab.com>
Date: Wed, 11 Oct 2023 16:40:06 +0100
Subject: [PATCH 2/7] Updates from review

Signed-off-by: Raimund Hook <rhook@gitlab.com>
---
 app/views/profiles/show.html.haml                         | 2 +-
 app/views/users/show.html.haml                            | 3 ++-
 db/migrate/20230927124202_add_mastodon_to_user_details.rb | 4 +---
 locale/gitlab.pot                                         | 3 ---
 4 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 6ff7583e736d384f..b50a454df72baf51 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -124,7 +124,7 @@
           = external_accounts_docs_link
       .form-group.gl-form-group
         = f.label :mastodon
-        = f.text_field :mastodon, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@robin@example.com")
+        = f.text_field :mastodon, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: "@robin@example.com"
 
       .form-group.gl-form-group
         = f.label :website_url, s_('Profiles|Website url')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 13c5f73d96024bd1..ddde0f17d16729e5 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -113,8 +113,9 @@
                       = sprite_icon('discord', css_class: 'discord-icon')
                 - if @user.mastodon.present?
                   = render 'middle_dot_divider', breakpoint: 'sm' do
+                    = sprite_icon('mastodon', css_class: 'mastodon-icon')
                     = link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do
-                      = sprite_icon('mastodon', css_class: 'mastodon-icon')
+                      = mastodon_url(@user).gsub('https://', '')
                 - if @user.website_url.present?
                   = render 'middle_dot_divider', stacking: true do
                     - if Feature.enabled?(:security_auto_fix) && @user.bot?
diff --git a/db/migrate/20230927124202_add_mastodon_to_user_details.rb b/db/migrate/20230927124202_add_mastodon_to_user_details.rb
index b23fd21837954be2..a1aa099087b98c51 100644
--- a/db/migrate/20230927124202_add_mastodon_to_user_details.rb
+++ b/db/migrate/20230927124202_add_mastodon_to_user_details.rb
@@ -7,15 +7,13 @@ class AddMastodonToUserDetails < Gitlab::Database::Migration[2.1]
 
   def up
     with_lock_retries do
-      add_column :user_details, :mastodon, :text, default: '', null: false
+      add_column :user_details, :mastodon, :text, default: '', null: false, if_not_exists: true
     end
 
     add_text_limit :user_details, :mastodon, USER_DETAILS_FIELD_LIMIT
   end
 
   def down
-    remove_text_limit :user_details, :mastodon
-
     with_lock_retries do
       remove_column :user_details, :mastodon
     end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ca11ea53c0668c86..9c223a5381cceaa2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -36207,9 +36207,6 @@ msgstr ""
 msgid "Profiles|%{provider} Active"
 msgstr ""
 
-msgid "Profiles|@robin@example.com"
-msgstr ""
-
 msgid "Profiles|@username"
 msgstr ""
 
-- 
GitLab


From cf34663fc8d7f34b5b316906084c28d44583ac27 Mon Sep 17 00:00:00 2001
From: Raimund Hook <rhook@gitlab.com>
Date: Fri, 13 Oct 2023 15:56:47 +0100
Subject: [PATCH 3/7] Added FF for UI elements with user actor

Signed-off-by: Raimund Hook <rhook@gitlab.com>
---
 app/views/profiles/show.html.haml                       | 7 ++++---
 app/views/users/show.html.haml                          | 5 ++---
 config/feature_flags/development/mastodon_social_ui.yml | 8 ++++++++
 3 files changed, 14 insertions(+), 6 deletions(-)
 create mode 100644 config/feature_flags/development/mastodon_social_ui.yml

diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index b50a454df72baf51..002953b165831c64 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -122,9 +122,10 @@
           allow_empty: true}
         %small.form-text.text-gl-muted
           = external_accounts_docs_link
-      .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"
+      - if Feature.enabled?(:mastodon_social_ui, @user)
+        .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"
 
       .form-group.gl-form-group
         = f.label :website_url, s_('Profiles|Website url')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index ddde0f17d16729e5..29360dc7c9484d21 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -111,11 +111,10 @@
                   = render 'middle_dot_divider', breakpoint: 'sm' do
                     = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
                       = sprite_icon('discord', css_class: 'discord-icon')
-                - if @user.mastodon.present?
+                - if Feature.enabled?(:mastodon_social_ui, @user) && @user.mastodon.present?
                   = render 'middle_dot_divider', breakpoint: 'sm' do
-                    = sprite_icon('mastodon', css_class: 'mastodon-icon')
                     = link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do
-                      = mastodon_url(@user).gsub('https://', '')
+                      = sprite_icon('mastodon', css_class: 'mastodon-icon')
                 - if @user.website_url.present?
                   = render 'middle_dot_divider', stacking: true do
                     - if Feature.enabled?(:security_auto_fix) && @user.bot?
diff --git a/config/feature_flags/development/mastodon_social_ui.yml b/config/feature_flags/development/mastodon_social_ui.yml
new file mode 100644
index 0000000000000000..5e04d8176e4924d1
--- /dev/null
+++ b/config/feature_flags/development/mastodon_social_ui.yml
@@ -0,0 +1,8 @@
+---
+name: mastodon_social_ui
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132892
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428163
+milestone: '16.5'
+type: development
+group: group::tenant scale
+default_enabled: false
-- 
GitLab


From f97d421bb0c8ecda1386df7af6e42d246e17aa40 Mon Sep 17 00:00:00 2001
From: Raimund Hook <rhook@gitlab.com>
Date: Fri, 13 Oct 2023 16:06:55 +0100
Subject: [PATCH 4/7] Update doc message with an 'introduced' blurb

Signed-off-by: Raimund Hook <rhook@gitlab.com>
---
 doc/user/profile/index.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 76b454f164381d2c..183a1bc599d89aac 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -128,6 +128,8 @@ to match your username.
 
 ## Add external accounts to your user profile page
 
+> Mastodon user account [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132892) as a beta feature in 16.5 [with a flag](../feature_flags.md) named `mastodon_social_ui`. Disabled by default.
+
 You can add links to certain other external accounts you might have, like Skype and Twitter.
 They can help other users connect with you on other platforms.
 
-- 
GitLab


From 97fcf10989aa0031cff57c3b4847ac11519e6136 Mon Sep 17 00:00:00 2001
From: Niklas <mc.taucher2003@gmail.com>
Date: Tue, 17 Oct 2023 09:23:35 +0000
Subject: [PATCH 5/7] Apply 1 suggestion(s) to 1 file(s)

---
 spec/models/user_detail_spec.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 58f4322f21f1dc24..b443988cde9aa200 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -62,7 +62,7 @@
     describe '#mastodon' do
       it { is_expected.to validate_length_of(:mastodon).is_at_most(500) }
 
-      context 'when discord is set' do
+      context 'when mastodon is set' do
         let_it_be(:user_detail) { create(:user_detail) }
 
         it 'accepts a valid mastodon username' do
-- 
GitLab


From 5050e772b91c7b9599a7d286363cf554bda4ba67 Mon Sep 17 00:00:00 2001
From: Jon Glassman <jglassman@gitlab.com>
Date: Tue, 17 Oct 2023 11:50:06 +0000
Subject: [PATCH 6/7] Apply 1 suggestion(s) to 1 file(s)

---
 doc/user/profile/index.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 183a1bc599d89aac..fea9bc491cf9a4b7 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -128,7 +128,7 @@ to match your username.
 
 ## Add external accounts to your user profile page
 
-> Mastodon user account [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132892) as a beta feature in 16.5 [with a flag](../feature_flags.md) named `mastodon_social_ui`. Disabled by default.
+> Mastodon user account [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132892) as a beta feature in 16.5 [with a flag](../feature_flags.md) named `mastodon_social_ui`. Disabled by default.
 
 You can add links to certain other external accounts you might have, like Skype and Twitter.
 They can help other users connect with you on other platforms.
-- 
GitLab


From fb2df5c629879dbbe2d6ab9209b352cf6b2da706 Mon Sep 17 00:00:00 2001
From: Raimund Hook <rhook@gitlab.com>
Date: Wed, 18 Oct 2023 18:52:11 +0100
Subject: [PATCH 7/7] Extract regex into constant

Signed-off-by: Raimund Hook <rhook@gitlab.com>
---
 app/helpers/application_helper.rb |  2 +-
 app/models/user_detail.rb         | 14 +++++++++++++-
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 60c77e14a5554319..541a71d3302f80af 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -374,7 +374,7 @@ def discord_url(user)
   def mastodon_url(user)
     return '' if user.mastodon.blank?
 
-    url = user.mastodon.match %r{\A@?\b([\w\d.%+-]+)@([\w\d.-]+\.\w{2,})\b\z}
+    url = user.mastodon.match UserDetail::MASTODON_VALIDATION_REGEX
 
     "https://#{url[2]}/@#{url[1]}"
   end
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index fcf10adae6f67bed..bbb08ed577430c04 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -17,6 +17,18 @@ class UserDetail < MainClusterwide::ApplicationRecord
 
   DEFAULT_FIELD_LENGTH = 500
 
+  MASTODON_VALIDATION_REGEX = /
+    \A            # beginning of string
+    @?\b          # optional leading at
+    ([\w\d.%+-]+) # character group to pick up words in user portion of username
+    @             # separator between user and host
+    (             # beginning of charagter group for host portion
+      [\w\d.-]+   # character group to pick up words in host portion of username
+      \.\w{2,}    # pick up tld of host domain, 2 chars or more
+    )\b           # end of character group to pick up words in host portion of username
+    \z            # end of string
+  /x
+
   validates :discord, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
   validate :discord_format
   validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
@@ -66,7 +78,7 @@ def discord_format
 end
 
 def mastodon_format
-  return if mastodon.blank? || mastodon =~ %r{\A@?\b([\w\d.%+-]+)@([\w\d.-]+\.\w{2,})\b\z}
+  return if mastodon.blank? || mastodon =~ UserDetail::MASTODON_VALIDATION_REGEX
 
   errors.add(:mastodon, _('must contain only a mastodon username.'))
 end
-- 
GitLab