Add the default emoji skin tone user preference

What does this MR do and why?

Adds a per-user Default emoji skin tone preference with six values (default plus five tones). It is the second step of the default emoji skin tone epic, after the skin tone mapping fixture.

  • Database: emoji_skin_tone smallint DEFAULT 0 NOT NULL on user_preferences, a Rails enum on UserPreference, and delegates on User.
  • GraphQL: UserPreferences.emojiSkinTone, an emojiSkinTone argument on userPreferencesUpdate that rejects null, and the EmojiSkinTone enum. Field and argument are marked experiment for 19.6.
  • REST: emoji_skin_tone on GET /user/preferences and PUT /user/preferences, restricted to the six values. An unknown value returns 400 with emoji_skin_tone does not have a valid value.
  • Behind the new emoji_skin_tone_preference flag (wip, default off): a Default emoji skin tone dropdown list on the preferences page, with the thumbs up in each tone next to the tone name as the options, and gon.emoji_skin_tone. With the flag off nothing is visible and the gon key is absent.

Nothing applies the tone yet. The consumers are server-side reaction toggling and the web picker and autocomplete, which need this MR deployed one release ahead of the frontend.

Resolves https://gitlab.com/gitlab-org/gitlab/-/issues/633107.

Decisions
  • The enum value default mirrors Slack and Unicode. scopes: false avoids a UserPreference.default class method, and prefix: true keeps the predicates (emoji_skin_tone_light?) off generic names such as light? and dark?.
  • A typed column rather than the JSON settings blob, because the value is validated and queried through two APIs.
  • The GraphQL field is non-null because the column is NOT NULL. The mutation argument rejects null instead of silently ignoring it, so a client gets an error rather than an unchanged value.
  • A presence validation on the model turns a blank form value into a 400 instead of a NOT NULL violation.
  • The setting sits on the Editing tab in the Markdown section, next to Enable emoji autocomplete. The issue says "under Behavior", but the preferences page was split into tabs after the issue was written and the emoji settings now live in Editing.
  • The control is a listbox mounted through the page's existing .js-listbox-input hook, the same as the Homepage setting on the Behavior tab, so no JavaScript was added. Each option shows the thumbs up in that tone followed by the Unicode modifier name (Light to Dark, with Default for the unmodified emoji), which makes a separate preview unnecessary. The emoji is the native Unicode character, because listbox options are plain text. The choice is saved with Save changes, like the other dropdown lists on the page.

Database

The migration adds one column with a default to user_preferences (one row per user, no index). No new queries: the preference is read through the existing lookup of user_preferences by user_id.

Migration output
main: == 20261014224518 AddEmojiSkinToneToUserPreferences: migrating ================
main: -- add_column(:user_preferences, :emoji_skin_tone, :smallint, {default: 0, null: false})
main:    -> 0.0148s
main: == 20261014224518 AddEmojiSkinToneToUserPreferences: migrated (0.0196s) =======

main: == 20261014224518 AddEmojiSkinToneToUserPreferences: reverting ================
main: -- remove_column(:user_preferences, :emoji_skin_tone, :smallint, {default: 0, null: false})
main:    -> 0.0118s
main: == 20261014224518 AddEmojiSkinToneToUserPreferences: reverted (0.0202s) =======

References

Screenshots or screen recordings

Preferences, Editing tab, Markdown section.

Before (flag off, same as master) After (flag on)
before-flag-off after-listbox-dark

The same setting in light mode:

after-listbox-light

How to set up and validate locally

  1. In a Rails console, enable the flag:

    Feature.enable(:emoji_skin_tone_preference)
  2. Open Preferences, then the Editing tab. In the Markdown section, open the Default emoji skin tone dropdown list, pick a tone, and select Save changes. Reload: the dropdown list shows the saved tone, and gon.emoji_skin_tone in the browser console holds it.

  3. In GraphiQL (/-/graphql-explorer):

    mutation { userPreferencesUpdate(input: { emojiSkinTone: MEDIUM }) { userPreferences { emojiSkinTone } errors } }
    query { currentUser { userPreferences { emojiSkinTone } } }

    emojiSkinTone: null returns a validation error.

  4. REST, with a personal access token:

    curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --url "http://gdk.test:3000/api/v4/user/preferences?emoji_skin_tone=dark"
    curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --url "http://gdk.test:3000/api/v4/user/preferences?emoji_skin_tone=purple"
    curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" --url "http://gdk.test:3000/api/v4/user/preferences"

    The second call returns 400 with emoji_skin_tone does not have a valid value.

  5. Disable the flag. The dropdown list is gone and gon.emoji_skin_tone is absent, while both APIs keep working.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Marcel van Remmerden

Merge request reports

Loading
Loading