Skip to content

Commit

Permalink
Merge pull request #3834 from thematters/feat/direct-image-upload
Browse files Browse the repository at this point in the history
Direct Image Upload
  • Loading branch information
robertu7 authored Oct 5, 2023
2 parents 1831de8 + 2d686b4 commit c81b856
Show file tree
Hide file tree
Showing 17 changed files with 367 additions and 141 deletions.
2 changes: 2 additions & 0 deletions .env.prod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
NEXT_PUBLIC_RUNTIME_ENV=production
NEXT_PUBLIC_SITE_DOMAIN=matters.town
NEXT_PUBLIC_SITE_BRAND_NAME=Matters.Town
NEXT_PUBLIC_SITE_DOMAIN_TLD=matters.town
NEXT_PUBLIC_SITE_DOMAIN_TLD_OLD=matters.news
NEXT_PUBLIC_EMBED_ASSET_DOMAIN=assets.matters.news
NEXT_PUBLIC_CF_IMAGE_URL=https://imagedelivery.net/kDRCweMmqLnTPNlbum-pYA/prod
NEXT_PUBLIC_NEXT_ASSET_DOMAIN=assets-next.mattersprotocol.io
Expand Down
9 changes: 9 additions & 0 deletions lang/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@
"9J0iCw": {
"defaultMessage": "Deleted user"
},
"9Vkz9W": {
"defaultMessage": "Maximum image area is limited to 100 megapixels (for example, 10,000×10,000 pixels)."
},
"9mfw9c": {
"defaultMessage": "Wallet address",
"description": "src/components/UserProfile/WalletLabel/WalletDialog/index.tsx"
Expand Down Expand Up @@ -838,6 +841,9 @@
"defaultMessage": "New likes",
"description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx"
},
"OyvGvT": {
"defaultMessage": "Images have a 5 megabyte (MB) size limit."
},
"PLBmDT": {
"defaultMessage": "Password",
"description": "src/views/Me/Settings/Settings/Password/index.tsx"
Expand Down Expand Up @@ -1094,6 +1100,9 @@
"defaultMessage": "commented in your circle broadcast",
"description": "src/components/Notice/CircleNotice/CircleNewBroadcastComments.tsx"
},
"Y7N/Jg": {
"defaultMessage": "Maximum image dimension is 12,000 pixels."
},
"YPMn9n": {
"defaultMessage": "Please log in.",
"description": "UNAUTHENTICATED"
Expand Down
9 changes: 9 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@
"9J0iCw": {
"defaultMessage": "Deleted user"
},
"9Vkz9W": {
"defaultMessage": "Maximum image area is limited to 100 megapixels (for example, 10,000×10,000 pixels)."
},
"9mfw9c": {
"defaultMessage": "Wallet address",
"description": "src/components/UserProfile/WalletLabel/WalletDialog/index.tsx"
Expand Down Expand Up @@ -838,6 +841,9 @@
"defaultMessage": "New likes",
"description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx"
},
"OyvGvT": {
"defaultMessage": "Images have a 5 megabyte (MB) size limit."
},
"PLBmDT": {
"defaultMessage": "Password",
"description": "src/views/Me/Settings/Settings/Password/index.tsx"
Expand Down Expand Up @@ -1094,6 +1100,9 @@
"defaultMessage": "commented in your circle broadcast",
"description": "src/components/Notice/CircleNotice/CircleNewBroadcastComments.tsx"
},
"Y7N/Jg": {
"defaultMessage": "Maximum image dimension is 12,000 pixels."
},
"YPMn9n": {
"defaultMessage": "Please log in.",
"description": "UNAUTHENTICATED"
Expand Down
9 changes: 9 additions & 0 deletions lang/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@
"9J0iCw": {
"defaultMessage": "已注销用户"
},
"9Vkz9W": {
"defaultMessage": "图片面积不得超过 1 亿像素(如 10,000 x 10,000 像素)"
},
"9mfw9c": {
"defaultMessage": "钱包地址",
"description": "src/components/UserProfile/WalletLabel/WalletDialog/index.tsx"
Expand Down Expand Up @@ -838,6 +841,9 @@
"defaultMessage": "作品被赞赏",
"description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx"
},
"OyvGvT": {
"defaultMessage": "图片大小不得超过 5MB"
},
"PLBmDT": {
"defaultMessage": "登录密码",
"description": "src/views/Me/Settings/Settings/Password/index.tsx"
Expand Down Expand Up @@ -1094,6 +1100,9 @@
"defaultMessage": "在你的围炉中留言",
"description": "src/components/Notice/CircleNotice/CircleNewBroadcastComments.tsx"
},
"Y7N/Jg": {
"defaultMessage": "图片边长不得超过 12,000 像素"
},
"YPMn9n": {
"defaultMessage": "请先登入再进行操作",
"description": "UNAUTHENTICATED"
Expand Down
9 changes: 9 additions & 0 deletions lang/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@
"9J0iCw": {
"defaultMessage": "已註銷用戶"
},
"9Vkz9W": {
"defaultMessage": "圖片面積不得超過 1 億像素(如 10,000x10,000 像素)"
},
"9mfw9c": {
"defaultMessage": "錢包地址",
"description": "src/components/UserProfile/WalletLabel/WalletDialog/index.tsx"
Expand Down Expand Up @@ -838,6 +841,9 @@
"defaultMessage": "作品被讚賞",
"description": "src/views/Me/Settings/Notifications/GeneralSettings/index.tsx"
},
"OyvGvT": {
"defaultMessage": "圖片大小不得超過 5MB"
},
"PLBmDT": {
"defaultMessage": "登入密碼",
"description": "src/views/Me/Settings/Settings/Password/index.tsx"
Expand Down Expand Up @@ -1094,6 +1100,9 @@
"defaultMessage": "在你的圍爐中留言",
"description": "src/components/Notice/CircleNotice/CircleNewBroadcastComments.tsx"
},
"Y7N/Jg": {
"defaultMessage": "圖片邊長不得超過 12,000 像素"
},
"YPMn9n": {
"defaultMessage": "請先登入再進行操作",
"description": "UNAUTHENTICATED"
Expand Down
11 changes: 11 additions & 0 deletions src/common/enums/csp.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const isProd = process.env.NEXT_PUBLIC_RUNTIME_ENV === 'production'
const site_domain_tld =
process.env.NEXT_PUBLIC_SITE_DOMAIN_TLD || 'matters.town',
site_domain_tld_old =
Expand Down Expand Up @@ -70,6 +71,13 @@ const IMG_SRC = [
site_domain_tld_old
),

// For image validation
// @see {@url src/common/utils/form/image.tsx}
'blob:',
`*.${site_domain_tld}`,
isProd ? undefined : 'localhost',
isProd ? undefined : '127.0.0.1',

// Alchemy NFT CDN
'nft-cdn.alchemy.com',

Expand Down Expand Up @@ -105,6 +113,9 @@ const CONNECT_SRC = [
site_domain_tld_old
),

// Cloudflare Image Upload
'upload.imagedelivery.net',

// Sentry
'*.ingest.sentry.io',

Expand Down
5 changes: 4 additions & 1 deletion src/common/enums/file.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const UPLOAD_IMAGE_SIZE_LIMIT: number = 5 * 1024 * 1024
export const UPLOAD_IMAGE_SIZE_LIMIT: number = 5 * 1024 * 1024 // 5MB
export const UPLOAD_IMAGE_DIMENSION_LIMIT: number = 12e3 // 12,000 pixels
export const UPLOAD_IMAGE_AREA_LIMIT: number = 10e3 * 10e3 // 100 megapixels
export const UPLOAD_IMAGE_METADATA_SIZE_LIMIT: number = 1024 // 1024 bytes

export const UPLOAD_AUDIO_SIZE_LIMIT: number = 100 * 1024 * 1024

Expand Down
63 changes: 63 additions & 0 deletions src/common/utils/form/image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FormattedMessage } from 'react-intl'

import {
UPLOAD_IMAGE_AREA_LIMIT,
UPLOAD_IMAGE_DIMENSION_LIMIT,
UPLOAD_IMAGE_SIZE_LIMIT,
} from '~/common/enums'
import { toast } from '~/components'

export const validateImage = (image: File) =>
new Promise<boolean>((resolve, reject) => {
// size limits
const isExceedSizeLimit = image.size > UPLOAD_IMAGE_SIZE_LIMIT
if (isExceedSizeLimit) {
toast.error({
message: (
<FormattedMessage defaultMessage="Images have a 5 megabyte (MB) size limit." />
),
})
return resolve(false)
}

// dimension limits
try {
const $img = new Image()

$img.onload = () => {
const { width, height } = $img
window.URL.revokeObjectURL($img.src)

const isExceedDimensionLimit =
width > UPLOAD_IMAGE_DIMENSION_LIMIT ||
height > UPLOAD_IMAGE_DIMENSION_LIMIT
const isExceedAreaLimit = width * height > UPLOAD_IMAGE_AREA_LIMIT

if (isExceedDimensionLimit) {
toast.error({
message: (
<FormattedMessage defaultMessage="Maximum image dimension is 12,000 pixels." />
),
})
return resolve(false)
}

if (isExceedAreaLimit) {
toast.error({
message: (
<FormattedMessage defaultMessage="Maximum image area is limited to 100 megapixels (for example, 10,000×10,000 pixels)." />
),
})
return resolve(false)
}

return resolve(true)
}

$img.src = window.URL.createObjectURL(image)
} catch (exception) {
return reject(exception)
}

return resolve(true)
})
1 change: 1 addition & 0 deletions src/common/utils/form/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './error'
export * from './image'
export * from './validate'
28 changes: 7 additions & 21 deletions src/components/Editor/Article/FloatingMenu/UploadImageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@ import classNames from 'classnames'
import { useContext, useState } from 'react'

import { ReactComponent as IconEditorMenuImage } from '@/public/static/icons/32px/editor-menu-image.svg'
import {
ACCEPTED_UPLOAD_IMAGE_TYPES,
ASSET_TYPE,
UPLOAD_IMAGE_SIZE_LIMIT,
} from '~/common/enums'
import { translate } from '~/common/utils'
import { ACCEPTED_UPLOAD_IMAGE_TYPES, ASSET_TYPE } from '~/common/enums'
import { translate, validateImage } from '~/common/utils'
import {
IconSpinner16,
LanguageContext,
Expand Down Expand Up @@ -50,22 +46,12 @@ const UploadImageButton: React.FC<UploadImageButtonProps> = ({
}

const files = event.target.files
event.target.value = ''

const hasExceedLimit = Array.from(files).some(
(file) => file.size > UPLOAD_IMAGE_SIZE_LIMIT
)
if (hasExceedLimit) {
toast.error({
message: (
<Translate
zh_hant="上傳檔案超過 5 MB"
zh_hans="上传文件超过 5 MB"
en="upload file size exceeds 5 MB"
/>
),
})

event.target.value = ''
const hasInvalidImage = await Promise.all(
Array.from(files).map(validateImage)
).then((results) => results.some((result) => !result))
if (hasInvalidImage) {
return
}

Expand Down
Loading

1 comment on commit c81b856

@vercel
Copy link

@vercel vercel bot commented on c81b856 Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.