diff --git a/app/avo/resources/user.rb b/app/avo/resources/user.rb
index df5ab774bb4..d34770c99d1 100644
--- a/app/avo/resources/user.rb
+++ b/app/avo/resources/user.rb
@@ -33,7 +33,7 @@ def fields # rubocop:disable Metrics
field :email_reset, as: :boolean
field :handle, as: :text
field :public_email, as: :boolean
- field :twitter_username, as: :text, as_html: true, format_using: -> { link_to value, "https://twitter.com/#{value}", target: :_blank, rel: :noopener if value.present? }
+ field :social_link, as: :text, as_html: true
field :unconfirmed_email, as: :text
field :mail_fails, as: :number
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 58b613dd642..c3695440ce7 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -61,7 +61,7 @@ def params_user
end
end
- PERMITTED_PROFILE_PARAMS = %i[handle twitter_username unconfirmed_email public_email full_name].freeze
+ PERMITTED_PROFILE_PARAMS = %i[handle social_link unconfirmed_email public_email full_name].freeze
def verify_password
password = params.expect(user: :password)[:password]
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 6989e304fa5..41bd9c33dca 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -27,7 +27,7 @@ def create
location
password
website
- twitter_username
+ social_link
full_name
].freeze
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 2525181b134..c0853233443 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -1,10 +1,6 @@
module UsersHelper
- def twitter_username(user)
- "@#{user.twitter_username}" if user.twitter_username.present?
- end
-
- def twitter_url(user)
- "https://twitter.com/#{user.twitter_username}"
+ def social_link(user)
+ user.social_link.presence
end
def show_policies_acknowledge_banner?(user)
diff --git a/app/models/user.rb b/app/models/user.rb
index 4e2de08a037..e537d0cca90 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -73,10 +73,7 @@ class User < ApplicationRecord
validates :handle, format: { with: Patterns::HANDLE_PATTERN }, length: { within: 2..40 }, allow_nil: true
validate :unique_with_org_handle
- validates :twitter_username, format: {
- with: /\A[a-zA-Z0-9_]*\z/,
- message: "can only contain letters, numbers, and underscores"
- }, allow_nil: true, length: { within: 0..20 }
+ validates :social_link, length: { maximum: Gemcutter::MAX_FIELD_LENGTH }, format: { with: Patterns::URL_VALIDATION_REGEXP }, allow_blank: true
validates :password,
length: { minimum: 10 },
@@ -344,7 +341,8 @@ def clear_personal_attributes
handle: nil, email_confirmed: false,
unconfirmed_email: nil, blocked_email: nil,
api_key: nil, confirmation_token: nil, remember_token: nil,
- twitter_username: nil, webauthn_id: nil, full_name: nil,
+ twitter_username: nil, social_link: nil,
+ webauthn_id: nil, full_name: nil,
totp_seed: nil, mfa_hashed_recovery_codes: nil,
mfa_level: :disabled,
password: SecureRandom.hex(20).encode("UTF-8")
diff --git a/app/views/dashboards/_subject.html.erb b/app/views/dashboards/_subject.html.erb
index b7efadc3565..bbbc82ae425 100644
--- a/app/views/dashboards/_subject.html.erb
+++ b/app/views/dashboards/_subject.html.erb
@@ -21,14 +21,10 @@
<% end %>
- <% if user.twitter_username.present? %>
+ <% if user.social_link.present? %>
- <%= icon_tag("x-twitter", color: :primary, class: "w-6 text-orange mr-3") %>
<%=
- link_to(
- twitter_username(user),
- twitter_url(user)
- )
+ link_to(social_link(user))
%>
<% end %>
diff --git a/app/views/profiles/edit.html.erb b/app/views/profiles/edit.html.erb
index 0af673a168c..1e1b6bb59fc 100644
--- a/app/views/profiles/edit.html.erb
+++ b/app/views/profiles/edit.html.erb
@@ -21,21 +21,16 @@
- <%= form.label :twitter_username, class: 'form__label form__label__icon-container' do %>
- <%=
- image_tag("/images/x_icon.png", alt: 'X icon', class: 'form__label__icon')
- %>
-
-
<%= t('.twitter_username') %>
+ <%= form.label :social_link, class: 'form__label form__label__icon-container' do %>
+
<%= t('.social_link') %>
<% end %>
- <%= t('.optional_twitter_username') %>
+ <%= t('.optional_social_link') %>
- @
- <%= form.text_field(:twitter_username, class: 'form__input') %>
+ <%= form.text_field(:social_link, class: 'form__input') %>
diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb
index 4076e2383da..f5f82344072 100644
--- a/app/views/profiles/show.html.erb
+++ b/app/views/profiles/show.html.erb
@@ -65,19 +65,10 @@
<% end %>
- <% if @user.twitter_username.present? %>
- <%=
- image_tag(
- "/images/x_icon.png",
- alt: "X icon",
- class: "profile__header__icon"
- )
- %>
-
+ <% if @user.social_link.present? %>
<%=
link_to(
- twitter_username(@user),
- twitter_url(@user),
+ social_link(@user),
class: "profile__header__attribute t-link--black"
)
%>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 4d9b7b5c17a..0dcc5e68bc3 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -763,8 +763,8 @@ de:
email_awaiting_confirmation:
enter_password:
optional_full_name:
- optional_twitter_username:
- twitter_username: Benutzername
+ optional_social_link:
+ social_link:
title: Bearbeite Profil
delete:
delete:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 31581e209c5..0c53d2e49af 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -684,8 +684,8 @@ en:
email_awaiting_confirmation: Please confirm your new email address %{unconfirmed_email}
enter_password: Please enter your account's password
optional_full_name: Optional. Will be displayed publicly
- optional_twitter_username: Optional X username. Will be displayed publicly
- twitter_username: Username
+ optional_social_link: Optional social link. Will be displayed publicly
+ social_link: Social Link
title: Edit profile
delete:
delete: Delete
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 0736cccf22c..83ba495d96d 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -767,9 +767,8 @@ es:
%{unconfirmed_email}
enter_password: Por favor introduce tu contraseña
optional_full_name: Opcional. Será mostrado en tu perfil público
- optional_twitter_username: Usuario de X opcional. Será mostrado en tu perfil
- público
- twitter_username: Usuario
+ optional_social_link:
+ social_link:
title: Editar perfil
delete:
delete: Eliminar
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 4a5d1577903..3b573ca65e9 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -696,8 +696,8 @@ fr:
%{unconfirmed_email}
enter_password: Veuillez entrer le mot de passe de votre compte
optional_full_name:
- optional_twitter_username: Nom d'utilisateur X optionnel. Sera affiché publiquement
- twitter_username: Pseudonyme
+ optional_social_link:
+ social_link:
title: Modification de profil
delete:
delete: Supprimer
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index e9593afbf77..7453ee1b5e1 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -686,8 +686,8 @@ ja:
email_awaiting_confirmation: 新しいEメールアドレス%{unconfirmed_email}を確認してください。
enter_password: アカウントのパスワードを入力してください。
optional_full_name: 省略可能です。公開されます。
- optional_twitter_username: Xのユーザー名。省略可能です。公開されます。
- twitter_username: ユーザー名
+ optional_social_link:
+ social_link:
title: プロフィールを編集
delete:
delete: 削除
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index c4edc022f8f..ee516bdc0e2 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -675,8 +675,8 @@ nl:
email_awaiting_confirmation:
enter_password:
optional_full_name:
- optional_twitter_username:
- twitter_username: Gebruikersnaam
+ optional_social_link:
+ social_link:
title: Wijzig profiel
delete:
delete:
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index b7e64a21f8d..23f562b175d 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -687,8 +687,8 @@ pt-BR:
email_awaiting_confirmation:
enter_password:
optional_full_name:
- optional_twitter_username:
- twitter_username: Usuário
+ optional_social_link:
+ social_link:
title: Editar Perfil
delete:
delete:
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index baf1c380640..c1e18212e51 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -684,8 +684,8 @@ zh-CN:
email_awaiting_confirmation: 请确认您新的邮箱地址 %{unconfirmed_email}
enter_password: 请输入您账户的密码
optional_full_name: 将公开显示(可选)
- optional_twitter_username: 可选 X 用户名。这将会被公开显示
- twitter_username: 用户名
+ optional_social_link:
+ social_link:
title: 修改个人资料
delete:
delete: 删除
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index c51bb260027..1daa8dd53e2 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -676,8 +676,8 @@ zh-TW:
email_awaiting_confirmation: 請確認您的新電子郵件地址 %{unconfirmed_email}
enter_password: 輸入密碼
optional_full_name: 選填。將公開顯示
- optional_twitter_username: X 帳號(可選)
- twitter_username: 帳號
+ optional_social_link:
+ social_link:
title: 編輯個人檔案
delete:
delete: 刪除
diff --git a/db/migrate/20250715222503_add_social_link_to_users.rb b/db/migrate/20250715222503_add_social_link_to_users.rb
new file mode 100644
index 00000000000..21a7572c1d5
--- /dev/null
+++ b/db/migrate/20250715222503_add_social_link_to_users.rb
@@ -0,0 +1,5 @@
+class AddSocialLinkToUsers < ActiveRecord::Migration[8.0]
+ def change
+ add_column :users, :social_link, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e635eaa8028..4a577b1593f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_07_02_195347) do
+ActiveRecord::Schema[8.0].define(version: 2025_07_15_222503) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
enable_extension "pg_catalog.plpgsql"
@@ -603,6 +603,7 @@
t.boolean "public_email", default: false, null: false
t.datetime "deleted_at"
t.datetime "policies_acknowledged_at"
+ t.string "social_link"
t.index "lower((email)::text) varchar_pattern_ops", name: "index_users_on_lower_email"
t.index ["email"], name: "index_users_on_email"
t.index ["handle"], name: "index_users_on_handle"
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index cbd6ac2796d..f43ec7ca81a 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -162,14 +162,26 @@ class UserTest < ActiveSupport::TestCase
end
end
- context "twitter_username" do
- should validate_length_of(:twitter_username).is_at_most(20)
- should allow_value("user123_32").for(:twitter_username)
- should_not allow_value("@user").for(:twitter_username)
- should_not allow_value("user 1").for(:twitter_username)
- should_not allow_value("user-1").for(:twitter_username)
- should allow_value("01234567890123456789").for(:twitter_username)
- should_not allow_value("012345678901234567890").for(:twitter_username)
+ context "social_link" do
+ should "be less than 255 characters" do
+ user = build(:user, social_link: format("%s.example.com", "a" * 256))
+
+ refute_predicate user, :valid?
+ assert_contains user.errors[:social_link], "is too long (maximum is 255 characters)"
+ end
+
+ should "be valid when it matches a valid URI regex" do
+ user = build(:user, social_link: "https://example.com/someone")
+
+ assert_predicate user, :valid?
+ end
+
+ should "be invalid when it doesn't match a valid URI regex" do
+ user = build(:user, social_link: ">\".gmail.com")
+
+ refute_predicate user, :valid?
+ assert_contains user.errors[:social_link], "is invalid"
+ end
end
context "password" do
diff --git a/test/system/profile_test.rb b/test/system/profile_test.rb
index 0988e056b94..ea0e6b2a7da 100644
--- a/test/system/profile_test.rb
+++ b/test/system/profile_test.rb
@@ -107,42 +107,43 @@ class ProfileTest < ApplicationSystemTestCase
assert page.has_content?("Email Me")
end
- test "adding X(formerly Twitter) username" do
+ test "adding social link" do
+ social_link = "https://example.com/nick1"
sign_in
visit profile_path("nick1")
click_link "Edit Profile"
- fill_in "user_twitter_username", with: "nick1"
+ fill_in "user_social_link", with: social_link
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Update"
sign_out
visit profile_path("nick1")
- assert page.has_link?("@nick1", href: "https://twitter.com/nick1")
+ assert page.has_link?(social_link)
end
- test "adding X(formerly Twitter) username without filling in your password" do
- twitter_username = "nick1twitter"
+ test "adding social link without filling in your password" do
+ social_link = "https://example.com/nick1"
sign_in
visit profile_path("nick1")
click_link "Edit Profile"
- fill_in "user_twitter_username", with: twitter_username
+ fill_in "user_social_link", with: social_link
- assert_equal twitter_username, page.find_by_id("user_twitter_username").value
+ assert_equal social_link, page.find_by_id("user_social_link").value
click_button "Update"
- # Verify that the newly added Twitter username is still on the form so that the user does not need to re-enter it
- assert_equal twitter_username, page.find_by_id("user_twitter_username").value
+ # Verify that the newly added social link is still on the form so that the user does not need to re-enter it
+ assert_equal social_link, page.find_by_id("user_social_link").value
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Update"
assert page.has_content? "Your profile was updated."
- assert_equal twitter_username, page.find_by_id("user_twitter_username").value
+ assert_equal social_link, page.find_by_id("user_social_link").value
end
test "deleting profile" do