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