Pre-fill alt text with 10-million card post (#5389)
* Pre-fill alt text with 10-million card post (#5377)
* Clean up type
* Tweak alt copy
* Add pt translation, fix typo
---------
Co-authored-by: Calvin <clavin@users.noreply.github.com>
(cherry picked from commit 2745cba3ea)
This commit is contained in:
@@ -197,6 +197,27 @@ export function TenMillionInner({
|
|||||||
const isLoadingData = isProfileLoading || !moderation || !profile
|
const isLoadingData = isProfileLoading || !moderation || !profile
|
||||||
const isLoadingImage = !uri
|
const isLoadingImage = !uri
|
||||||
|
|
||||||
|
const displayName = React.useMemo(() => {
|
||||||
|
if (!profile || !moderation) return ''
|
||||||
|
return sanitizeDisplayName(
|
||||||
|
profile.displayName || sanitizeHandle(profile.handle),
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)
|
||||||
|
}, [profile, moderation])
|
||||||
|
const handle = React.useMemo(() => {
|
||||||
|
if (!profile) return ''
|
||||||
|
return sanitizeHandle(profile.handle, '@')
|
||||||
|
}, [profile])
|
||||||
|
const joinedDate = React.useMemo(() => {
|
||||||
|
if (!profile || !profile.createdAt) return ''
|
||||||
|
const date = i18n.date(profile.createdAt, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
return date
|
||||||
|
}, [i18n, profile])
|
||||||
|
|
||||||
const error: string = React.useMemo(() => {
|
const error: string = React.useMemo(() => {
|
||||||
if (profileError) {
|
if (profileError) {
|
||||||
return _(
|
return _(
|
||||||
@@ -235,19 +256,34 @@ export function TenMillionInner({
|
|||||||
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
|
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
|
||||||
userNumber,
|
userNumber,
|
||||||
)}!`,
|
)}!`,
|
||||||
), // TODO
|
),
|
||||||
imageUris: [
|
imageUris: [
|
||||||
{
|
{
|
||||||
uri,
|
uri,
|
||||||
width: WIDTH,
|
width: WIDTH,
|
||||||
height: HEIGHT,
|
height: HEIGHT,
|
||||||
|
altText: _(
|
||||||
|
msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
|
||||||
|
userNumber,
|
||||||
|
)}, ${displayName} ${handle}, joined on ${joinedDate}"`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}, 1e3)
|
}, 1e3)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [_, i18n, control, openComposer, uri, userNumber])
|
}, [
|
||||||
|
_,
|
||||||
|
i18n,
|
||||||
|
control,
|
||||||
|
openComposer,
|
||||||
|
uri,
|
||||||
|
userNumber,
|
||||||
|
displayName,
|
||||||
|
handle,
|
||||||
|
joinedDate,
|
||||||
|
])
|
||||||
const onNativeShare = React.useCallback(() => {
|
const onNativeShare = React.useCallback(() => {
|
||||||
if (uri) {
|
if (uri) {
|
||||||
control.close(() => {
|
control.close(() => {
|
||||||
@@ -490,11 +526,7 @@ export function TenMillionInner({
|
|||||||
a.leading_tight,
|
a.leading_tight,
|
||||||
{maxWidth: '60%'},
|
{maxWidth: '60%'},
|
||||||
]}>
|
]}>
|
||||||
{sanitizeDisplayName(
|
{displayName}
|
||||||
profile.displayName ||
|
|
||||||
sanitizeHandle(profile.handle),
|
|
||||||
moderation.ui('displayName'),
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
<View
|
<View
|
||||||
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
|
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
|
||||||
@@ -508,7 +540,7 @@ export function TenMillionInner({
|
|||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
lightTheme.atoms.text_contrast_medium,
|
lightTheme.atoms.text_contrast_medium,
|
||||||
]}>
|
]}>
|
||||||
{sanitizeHandle(profile.handle, '@')}
|
{handle}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{profile.createdAt && (
|
{profile.createdAt && (
|
||||||
@@ -524,14 +556,7 @@ export function TenMillionInner({
|
|||||||
a.text_right,
|
a.text_right,
|
||||||
lightTheme.atoms.text_contrast_low,
|
lightTheme.atoms.text_contrast_low,
|
||||||
]}>
|
]}>
|
||||||
<Trans>
|
<Trans>Joined on {joinedDate}</Trans>
|
||||||
Joined{' '}
|
|
||||||
{i18n.date(profile.createdAt, {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ msgstr "{0, plural, one {# segon} other {# segons}}"
|
|||||||
|
|
||||||
#: src/components/KnownFollowers.tsx:179
|
#: src/components/KnownFollowers.tsx:179
|
||||||
#~ msgid "{0, plural, one {and # other} other {and # others}}"
|
#~ msgid "{0, plural, one {and # other} other {and # others}}"
|
||||||
#~ msgstr "{0, plural, one {i # altre} other {i # altres}"
|
#~ msgstr "{0, plural, one {i # altre} other {i # altres}}"
|
||||||
|
|
||||||
#: src/components/ProfileHoverCard/index.web.tsx:398
|
#: src/components/ProfileHoverCard/index.web.tsx:398
|
||||||
#: src/screens/Profile/Header/Metrics.tsx:23
|
#: src/screens/Profile/Header/Metrics.tsx:23
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {ImageModel} from './image'
|
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
|
||||||
import {openPicker} from 'lib/media/picker'
|
|
||||||
import {getImageDim} from 'lib/media/manip'
|
import {getImageDim} from 'lib/media/manip'
|
||||||
|
import {openPicker} from 'lib/media/picker'
|
||||||
|
import {ImageInitOptions, ImageModel} from './image'
|
||||||
|
|
||||||
interface InitialImageUri {
|
interface InitialImageUri {
|
||||||
uri: string
|
uri: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
|
altText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GalleryModel {
|
export class GalleryModel {
|
||||||
images: ImageModel[] = []
|
images: ImageModel[] = []
|
||||||
|
|
||||||
constructor(uris?: {uri: string; width: number; height: number}[]) {
|
constructor(uris?: InitialImageUri[]) {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
|
|
||||||
if (uris) {
|
if (uris) {
|
||||||
@@ -33,7 +34,7 @@ export class GalleryModel {
|
|||||||
return this.images.some(image => image.altText.trim() === '')
|
return this.images.some(image => image.altText.trim() === '')
|
||||||
}
|
}
|
||||||
|
|
||||||
*add(image_: Omit<RNImage, 'size'>) {
|
*add(image_: ImageInitOptions) {
|
||||||
if (this.size >= 4) {
|
if (this.size >= 4) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -59,7 +60,6 @@ export class GalleryModel {
|
|||||||
path: uri,
|
path: uri,
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
mime: 'image/jpeg',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@@ -100,10 +100,10 @@ export class GalleryModel {
|
|||||||
async addFromUris(uris: InitialImageUri[]) {
|
async addFromUris(uris: InitialImageUri[]) {
|
||||||
for (const uriObj of uris) {
|
for (const uriObj of uris) {
|
||||||
this.add({
|
this.add({
|
||||||
mime: 'image/jpeg',
|
|
||||||
height: uriObj.height,
|
height: uriObj.height,
|
||||||
width: uriObj.width,
|
width: uriObj.width,
|
||||||
path: uriObj.uri,
|
path: uriObj.uri,
|
||||||
|
altText: uriObj.altText,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
|
||||||
import {POST_IMG_MAX} from 'lib/constants'
|
|
||||||
import * as ImageManipulator from 'expo-image-manipulator'
|
import * as ImageManipulator from 'expo-image-manipulator'
|
||||||
import {getDataUriSize} from 'lib/media/util'
|
|
||||||
import {openCropper} from 'lib/media/picker'
|
|
||||||
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
|
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
|
||||||
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {Position} from 'react-avatar-editor'
|
import {Position} from 'react-avatar-editor'
|
||||||
import {Dimensions} from 'lib/media/types'
|
|
||||||
import {isIOS} from 'platform/detection'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {POST_IMG_MAX} from 'lib/constants'
|
||||||
|
import {openCropper} from 'lib/media/picker'
|
||||||
|
import {Dimensions} from 'lib/media/types'
|
||||||
|
import {getDataUriSize} from 'lib/media/util'
|
||||||
|
import {isIOS} from 'platform/detection'
|
||||||
|
|
||||||
export interface ImageManipulationAttributes {
|
export interface ImageManipulationAttributes {
|
||||||
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
|
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
|
||||||
@@ -19,6 +20,13 @@ export interface ImageManipulationAttributes {
|
|||||||
flipVertical?: boolean
|
flipVertical?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageInitOptions {
|
||||||
|
path: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
altText?: string
|
||||||
|
}
|
||||||
|
|
||||||
const MAX_IMAGE_SIZE_IN_BYTES = 976560
|
const MAX_IMAGE_SIZE_IN_BYTES = 976560
|
||||||
|
|
||||||
export class ImageModel implements Omit<RNImage, 'size'> {
|
export class ImageModel implements Omit<RNImage, 'size'> {
|
||||||
@@ -41,12 +49,15 @@ export class ImageModel implements Omit<RNImage, 'size'> {
|
|||||||
}
|
}
|
||||||
prevAttributes: ImageManipulationAttributes = {}
|
prevAttributes: ImageManipulationAttributes = {}
|
||||||
|
|
||||||
constructor(image: Omit<RNImage, 'size'>) {
|
constructor(image: ImageInitOptions) {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
|
|
||||||
this.path = image.path
|
this.path = image.path
|
||||||
this.width = image.width
|
this.width = image.width
|
||||||
this.height = image.height
|
this.height = image.height
|
||||||
|
if (image.altText !== undefined) {
|
||||||
|
this.setAltText(image.altText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
|
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export interface ComposerOpts {
|
|||||||
mention?: string // handle of user to mention
|
mention?: string // handle of user to mention
|
||||||
openEmojiPicker?: (pos: DOMRect | undefined) => void
|
openEmojiPicker?: (pos: DOMRect | undefined) => void
|
||||||
text?: string
|
text?: string
|
||||||
imageUris?: {uri: string; width: number; height: number}[]
|
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateContext = ComposerOpts | undefined
|
type StateContext = ComposerOpts | undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user