Use dedicated indicator for non-ascii domain names

This commit is contained in:
Tusooa Zhu 2022-08-29 18:46:41 -04:00
parent e812d5ea3c
commit 0a79a74773
No known key found for this signature in database
GPG Key ID: 7B467EDE43A08224
22 changed files with 151 additions and 49 deletions

View File

@ -1,5 +1,6 @@
import UserPopover from '../user_popover/user_popover.vue' import UserPopover from '../user_popover/user_popover.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -10,7 +11,8 @@ const BasicUserCard = {
components: { components: {
UserPopover, UserPopover,
UserAvatar, UserAvatar,
RichContent RichContent,
UserLink
}, },
methods: { methods: {
userProfileLink (user) { userProfileLink (user) {

View File

@ -30,12 +30,10 @@
/> />
</div> </div>
<div> <div>
<router-link <user-link
class="basic-user-card-screen-name" class="basic-user-card-screen-name"
:to="userProfileLink(user)" :user="user"
> />
@{{ user.screen_name_ui }}
</router-link>
</div> </div>
<slot /> <slot />
</div> </div>

View File

@ -1,5 +1,6 @@
import Completion from '../../services/completion/completion.js' import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue' import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash' import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@ -120,7 +121,8 @@ const EmojiInput = {
} }
}, },
components: { components: {
EmojiPicker EmojiPicker,
UnicodeDomainIndicator
}, },
computed: { computed: {
padEmoji () { padEmoji () {

View File

@ -50,7 +50,21 @@
<span v-else>{{ suggestion.replacement }}</span> <span v-else>{{ suggestion.replacement }}</span>
</span> </span>
<div class="label"> <div class="label">
<span class="displayText">{{ suggestion.displayText }}</span> <span
v-if="suggestion.user"
class="displayText"
>
{{ suggestion.displayText }}<UnicodeDomainIndicator
:user="suggestion.user"
:at="false"
/>
</span>
<span
v-if="!suggestion.user"
class="displayText"
>
{{ suggestion.displayText }}
</span>
<span class="detailText">{{ suggestion.detailText }}</span> <span class="detailText">{{ suggestion.detailText }}</span>
</div> </div>
</div> </div>

View File

@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => {
return diff + nameAlphabetically + screenNameAlphabetically return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */ /* eslint-disable camelcase */
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ }).map((user) => ({
displayText: screen_name_ui, user,
detailText: name, displayText: user.screen_name_ui,
imageUrl: profile_image_url_original, detailText: user.name,
replacement: '@' + screen_name + ' ' imageUrl: user.profile_image_url_original,
replacement: '@' + user.screen_name + ' '
})) }))
/* eslint-enable camelcase */ /* eslint-enable camelcase */

View File

@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -16,6 +17,7 @@ const MentionLink = {
name: 'MentionLink', name: 'MentionLink',
components: { components: {
UserAvatar, UserAvatar,
UnicodeDomainIndicator,
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue')) UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
}, },
props: { props: {

View File

@ -47,6 +47,9 @@
class="serverName" class="serverName"
:class="{ '-faded': shouldFadeDomain }" :class="{ '-faded': shouldFadeDomain }"
v-html="'@' + serverName" v-html="'@' + serverName"
/><UnicodeDomainIndicator
v-if="shouldShowFullUserName"
:user="user"
/> />
</span> </span>
<span <span

View File

@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import Report from '../report/report.vue' import Report from '../report/report.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserPopover from '../user_popover/user_popover.vue' import UserPopover from '../user_popover/user_popover.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
@ -50,7 +51,8 @@ const Notification = {
Status, Status,
Report, Report,
RichContent, RichContent,
UserPopover UserPopover,
UserLink
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View File

@ -11,9 +11,10 @@
class="Notification container -muted" class="Notification container -muted"
> >
<small> <small>
<router-link :to="userProfileLink"> <user-link
{{ notification.from_profile.screen_name_ui }} :user="notification.from_profile"
</router-link> :at="false"
/>
</small> </small>
<button <button
class="button-unstyled unmute" class="button-unstyled unmute"
@ -174,12 +175,10 @@
v-if="notification.type === 'follow' || notification.type === 'follow_request'" v-if="notification.type === 'follow' || notification.type === 'follow_request'"
class="follow-text" class="follow-text"
> >
<router-link <user-link
:to="userProfileLink"
class="follow-name" class="follow-name"
> :user="notification.from_profile"
@{{ notification.from_profile.screen_name_ui }} />
</router-link>
<div <div
v-if="notification.type === 'follow_request'" v-if="notification.type === 'follow_request'"
style="white-space: nowrap;" style="white-space: nowrap;"
@ -210,9 +209,9 @@
v-else-if="notification.type === 'move'" v-else-if="notification.type === 'move'"
class="move-text" class="move-text"
> >
<router-link :to="targetUserProfileLink"> <user-link
@{{ notification.target.screen_name_ui }} :user="notification.target"
</router-link> />
</div> </div>
<Report <Report
v-else-if="notification.type === 'pleroma:report'" v-else-if="notification.type === 'pleroma:report'"

View File

@ -13,6 +13,7 @@ import StatusPopover from '../status_popover/status_popover.vue'
import UserPopover from '../user_popover/user_popover.vue' import UserPopover from '../user_popover/user_popover.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import UserLink from '../user_link/user_link.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue' import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue' import MentionLink from 'src/components/mention_link/mention_link.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -115,7 +116,8 @@ const Status = {
RichContent, RichContent,
MentionLink, MentionLink,
MentionsLine, MentionsLine,
UserPopover UserPopover,
UserLink
}, },
props: [ props: [
'statusoid', 'statusoid',

View File

@ -25,9 +25,10 @@
class="fa-scale-110 fa-old-padding repeat-icon" class="fa-scale-110 fa-old-padding repeat-icon"
icon="retweet" icon="retweet"
/> />
<router-link :to="userProfileLink"> <user-link
{{ status.user.screen_name_ui }} :user="status.user"
</router-link> :at="false"
/>
</small> </small>
<small <small
v-if="showReasonMutedThread" v-if="showReasonMutedThread"
@ -164,13 +165,12 @@
> >
{{ status.user.name }} {{ status.user.name }}
</h4> </h4>
<router-link <user-link
class="account-name" class="account-name"
:title="status.user.screen_name_ui" :title="status.user.screen_name_ui"
:to="userProfileLink" :user="status.user"
> :at="false"
{{ status.user.screen_name_ui }} />
</router-link>
<img <img
v-if="!!(status.user && status.user.favicon)" v-if="!!(status.user && status.user.favicon)"
class="status-favicon" class="status-favicon"

View File

@ -0,0 +1,26 @@
<template>
<FAIcon
v-if="user && user.screen_name_ui_contains_non_ascii"
icon="code"
:title="$t('unicode_domain_indicator.tooltip')"
/>
</template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCode
} from '@fortawesome/free-solid-svg-icons'
library.add(
faCode
)
const UnicodeDomainIndicator = {
props: {
user: Object
}
}
export default UnicodeDomainIndicator
</script>

View File

@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue' import AccountActions from '../account_actions/account_actions.vue'
import Select from '../select/select.vue' import Select from '../select/select.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
@ -134,7 +135,8 @@ export default {
ProgressButton, ProgressButton,
FollowButton, FollowButton,
Select, Select,
RichContent RichContent,
UserLink
}, },
methods: { methods: {
muteUser () { muteUser () {

View File

@ -106,13 +106,10 @@
</button> </button>
</div> </div>
<div class="bottom-line"> <div class="bottom-line">
<router-link <user-link
class="user-screen-name" class="user-screen-name"
:title="user.screen_name_ui" :user="user"
:to="userProfileLink(user)" />
>
@{{ user.screen_name_ui }}
</router-link>
<template v-if="!hideBio"> <template v-if="!hideBio">
<span <span
v-if="user.deactivated" v-if="user.deactivated"

View File

@ -0,0 +1,38 @@
<template>
<router-link
:title="user.screen_name_ui"
:to="userProfileLink(user)"
>
{{ at ? '@' : '' }}{{ user.screen_name_ui }}<UnicodeDomainIndicator
:user="user"
/>
</router-link>
</template>
<script>
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const UserLink = {
props: {
user: Object,
at: {
type: Boolean,
default: true
}
},
components: {
UnicodeDomainIndicator
},
methods: {
userProfileLink (user) {
return generateProfileLink(
user.id, user.screen_name,
this.$store.state.instance.restrictedNicknames
)
}
}
}
export default UserLink
</script>

View File

@ -1,5 +1,6 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@ -15,6 +16,7 @@ const UserListPopover = {
], ],
components: { components: {
RichContent, RichContent,
UnicodeDomainIndicator,
Popover: defineAsyncComponent(() => import('../popover/popover.vue')), Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue')) UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue'))
}, },

View File

@ -29,7 +29,7 @@
:emoji="user.emoji" :emoji="user.emoji"
/> />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<span class="user-list-screen-name">{{ user.screen_name_ui }}</span> <span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" />
</div> </div>
</div> </div>
</template> </template>

View File

@ -2,13 +2,15 @@ import Status from '../status/status.vue'
import List from '../list/list.vue' import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import Modal from '../modal/modal.vue' import Modal from '../modal/modal.vue'
import UserLink from '../user_link/user_link.vue'
const UserReportingModal = { const UserReportingModal = {
components: { components: {
Status, Status,
List, List,
Checkbox, Checkbox,
Modal Modal,
UserLink
}, },
data () { data () {
return { return {

View File

@ -5,9 +5,13 @@
> >
<div class="user-reporting-panel panel"> <div class="user-reporting-panel panel">
<div class="panel-heading"> <div class="panel-heading">
<div class="title"> <i18n-t
{{ $t('user_reporting.title', [user.screen_name_ui]) }} tag="div"
</div> keypath="user_reporting.title"
class="title"
>
<UserLink :user="user" />
</i18n-t>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="user-reporting-panel-left"> <div class="user-reporting-panel-left">

View File

@ -1006,5 +1006,8 @@
"update_changelog": "For more details on what's changed, see {theFullChangelog}.", "update_changelog": "For more details on what's changed, see {theFullChangelog}.",
"update_changelog_here": "the full changelog", "update_changelog_here": "the full changelog",
"art_by": "Art by {linkToArtist}" "art_by": "Art by {linkToArtist}"
},
"unicode_domain_indicator": {
"tooltip": "This domain contains non-ascii characters."
} }
} }

View File

@ -214,12 +214,14 @@ export const parseUser = (data) => {
output.screen_name_ui = output.screen_name output.screen_name_ui = output.screen_name
if (output.screen_name && output.screen_name.includes('@')) { if (output.screen_name && output.screen_name.includes('@')) {
const parts = output.screen_name.split('@') const parts = output.screen_name.split('@')
let unicodeDomain = punycode.toUnicode(parts[1]) const unicodeDomain = punycode.toUnicode(parts[1])
if (unicodeDomain !== parts[1]) { if (unicodeDomain !== parts[1]) {
// Add some identifier so users can potentially spot spoofing attempts: // Add some identifier so users can potentially spot spoofing attempts:
// lain.com and xn--lin-6cd.com would appear identical otherwise. // lain.com and xn--lin-6cd.com would appear identical otherwise.
unicodeDomain = '🌏' + unicodeDomain output.screen_name_ui_contains_non_ascii = true
output.screen_name_ui = [parts[0], unicodeDomain].join('@') output.screen_name_ui = [parts[0], unicodeDomain].join('@')
} else {
output.screen_name_ui_contains_non_ascii = false
} }
} }

View File

@ -269,7 +269,8 @@ describe('API Entities normalizer', () => {
it('converts IDN to unicode and marks it as internatonal', () => { it('converts IDN to unicode and marks it as internatonal', () => {
const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' }) const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' })
expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@🌏lаin.com') expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@lаin.com')
expect(parseUser(user)).to.have.property('screen_name_ui_contains_non_ascii').that.equal(true)
}) })
}) })