Skip to content

Commit

Permalink
Fix handling of emojis with ZWJ sequences in profile initials
Browse files Browse the repository at this point in the history
  • Loading branch information
absidue committed Apr 25, 2024
1 parent f336215 commit d0d669b
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineComponent } from 'vue'
import { sanitizeForHtmlId } from '../../helpers/accessibility'
import { MAIN_PROFILE_ID } from '../../../constants'
import { getFirstCharacter } from '../../helpers/strings'

export default defineComponent({
name: 'FtProfileBubble',
Expand All @@ -24,14 +25,19 @@ export default defineComponent({
},
emits: ['click'],
computed: {
locale: function () {
return this.$i18n.locale.replace('_', '-')
},
isMainProfile: function () {
return this.profileId === MAIN_PROFILE_ID
},
sanitizedId: function() {
return 'profileBubble' + sanitizeForHtmlId(this.profileId)
},
profileInitial: function () {
return this?.profileName?.length > 0 ? Array.from(this.translatedProfileName)[0].toUpperCase() : ''
return this.profileName
? getFirstCharacter(this.translatedProfileName, this.locale).toUpperCase()
: ''
},
translatedProfileName: function () {
return this.isMainProfile ? this.$t('Profile.All Channels') : this.profileName
Expand Down
8 changes: 7 additions & 1 deletion src/renderer/components/ft-profile-edit/ft-profile-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import FtButton from '../../components/ft-button/ft-button.vue'
import { MAIN_PROFILE_ID } from '../../../constants'
import { calculateColorLuminance, colors } from '../../helpers/colors'
import { showToast } from '../../helpers/utils'
import { getFirstCharacter } from '../../helpers/strings'

export default defineComponent({
name: 'FtProfileEdit',
Expand Down Expand Up @@ -47,11 +48,16 @@ export default defineComponent({
}
},
computed: {
locale: function () {
return this.$i18n.locale.replace('_', '-')
},
colorValues: function () {
return colors.map(color => color.value)
},
profileInitial: function () {
return this?.profileName?.length > 0 ? Array.from(this.translatedProfileName)[0].toUpperCase() : ''
return this.profileName
? getFirstCharacter(this.translatedProfileName, this.locale).toUpperCase()
: ''
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
Expand Down
13 changes: 10 additions & 3 deletions src/renderer/components/ft-profile-selector/ft-profile-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import FtCard from '../../components/ft-card/ft-card.vue'
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
import { showToast } from '../../helpers/utils'
import { MAIN_PROFILE_ID } from '../../../constants'
import { getFirstCharacter } from '../../helpers/strings'

export default defineComponent({
name: 'FtProfileSelector',
Expand All @@ -19,19 +20,25 @@ export default defineComponent({
}
},
computed: {
locale: function () {
return this.$i18n.locale.replace('_', '-')
},
profileList: function () {
return this.$store.getters.getProfileList
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeProfileInitial: function () {
// use Array.from, so that emojis don't get split up into individual character codes
return this.activeProfile?.name?.length > 0 ? Array.from(this.translatedProfileName(this.activeProfile))[0].toUpperCase() : ''
return this.activeProfile?.name
? getFirstCharacter(this.translatedProfileName(this.activeProfile), this.locale).toUpperCase()
: ''
},
profileInitials: function () {
return this.profileList.map((profile) => {
return profile?.name?.length > 0 ? Array.from(this.translatedProfileName(profile))[0].toUpperCase() : ''
return profile?.name
? getFirstCharacter(this.translatedProfileName(profile), this.locale).toUpperCase()
: ''
})
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import FtButton from '../../components/ft-button/ft-button.vue'

import { MAIN_PROFILE_ID } from '../../../constants'
import { deepCopy, showToast } from '../../helpers/utils'
import { getFirstCharacter } from '../../helpers/strings'

export default defineComponent({
name: 'FtSubscribeButton',
Expand Down Expand Up @@ -45,9 +46,14 @@ export default defineComponent({
}
},
computed: {
locale: function () {
return this.$i18n.locale.replace('_', '-')
},
profileInitials: function () {
return this.profileDisplayList.map((profile) => {
return profile?.name?.length > 0 ? Array.from(profile.name)[0].toUpperCase() : ''
return profile.name
? getFirstCharacter(profile.name, this.locale).toUpperCase()
: ''
})
},

Expand Down
27 changes: 27 additions & 0 deletions src/renderer/helpers/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,30 @@ export function translateWindowTitle(title, i18n) {
return null
}
}

/**
* Returns the first user-perceived character,
* respecting language specific rules and
* emojis made up of multiple codepoints like flags and families.
* @param {string} text
* @param {string} locale
* @returns {string}
*/
export function getFirstCharacter(text, locale) {
if (text.length === 0) {
return ''
}

// Firefox only received support for Intl.Segmenter support in version 125 (2024-04-16)
// so fallback to Array.from just in case.
// TODO: Remove fallback in the future
if (Intl.Segmenter) {
const segmenter = new Intl.Segmenter([locale, 'en'], { granularity: 'grapheme' })

// Use iterator directly as we only need the first segment
const firstSegment = segmenter.segment(text)[Symbol.iterator]().next().value
return firstSegment.segment
} else {
return Array.from(text)[0]
}
}

0 comments on commit d0d669b

Please sign in to comment.