From a95025c0dfa42e450073ddfa2d85745fefe0785e Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:14:24 +0800 Subject: [PATCH 01/35] fix(Comment): delete comment cache error --- .../Comment/DropdownActions/DeleteComment/Dialog.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Comment/DropdownActions/DeleteComment/Dialog.tsx b/src/components/Comment/DropdownActions/DeleteComment/Dialog.tsx index 1747222176..9bc5736948 100644 --- a/src/components/Comment/DropdownActions/DeleteComment/Dialog.tsx +++ b/src/components/Comment/DropdownActions/DeleteComment/Dialog.tsx @@ -21,6 +21,7 @@ const DELETE_COMMENT = gql` id state node { + id ... on Moment { id commentCount @@ -63,7 +64,10 @@ const DeleteCommentDialog = ({ commentCount: node.commentCount - 1, __typename: 'Moment', } - : {}, + : { + id: node?.id || '', + __typename: 'Article', + }, __typename: 'Comment', }, }, From a88acab06117107fc24b4f8c2f59785bfc5903fc Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:28:03 +0800 Subject: [PATCH 02/35] fix(notice): fix incorrect word counting of `truncateNoticeTitle` --- src/common/utils/text/notice.test.ts | 212 +++++------------- src/common/utils/text/notice.ts | 196 ++++------------ .../Notice/NoticeCollectionTitle.tsx | 5 +- src/components/Notice/NoticeMomentTitle.tsx | 8 +- 4 files changed, 101 insertions(+), 320 deletions(-) diff --git a/src/common/utils/text/notice.test.ts b/src/common/utils/text/notice.test.ts index c51d491a6b..a00efd1fb7 100644 --- a/src/common/utils/text/notice.test.ts +++ b/src/common/utils/text/notice.test.ts @@ -1,164 +1,74 @@ import { describe, expect, it } from 'vitest' -import { UserLanguage } from '~/gql/graphql' - import { truncateNoticeTitle } from './notice' -describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { - describe('for Chinese', () => { - it('should truncate the title to the specified maximum number of words', () => { - const title = '这是一个标题这是一个标题这是一个标题' - const maxLength = 3 - const expected = '这是一...' - const result = truncateNoticeTitle(title, { - locale: UserLanguage.ZhHans, - maxLength, - }) - // Assert - expect(result).toEqual(expected) - }) - - it('should return the title as is if it has fewer words than the maximum', () => { - const title = '这是一个标题' - const maxLength = 7 - const result = truncateNoticeTitle(title, { - locale: UserLanguage.ZhHans, - maxLength, - }) - // Assert - expect(result).toEqual(title) - }) - - it('should return the title for the default length of 10 words', () => { - const title = '这是一个标题这是一个标题这是一个标题' - const expected = '这是一个标题这是一个...' - const result = truncateNoticeTitle(title, { locale: UserLanguage.ZhHans }) - // Assert - expect(result).toEqual(expected) - }) - }) - - describe('for English', () => { - it('should return the title as is if it has fewer words than the maximum', () => { - const title = 'The birds are chirping and the sun is shining' - const maxLength = 50 - const result = truncateNoticeTitle(title, { - locale: UserLanguage.En, - maxLength, - }) - // Assert - expect(result).toEqual(title) - }) +const CHINESE_ONLY = '这是一个标题这是一个标题这是一个标题这是一个标题这是一个' +const CHINESE_WITH_NUMBERS_AND_PUNCTUATION = + '看起來 10 拍,快樂喜歡如其實也是我於有我的部分' +const ENGLISH_ONLY = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +const ENGLISH_WITH_NUMBERS_AND_PUNCTUATION = + 'Lorem ipsum 10 dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +const MIXED = + '看起來 10 拍,consectetur Lorem ipsum dolor sit amet, adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +const MENTIONS = + '看起來 10 拍 @用戶 @user Lorem ipsum dolor,快樂喜歡如其實也是我於有我的部分' - it('should truncate the title to the specified maximum number of words', () => { - const title = 'The birds are chirping and the sun is shining' - const maxLength = 27 - const expected = 'The birds are chirping and...' - const result = truncateNoticeTitle(title, { - locale: UserLanguage.En, - maxLength, - }) - // Assert - expect(result).toEqual(expected) - }) +describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { + it('should return the title for the default length of 10 words', () => { + expect(truncateNoticeTitle(CHINESE_ONLY)).toEqual('这是一个标题这是一个...') + expect(truncateNoticeTitle(CHINESE_WITH_NUMBERS_AND_PUNCTUATION)).toEqual( + '看起來 10 拍,快樂喜歡如...' + ) + expect(truncateNoticeTitle(ENGLISH_ONLY)).toEqual( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do...' + ) + expect(truncateNoticeTitle(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION)).toEqual( + 'Lorem ipsum 10 dolor sit amet, consectetur adipiscing elit, sed...' + ) + expect(truncateNoticeTitle(MIXED)).toEqual( + '看起來 10 拍,consectetur Lorem ipsum dolor sit...' + ) + expect(truncateNoticeTitle(MENTIONS)).toEqual( + '看起來 10 拍 @用戶 @user Lorem ipsum dolor...' + ) }) - describe('for English with tagged users', () => { - it('should truncate characters to under 10 words for english', () => { - expect( - truncateNoticeTitle('This is a very long sentence.', { - includeAtSign: true, - }) - ).toBe('This is a...') - expect( - truncateNoticeTitle('Hello, world.', { includeAtSign: true }) - ).toBe('Hello,...') - }) + it('should truncate the title to the specified maximum number of words', () => { + const maxLength = 6 - it('should truncate if over 10 characters with tagged users and remaining length is 0 while having english characters', () => { - expect( - truncateNoticeTitle('This is a craaaazy article here! @user1 @user2', { - includeAtSign: true, - }) - ).toBe('This is a...@user1 @user2') - }) + expect(truncateNoticeTitle(CHINESE_ONLY, maxLength)).toEqual( + '这是一个标题...' + ) + expect( + truncateNoticeTitle(CHINESE_WITH_NUMBERS_AND_PUNCTUATION, maxLength) + ).toEqual('看起來 10 拍,快...') + expect(truncateNoticeTitle(ENGLISH_ONLY, maxLength)).toEqual( + 'Lorem ipsum dolor sit amet, consectetur...' + ) + expect( + truncateNoticeTitle(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION, maxLength) + ).toEqual('Lorem ipsum 10 dolor sit amet...') + expect(truncateNoticeTitle(MIXED, maxLength)).toEqual( + '看起來 10 拍,consectetur...' + ) + expect(truncateNoticeTitle(MENTIONS, maxLength)).toEqual( + '看起來 10 拍 @用戶...' + ) }) - describe('for Chinese with tagged users', () => { - it('should not truncate if under 10 characters', () => { - expect( - truncateNoticeTitle('這篇文章真的很厲害!', { - locale: UserLanguage.ZhHant, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('這篇文章真的很厲害!') - expect( - truncateNoticeTitle('很厲害!', { - locale: UserLanguage.ZhHant, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('很厲害!') - }) - - it('should truncate if over 10 characters', () => { - expect( - truncateNoticeTitle('這篇文章真的很厲害,大家應該都來看一下!', { - locale: UserLanguage.ZhHant, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('這篇文章真的很厲害,...') - }) - - it('should truncate when the title is over 10 characters and the mentions are at the end', () => { - expect( - truncateNoticeTitle( - '這篇文章真的很厲害,大家應該都來看一下 @user1 @user2', - { locale: UserLanguage.ZhHant, maxLength: 10, includeAtSign: true } - ) - ).toBe('這篇文章真的很厲害,...@user1 @user2') - expect( - truncateNoticeTitle( - '這篇文章真的很厲害,大家應該都來看一下! @user1 @user2', - { locale: UserLanguage.ZhHant, maxLength: 10, includeAtSign: true } - ) - ).toBe('這篇文章真的很厲害,...@user1 @user2') - expect( - truncateNoticeTitle('這是一個時刻!!!!!!!@jj', { - locale: UserLanguage.ZhHant, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('這是一個時刻!!!!...@jj') - }) - - it('should truncate if over 10 characters with tagged users in the middle or the beginning', () => { - expect( - truncateNoticeTitle('我和 @zhangsan 在台北一起去吃吃吃!', { - locale: UserLanguage.ZhHans, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('我和 @zhangsan 在台北一起去...') - expect( - truncateNoticeTitle('@zhangsan 和我在台北一起去吃吃吃!', { - locale: UserLanguage.ZhHans, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('@zhangsan 和我在台北一起去吃...') - }) + it('should return the title as is if it has fewer words than the maximum', () => { + const maxLength = 100 - it('should truncate characters to when the mention is a bit spread out', () => { - expect( - truncateNoticeTitle('我和 @zhangsan 還有 @yp 在台北一起去吃吃吃!', { - locale: UserLanguage.ZhHans, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('我和 @zhangsan 還有 @yp 在台...') - }) + expect(truncateNoticeTitle(CHINESE_ONLY, maxLength)).toEqual(CHINESE_ONLY) + expect( + truncateNoticeTitle(CHINESE_WITH_NUMBERS_AND_PUNCTUATION, maxLength) + ).toEqual(CHINESE_WITH_NUMBERS_AND_PUNCTUATION) + expect(truncateNoticeTitle(ENGLISH_ONLY, maxLength)).toEqual(ENGLISH_ONLY) + expect( + truncateNoticeTitle(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION, maxLength) + ).toEqual(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION) + expect(truncateNoticeTitle(MIXED, maxLength)).toEqual(MIXED) + expect(truncateNoticeTitle(MENTIONS, maxLength)).toEqual(MENTIONS) }) }) diff --git a/src/common/utils/text/notice.ts b/src/common/utils/text/notice.ts index db101e79c2..58db112f75 100644 --- a/src/common/utils/text/notice.ts +++ b/src/common/utils/text/notice.ts @@ -1,182 +1,62 @@ -import { UserLanguage } from '~/gql/graphql' - -type TruncateNoticeTitleOptions = { - locale?: UserLanguage - maxLength?: number - includeAtSign?: boolean -} - /** * Truncates a title to a specified maximum length, while preserving tagged users. * * @param title - The title to truncate. * @param maxLength - The maximum length of the truncated title. - * @param locale - The locale to determine the truncation rules. Defaults to 'en'. + * - Each CJK character is counted as 1 unit. + * - Each latin word is counted as 1 unit. + * - Each tagged user is counted as 1 unit. + * - Ignoer spaces and punctuations. + * * @returns The truncated title with preserved tagged users. */ -export const truncateNoticeTitle = ( - title: string, - options: TruncateNoticeTitleOptions = {} -) => { - const DEFAULTS = { - locale: UserLanguage.En, - includeAtSign: false, - maxLength: 10, - } - let localOptions = { ...DEFAULTS, ...options } +const REGEXP_CJK = + '[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]' - if (/^zh/.test(localOptions.locale)) { - return localOptions.includeAtSign - ? truncateTitleForChineseWithAtSign(title, localOptions) - : truncateTitleForChinese(title, localOptions) - } else { - return localOptions.includeAtSign - ? truncateTitleForEnglishWithAtSign(title, localOptions) - : truncateTitleForEnglish(title, localOptions) +function countUnits(word: string) { + // Latin word + if (/^@\w+/.test(word) || new RegExp(`^@${REGEXP_CJK}+`).test(word)) { + return 1 } -} - -/** - * Truncates a title to a specified maximum length for Chinese (Simplified or traditional) text. - * - * @param text - The title to truncate. - * @param maxWords - The maximum number of words in the truncated title. Defaults to 10. - * @returns The truncated title. - */ -export function truncateTitleForChinese( - text: string, - { - maxLength, - }: { maxLength: NonNullable } -): string { - const chineseRegex = /[\u4e00-\u9fa5]/g - const chineseWords = text.match(chineseRegex) - if (chineseWords && chineseWords.length > maxLength) { - return chineseWords.slice(0, maxLength).join('') + '...' + // CJK + else if (new RegExp(REGEXP_CJK, 'g').test(word)) { + return 1 } - return text -} - -/** - * Truncates a title to a specified maximum length for English text. - * - * @param text - The title to truncate. - * @param maxLength - The maximum length of the truncated title. Defaults to 50. - * @returns The truncated title. - */ -export function truncateTitleForEnglish( - text: string, - { - maxLength, - }: { maxLength: NonNullable } -): string { - if (text.length > maxLength) { - const words = text.split(' ') - let truncatedText = '' - let count = 0 - for (const word of words) { - if (count + word.length <= maxLength) { - truncatedText += word + ' ' - count += word.length + 1 - } else { - break - } - } - return truncatedText.trim() + '...' + // Latin + else if (/^\w+/.test(word)) { + return 1 } - return text -} -/** - * Truncates a title in English to a specified maximum length, while preserving tagged users. - * - * @param title - The title to truncate. - * @param maxLength - The maximum length of the truncated title. - * @returns The truncated title with preserved tagged users. - */ -const truncateTitleForEnglishWithAtSign = ( - title: string, - { - maxLength, - }: { maxLength: NonNullable } -) => { - const words = title.split(/\s+/) - let hasTag = words.some((word) => word.startsWith('@')) - let truncated = '' - let count = 0 + // Ignore spaces and punctuations + return 0 +} - for (const word of words) { - if (word.startsWith('@')) { - truncated += `${word} ` - continue - } - if (count + word.length + 1 > maxLength) { - break - } - truncated += `${word} ` - count += word.length + 1 - } +function trimSpacesAndPunctuations(str: string) { + return str.replace(/^[\s\p{P}]+|[\s\p{P}]+$/gu, '') +} - let base = truncated.trim() + (title.length > count ? '...' : '') - if (hasTag && !base.includes('@')) { - for (const word of words) { - if (word.startsWith('@')) { - base += `${word} ` - } - } - } +export const truncateNoticeTitle = (title: string, maxLength: number = 10) => { + const components = + title.match( + new RegExp(`(@\\w+|@${REGEXP_CJK}+|\\w+|${REGEXP_CJK}|[^\w\s])`, 'g') + ) || [] - return base.trim() -} + let truncatedTitle = '' + let currentLength = 0 -/** - * Truncates a title in CJK (Chinese, Japanese, Korean) to a specified maximum length, while preserving tagged users. - * - * @param title - The title to truncate. - * @param maxLength - The maximum length of the truncated title. - * @returns The truncated title with preserved tagged users. - */ -const truncateTitleForChineseWithAtSign = ( - title: string, - { - maxLength, - }: { maxLength: NonNullable } -) => { - const pattern = /(@\w+|[^\x00-\x7F]|\s)/gu - const phrases = title.match(pattern)?.filter((s) => s !== ' ') || [] - let hasTag = phrases.some((p) => p.startsWith('@')) - let count = 0 - let truncated = '' + for (const [index, component] of components.entries()) { + const componentUnits = countUnits(component) - for (const [idx, p] of phrases.entries()) { - if (p.startsWith('@')) { - if (idx + 1 == phrases.length) { - truncated += ` ${p}` - count += 1 - } else if (idx === 0) { - truncated += `${p} ` - count += 1 - } else { - truncated += ` ${p} ` - count += 2 + if (currentLength + componentUnits > maxLength) { + if (index < components.length - 1) { + truncatedTitle = trimSpacesAndPunctuations(truncatedTitle) + '...' } - continue - } - if (count + 1 > maxLength) { break } - truncated += p - count++ - } - let base = truncated.trim() + (title.length > count ? '...' : '') - if (hasTag && !base.includes('@')) { - for (const p of phrases) { - if (p.startsWith('@')) { - base += `${p} ` - } - } + truncatedTitle += component + currentLength += componentUnits } - return base.trim() + return truncatedTitle } diff --git a/src/components/Notice/NoticeCollectionTitle.tsx b/src/components/Notice/NoticeCollectionTitle.tsx index 05534f2777..b06404b318 100644 --- a/src/components/Notice/NoticeCollectionTitle.tsx +++ b/src/components/Notice/NoticeCollectionTitle.tsx @@ -1,13 +1,11 @@ import gql from 'graphql-tag' import Link from 'next/link' -import { useContext } from 'react' import { TEST_ID } from '~/common/enums' import { toPath } from '~/common/utils' import { truncateNoticeTitle } from '~/common/utils/text/notice' import { CollectionNoticeFragment } from '~/gql/graphql' -import { LanguageContext } from '../Context' import styles from './styles.module.css' const NoticeCollectionTitle = ({ @@ -16,7 +14,6 @@ const NoticeCollectionTitle = ({ notice: CollectionNoticeFragment | null }) => { const userName = notice?.collection?.author.userName - const { lang } = useContext(LanguageContext) if (!notice || !userName) { return null @@ -34,7 +31,7 @@ const NoticeCollectionTitle = ({ className={styles.noticeArticleTitle} data-test-id={TEST_ID.NOTICE_COLLECTION_TITLE} > - {truncateNoticeTitle(notice.collection.title, { locale: lang })} + {truncateNoticeTitle(notice.collection.title)} ) diff --git a/src/components/Notice/NoticeMomentTitle.tsx b/src/components/Notice/NoticeMomentTitle.tsx index 99dd343478..928511d383 100644 --- a/src/components/Notice/NoticeMomentTitle.tsx +++ b/src/components/Notice/NoticeMomentTitle.tsx @@ -1,11 +1,9 @@ import gql from 'graphql-tag' import Link from 'next/link' -import { useContext } from 'react' import { useIntl } from 'react-intl' import { TEST_ID } from '~/common/enums' import { stripHtml, toPath, truncateNoticeTitle } from '~/common/utils' -import { LanguageContext } from '~/components' import { NoticeMomentTitleFragment } from '~/gql/graphql' import styles from './styles.module.css' @@ -15,7 +13,6 @@ const NoticeMomentTitle = ({ }: { moment: NoticeMomentTitleFragment }) => { - const { lang } = useContext(LanguageContext) const intl = useIntl() const path = toPath({ @@ -23,10 +20,7 @@ const NoticeMomentTitle = ({ moment, }) - const title = truncateNoticeTitle(stripHtml(moment.content || ''), { - maxLength: 10, - locale: lang, - }) + const title = truncateNoticeTitle(stripHtml(moment.content || '')) const images = moment.assets.length ? intl .formatMessage({ defaultMessage: `[image]`, id: 'W3tqQO' }) From 185c185ea8d949c8f85105ac9eeefabc993039d1 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:39:42 +0800 Subject: [PATCH 03/35] fix(notice): use explicit punctuations instead --- src/common/utils/form/validate.ts | 6 +++--- src/common/utils/text/notice.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/common/utils/form/validate.ts b/src/common/utils/form/validate.ts index 756092522b..7ac71cc36c 100644 --- a/src/common/utils/form/validate.ts +++ b/src/common/utils/form/validate.ts @@ -28,10 +28,10 @@ import { import { hasUpperCase, isValidPaymentPointer } from '../validator' -const PUNCTUATION_CHINESE = +export const PUNCTUATION_CHINESE = '\u3002\uff1f\uff01\uff0c\u3001\uff1b\uff1a\u201c\u201d\u2018\u2019\uff08\uff09\u300a\u300b\u3008\u3009\u3010\u3011\u300e\u300f\u300c\u300d\ufe43\ufe44\u3014\u3015\u2026\u2014\uff5e\ufe4f\uffe5' -const PUNCTUATION_ASCII = '\x00-\x2f\x3a-\x40\x5b-\x60\x7a-\x7f' -const REGEXP_ALL_PUNCTUATIONS = new RegExp( +export const PUNCTUATION_ASCII = '\x00-\x2f\x3a-\x40\x5b-\x60\x7a-\x7f' +export const REGEXP_ALL_PUNCTUATIONS = new RegExp( `^[${PUNCTUATION_CHINESE}${PUNCTUATION_ASCII}]*$` ) diff --git a/src/common/utils/text/notice.ts b/src/common/utils/text/notice.ts index 58db112f75..244589df57 100644 --- a/src/common/utils/text/notice.ts +++ b/src/common/utils/text/notice.ts @@ -1,3 +1,5 @@ +import { PUNCTUATION_ASCII, PUNCTUATION_CHINESE } from '../form' + /** * Truncates a title to a specified maximum length, while preserving tagged users. * @@ -32,7 +34,13 @@ function countUnits(word: string) { } function trimSpacesAndPunctuations(str: string) { - return str.replace(/^[\s\p{P}]+|[\s\p{P}]+$/gu, '') + return str.replace( + new RegExp( + `^[${PUNCTUATION_CHINESE}${PUNCTUATION_ASCII}]+|[${PUNCTUATION_CHINESE}${PUNCTUATION_ASCII}]+$`, + 'g' + ), + '' + ) } export const truncateNoticeTitle = (title: string, maxLength: number = 10) => { From 388e237b7e8d90558deaf52c7ee17c8ae6512ea9 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:28:03 +0800 Subject: [PATCH 04/35] fix(notice): fix incorrect word counting of `truncateNoticeTitle` --- src/common/utils/text/notice.test.ts | 212 +++++------------- src/common/utils/text/notice.ts | 196 ++++------------ .../Notice/NoticeCollectionTitle.tsx | 5 +- src/components/Notice/NoticeMomentTitle.tsx | 8 +- 4 files changed, 101 insertions(+), 320 deletions(-) diff --git a/src/common/utils/text/notice.test.ts b/src/common/utils/text/notice.test.ts index c51d491a6b..a00efd1fb7 100644 --- a/src/common/utils/text/notice.test.ts +++ b/src/common/utils/text/notice.test.ts @@ -1,164 +1,74 @@ import { describe, expect, it } from 'vitest' -import { UserLanguage } from '~/gql/graphql' - import { truncateNoticeTitle } from './notice' -describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { - describe('for Chinese', () => { - it('should truncate the title to the specified maximum number of words', () => { - const title = '这是一个标题这是一个标题这是一个标题' - const maxLength = 3 - const expected = '这是一...' - const result = truncateNoticeTitle(title, { - locale: UserLanguage.ZhHans, - maxLength, - }) - // Assert - expect(result).toEqual(expected) - }) - - it('should return the title as is if it has fewer words than the maximum', () => { - const title = '这是一个标题' - const maxLength = 7 - const result = truncateNoticeTitle(title, { - locale: UserLanguage.ZhHans, - maxLength, - }) - // Assert - expect(result).toEqual(title) - }) - - it('should return the title for the default length of 10 words', () => { - const title = '这是一个标题这是一个标题这是一个标题' - const expected = '这是一个标题这是一个...' - const result = truncateNoticeTitle(title, { locale: UserLanguage.ZhHans }) - // Assert - expect(result).toEqual(expected) - }) - }) - - describe('for English', () => { - it('should return the title as is if it has fewer words than the maximum', () => { - const title = 'The birds are chirping and the sun is shining' - const maxLength = 50 - const result = truncateNoticeTitle(title, { - locale: UserLanguage.En, - maxLength, - }) - // Assert - expect(result).toEqual(title) - }) +const CHINESE_ONLY = '这是一个标题这是一个标题这是一个标题这是一个标题这是一个' +const CHINESE_WITH_NUMBERS_AND_PUNCTUATION = + '看起來 10 拍,快樂喜歡如其實也是我於有我的部分' +const ENGLISH_ONLY = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +const ENGLISH_WITH_NUMBERS_AND_PUNCTUATION = + 'Lorem ipsum 10 dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +const MIXED = + '看起來 10 拍,consectetur Lorem ipsum dolor sit amet, adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +const MENTIONS = + '看起來 10 拍 @用戶 @user Lorem ipsum dolor,快樂喜歡如其實也是我於有我的部分' - it('should truncate the title to the specified maximum number of words', () => { - const title = 'The birds are chirping and the sun is shining' - const maxLength = 27 - const expected = 'The birds are chirping and...' - const result = truncateNoticeTitle(title, { - locale: UserLanguage.En, - maxLength, - }) - // Assert - expect(result).toEqual(expected) - }) +describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { + it('should return the title for the default length of 10 words', () => { + expect(truncateNoticeTitle(CHINESE_ONLY)).toEqual('这是一个标题这是一个...') + expect(truncateNoticeTitle(CHINESE_WITH_NUMBERS_AND_PUNCTUATION)).toEqual( + '看起來 10 拍,快樂喜歡如...' + ) + expect(truncateNoticeTitle(ENGLISH_ONLY)).toEqual( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do...' + ) + expect(truncateNoticeTitle(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION)).toEqual( + 'Lorem ipsum 10 dolor sit amet, consectetur adipiscing elit, sed...' + ) + expect(truncateNoticeTitle(MIXED)).toEqual( + '看起來 10 拍,consectetur Lorem ipsum dolor sit...' + ) + expect(truncateNoticeTitle(MENTIONS)).toEqual( + '看起來 10 拍 @用戶 @user Lorem ipsum dolor...' + ) }) - describe('for English with tagged users', () => { - it('should truncate characters to under 10 words for english', () => { - expect( - truncateNoticeTitle('This is a very long sentence.', { - includeAtSign: true, - }) - ).toBe('This is a...') - expect( - truncateNoticeTitle('Hello, world.', { includeAtSign: true }) - ).toBe('Hello,...') - }) + it('should truncate the title to the specified maximum number of words', () => { + const maxLength = 6 - it('should truncate if over 10 characters with tagged users and remaining length is 0 while having english characters', () => { - expect( - truncateNoticeTitle('This is a craaaazy article here! @user1 @user2', { - includeAtSign: true, - }) - ).toBe('This is a...@user1 @user2') - }) + expect(truncateNoticeTitle(CHINESE_ONLY, maxLength)).toEqual( + '这是一个标题...' + ) + expect( + truncateNoticeTitle(CHINESE_WITH_NUMBERS_AND_PUNCTUATION, maxLength) + ).toEqual('看起來 10 拍,快...') + expect(truncateNoticeTitle(ENGLISH_ONLY, maxLength)).toEqual( + 'Lorem ipsum dolor sit amet, consectetur...' + ) + expect( + truncateNoticeTitle(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION, maxLength) + ).toEqual('Lorem ipsum 10 dolor sit amet...') + expect(truncateNoticeTitle(MIXED, maxLength)).toEqual( + '看起來 10 拍,consectetur...' + ) + expect(truncateNoticeTitle(MENTIONS, maxLength)).toEqual( + '看起來 10 拍 @用戶...' + ) }) - describe('for Chinese with tagged users', () => { - it('should not truncate if under 10 characters', () => { - expect( - truncateNoticeTitle('這篇文章真的很厲害!', { - locale: UserLanguage.ZhHant, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('這篇文章真的很厲害!') - expect( - truncateNoticeTitle('很厲害!', { - locale: UserLanguage.ZhHant, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('很厲害!') - }) - - it('should truncate if over 10 characters', () => { - expect( - truncateNoticeTitle('這篇文章真的很厲害,大家應該都來看一下!', { - locale: UserLanguage.ZhHant, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('這篇文章真的很厲害,...') - }) - - it('should truncate when the title is over 10 characters and the mentions are at the end', () => { - expect( - truncateNoticeTitle( - '這篇文章真的很厲害,大家應該都來看一下 @user1 @user2', - { locale: UserLanguage.ZhHant, maxLength: 10, includeAtSign: true } - ) - ).toBe('這篇文章真的很厲害,...@user1 @user2') - expect( - truncateNoticeTitle( - '這篇文章真的很厲害,大家應該都來看一下! @user1 @user2', - { locale: UserLanguage.ZhHant, maxLength: 10, includeAtSign: true } - ) - ).toBe('這篇文章真的很厲害,...@user1 @user2') - expect( - truncateNoticeTitle('這是一個時刻!!!!!!!@jj', { - locale: UserLanguage.ZhHant, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('這是一個時刻!!!!...@jj') - }) - - it('should truncate if over 10 characters with tagged users in the middle or the beginning', () => { - expect( - truncateNoticeTitle('我和 @zhangsan 在台北一起去吃吃吃!', { - locale: UserLanguage.ZhHans, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('我和 @zhangsan 在台北一起去...') - expect( - truncateNoticeTitle('@zhangsan 和我在台北一起去吃吃吃!', { - locale: UserLanguage.ZhHans, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('@zhangsan 和我在台北一起去吃...') - }) + it('should return the title as is if it has fewer words than the maximum', () => { + const maxLength = 100 - it('should truncate characters to when the mention is a bit spread out', () => { - expect( - truncateNoticeTitle('我和 @zhangsan 還有 @yp 在台北一起去吃吃吃!', { - locale: UserLanguage.ZhHans, - maxLength: 10, - includeAtSign: true, - }) - ).toBe('我和 @zhangsan 還有 @yp 在台...') - }) + expect(truncateNoticeTitle(CHINESE_ONLY, maxLength)).toEqual(CHINESE_ONLY) + expect( + truncateNoticeTitle(CHINESE_WITH_NUMBERS_AND_PUNCTUATION, maxLength) + ).toEqual(CHINESE_WITH_NUMBERS_AND_PUNCTUATION) + expect(truncateNoticeTitle(ENGLISH_ONLY, maxLength)).toEqual(ENGLISH_ONLY) + expect( + truncateNoticeTitle(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION, maxLength) + ).toEqual(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION) + expect(truncateNoticeTitle(MIXED, maxLength)).toEqual(MIXED) + expect(truncateNoticeTitle(MENTIONS, maxLength)).toEqual(MENTIONS) }) }) diff --git a/src/common/utils/text/notice.ts b/src/common/utils/text/notice.ts index db101e79c2..58db112f75 100644 --- a/src/common/utils/text/notice.ts +++ b/src/common/utils/text/notice.ts @@ -1,182 +1,62 @@ -import { UserLanguage } from '~/gql/graphql' - -type TruncateNoticeTitleOptions = { - locale?: UserLanguage - maxLength?: number - includeAtSign?: boolean -} - /** * Truncates a title to a specified maximum length, while preserving tagged users. * * @param title - The title to truncate. * @param maxLength - The maximum length of the truncated title. - * @param locale - The locale to determine the truncation rules. Defaults to 'en'. + * - Each CJK character is counted as 1 unit. + * - Each latin word is counted as 1 unit. + * - Each tagged user is counted as 1 unit. + * - Ignoer spaces and punctuations. + * * @returns The truncated title with preserved tagged users. */ -export const truncateNoticeTitle = ( - title: string, - options: TruncateNoticeTitleOptions = {} -) => { - const DEFAULTS = { - locale: UserLanguage.En, - includeAtSign: false, - maxLength: 10, - } - let localOptions = { ...DEFAULTS, ...options } +const REGEXP_CJK = + '[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]' - if (/^zh/.test(localOptions.locale)) { - return localOptions.includeAtSign - ? truncateTitleForChineseWithAtSign(title, localOptions) - : truncateTitleForChinese(title, localOptions) - } else { - return localOptions.includeAtSign - ? truncateTitleForEnglishWithAtSign(title, localOptions) - : truncateTitleForEnglish(title, localOptions) +function countUnits(word: string) { + // Latin word + if (/^@\w+/.test(word) || new RegExp(`^@${REGEXP_CJK}+`).test(word)) { + return 1 } -} - -/** - * Truncates a title to a specified maximum length for Chinese (Simplified or traditional) text. - * - * @param text - The title to truncate. - * @param maxWords - The maximum number of words in the truncated title. Defaults to 10. - * @returns The truncated title. - */ -export function truncateTitleForChinese( - text: string, - { - maxLength, - }: { maxLength: NonNullable } -): string { - const chineseRegex = /[\u4e00-\u9fa5]/g - const chineseWords = text.match(chineseRegex) - if (chineseWords && chineseWords.length > maxLength) { - return chineseWords.slice(0, maxLength).join('') + '...' + // CJK + else if (new RegExp(REGEXP_CJK, 'g').test(word)) { + return 1 } - return text -} - -/** - * Truncates a title to a specified maximum length for English text. - * - * @param text - The title to truncate. - * @param maxLength - The maximum length of the truncated title. Defaults to 50. - * @returns The truncated title. - */ -export function truncateTitleForEnglish( - text: string, - { - maxLength, - }: { maxLength: NonNullable } -): string { - if (text.length > maxLength) { - const words = text.split(' ') - let truncatedText = '' - let count = 0 - for (const word of words) { - if (count + word.length <= maxLength) { - truncatedText += word + ' ' - count += word.length + 1 - } else { - break - } - } - return truncatedText.trim() + '...' + // Latin + else if (/^\w+/.test(word)) { + return 1 } - return text -} -/** - * Truncates a title in English to a specified maximum length, while preserving tagged users. - * - * @param title - The title to truncate. - * @param maxLength - The maximum length of the truncated title. - * @returns The truncated title with preserved tagged users. - */ -const truncateTitleForEnglishWithAtSign = ( - title: string, - { - maxLength, - }: { maxLength: NonNullable } -) => { - const words = title.split(/\s+/) - let hasTag = words.some((word) => word.startsWith('@')) - let truncated = '' - let count = 0 + // Ignore spaces and punctuations + return 0 +} - for (const word of words) { - if (word.startsWith('@')) { - truncated += `${word} ` - continue - } - if (count + word.length + 1 > maxLength) { - break - } - truncated += `${word} ` - count += word.length + 1 - } +function trimSpacesAndPunctuations(str: string) { + return str.replace(/^[\s\p{P}]+|[\s\p{P}]+$/gu, '') +} - let base = truncated.trim() + (title.length > count ? '...' : '') - if (hasTag && !base.includes('@')) { - for (const word of words) { - if (word.startsWith('@')) { - base += `${word} ` - } - } - } +export const truncateNoticeTitle = (title: string, maxLength: number = 10) => { + const components = + title.match( + new RegExp(`(@\\w+|@${REGEXP_CJK}+|\\w+|${REGEXP_CJK}|[^\w\s])`, 'g') + ) || [] - return base.trim() -} + let truncatedTitle = '' + let currentLength = 0 -/** - * Truncates a title in CJK (Chinese, Japanese, Korean) to a specified maximum length, while preserving tagged users. - * - * @param title - The title to truncate. - * @param maxLength - The maximum length of the truncated title. - * @returns The truncated title with preserved tagged users. - */ -const truncateTitleForChineseWithAtSign = ( - title: string, - { - maxLength, - }: { maxLength: NonNullable } -) => { - const pattern = /(@\w+|[^\x00-\x7F]|\s)/gu - const phrases = title.match(pattern)?.filter((s) => s !== ' ') || [] - let hasTag = phrases.some((p) => p.startsWith('@')) - let count = 0 - let truncated = '' + for (const [index, component] of components.entries()) { + const componentUnits = countUnits(component) - for (const [idx, p] of phrases.entries()) { - if (p.startsWith('@')) { - if (idx + 1 == phrases.length) { - truncated += ` ${p}` - count += 1 - } else if (idx === 0) { - truncated += `${p} ` - count += 1 - } else { - truncated += ` ${p} ` - count += 2 + if (currentLength + componentUnits > maxLength) { + if (index < components.length - 1) { + truncatedTitle = trimSpacesAndPunctuations(truncatedTitle) + '...' } - continue - } - if (count + 1 > maxLength) { break } - truncated += p - count++ - } - let base = truncated.trim() + (title.length > count ? '...' : '') - if (hasTag && !base.includes('@')) { - for (const p of phrases) { - if (p.startsWith('@')) { - base += `${p} ` - } - } + truncatedTitle += component + currentLength += componentUnits } - return base.trim() + return truncatedTitle } diff --git a/src/components/Notice/NoticeCollectionTitle.tsx b/src/components/Notice/NoticeCollectionTitle.tsx index 05534f2777..b06404b318 100644 --- a/src/components/Notice/NoticeCollectionTitle.tsx +++ b/src/components/Notice/NoticeCollectionTitle.tsx @@ -1,13 +1,11 @@ import gql from 'graphql-tag' import Link from 'next/link' -import { useContext } from 'react' import { TEST_ID } from '~/common/enums' import { toPath } from '~/common/utils' import { truncateNoticeTitle } from '~/common/utils/text/notice' import { CollectionNoticeFragment } from '~/gql/graphql' -import { LanguageContext } from '../Context' import styles from './styles.module.css' const NoticeCollectionTitle = ({ @@ -16,7 +14,6 @@ const NoticeCollectionTitle = ({ notice: CollectionNoticeFragment | null }) => { const userName = notice?.collection?.author.userName - const { lang } = useContext(LanguageContext) if (!notice || !userName) { return null @@ -34,7 +31,7 @@ const NoticeCollectionTitle = ({ className={styles.noticeArticleTitle} data-test-id={TEST_ID.NOTICE_COLLECTION_TITLE} > - {truncateNoticeTitle(notice.collection.title, { locale: lang })} + {truncateNoticeTitle(notice.collection.title)} ) diff --git a/src/components/Notice/NoticeMomentTitle.tsx b/src/components/Notice/NoticeMomentTitle.tsx index 99dd343478..928511d383 100644 --- a/src/components/Notice/NoticeMomentTitle.tsx +++ b/src/components/Notice/NoticeMomentTitle.tsx @@ -1,11 +1,9 @@ import gql from 'graphql-tag' import Link from 'next/link' -import { useContext } from 'react' import { useIntl } from 'react-intl' import { TEST_ID } from '~/common/enums' import { stripHtml, toPath, truncateNoticeTitle } from '~/common/utils' -import { LanguageContext } from '~/components' import { NoticeMomentTitleFragment } from '~/gql/graphql' import styles from './styles.module.css' @@ -15,7 +13,6 @@ const NoticeMomentTitle = ({ }: { moment: NoticeMomentTitleFragment }) => { - const { lang } = useContext(LanguageContext) const intl = useIntl() const path = toPath({ @@ -23,10 +20,7 @@ const NoticeMomentTitle = ({ moment, }) - const title = truncateNoticeTitle(stripHtml(moment.content || ''), { - maxLength: 10, - locale: lang, - }) + const title = truncateNoticeTitle(stripHtml(moment.content || '')) const images = moment.assets.length ? intl .formatMessage({ defaultMessage: `[image]`, id: 'W3tqQO' }) From df7831693acae00e63320ab2cfe966bb175d7074 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:39:42 +0800 Subject: [PATCH 05/35] fix(notice): use explicit punctuations instead --- src/common/utils/form/validate.ts | 6 +++--- src/common/utils/text/notice.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/common/utils/form/validate.ts b/src/common/utils/form/validate.ts index 756092522b..7ac71cc36c 100644 --- a/src/common/utils/form/validate.ts +++ b/src/common/utils/form/validate.ts @@ -28,10 +28,10 @@ import { import { hasUpperCase, isValidPaymentPointer } from '../validator' -const PUNCTUATION_CHINESE = +export const PUNCTUATION_CHINESE = '\u3002\uff1f\uff01\uff0c\u3001\uff1b\uff1a\u201c\u201d\u2018\u2019\uff08\uff09\u300a\u300b\u3008\u3009\u3010\u3011\u300e\u300f\u300c\u300d\ufe43\ufe44\u3014\u3015\u2026\u2014\uff5e\ufe4f\uffe5' -const PUNCTUATION_ASCII = '\x00-\x2f\x3a-\x40\x5b-\x60\x7a-\x7f' -const REGEXP_ALL_PUNCTUATIONS = new RegExp( +export const PUNCTUATION_ASCII = '\x00-\x2f\x3a-\x40\x5b-\x60\x7a-\x7f' +export const REGEXP_ALL_PUNCTUATIONS = new RegExp( `^[${PUNCTUATION_CHINESE}${PUNCTUATION_ASCII}]*$` ) diff --git a/src/common/utils/text/notice.ts b/src/common/utils/text/notice.ts index 58db112f75..244589df57 100644 --- a/src/common/utils/text/notice.ts +++ b/src/common/utils/text/notice.ts @@ -1,3 +1,5 @@ +import { PUNCTUATION_ASCII, PUNCTUATION_CHINESE } from '../form' + /** * Truncates a title to a specified maximum length, while preserving tagged users. * @@ -32,7 +34,13 @@ function countUnits(word: string) { } function trimSpacesAndPunctuations(str: string) { - return str.replace(/^[\s\p{P}]+|[\s\p{P}]+$/gu, '') + return str.replace( + new RegExp( + `^[${PUNCTUATION_CHINESE}${PUNCTUATION_ASCII}]+|[${PUNCTUATION_CHINESE}${PUNCTUATION_ASCII}]+$`, + 'g' + ), + '' + ) } export const truncateNoticeTitle = (title: string, maxLength: number = 10) => { From ad8b6cbdc677cc98f640938ebe0bf0f27a637b5c Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:51:25 +0800 Subject: [PATCH 06/35] fix(ArticleDetail): update assets --- src/views/ArticleDetail/Edit/index.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/views/ArticleDetail/Edit/index.tsx b/src/views/ArticleDetail/Edit/index.tsx index 08f42efdbc..3633a08862 100644 --- a/src/views/ArticleDetail/Edit/index.tsx +++ b/src/views/ArticleDetail/Edit/index.tsx @@ -1,13 +1,14 @@ import { useQuery } from '@apollo/react-hooks' import _omit from 'lodash/omit' import dynamic from 'next/dynamic' -import { useContext, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { ASSET_TYPE, ENTITY_TYPE, MAX_ARTICLE_REVISION_COUNT, } from '~/common/enums' +import { sleep } from '~/common/utils' import { EmptyLayout, Layout, @@ -86,6 +87,17 @@ const BaseEdit = ({ article }: { article: Article }) => { // cover const [assets, setAssets] = useState(article.assets || []) + + useEffect(() => { + const updateAssets = async () => { + // FIXME: newly uploaded images will return 404 in a short time + // https://community.cloudflare.com/t/new-uploaded-images-need-about-10-min-to-display-in-my-website/121568 + await sleep(300) + + setAssets(article.assets || []) + } + updateAssets() + }, [article.assets]) const [cover, setCover] = useState( assets.find((asset) => asset.path === article.cover) ) From 13aabbe5ec6588937645ee618d421c7bb06d1aa0 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:14:39 +0800 Subject: [PATCH 07/35] fix(notice): correct @mention handler --- src/common/utils/text/article.test.ts | 17 +++++++-- src/common/utils/text/article.ts | 41 +++++++++++++++++---- src/common/utils/text/notice.test.ts | 7 +++- src/common/utils/text/notice.ts | 22 +++++------ src/components/Notice/NoticeMomentTitle.tsx | 4 +- 5 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/common/utils/text/article.test.ts b/src/common/utils/text/article.test.ts index e4c1852243..405e52c441 100644 --- a/src/common/utils/text/article.test.ts +++ b/src/common/utils/text/article.test.ts @@ -17,6 +17,15 @@ describe('utils/text/article/stripHtml', () => { 'Hello, world!' ) + expect( + stripHtml( + '

Hello, @world+16我

', + { + ensureMentionTrailingSpace: true, + } + ) + ).toBe('Hello, @world+16我 好') + expect( stripHtml( '

Hello, world!

Hello, world!

Hello,
world!
' @@ -25,9 +34,11 @@ describe('utils/text/article/stripHtml', () => { }) it('should remove HTML tags and custom replacement', () => { - expect(stripHtml('

Hello, world!

', ' ')).toBe( - 'Hello, world !' - ) + expect( + stripHtml('

Hello, world!

', { + tagReplacement: ' ', + }) + ).toBe('Hello, world !') }) }) diff --git a/src/common/utils/text/article.ts b/src/common/utils/text/article.ts index 18be334ac4..baeac2aa7b 100644 --- a/src/common/utils/text/article.ts +++ b/src/common/utils/text/article.ts @@ -8,26 +8,49 @@ import { toSizedImageURL } from '../url' * * @see {@url https://github.com/thematters/ipns-site-generator/blob/main/src/utils/index.ts} */ -export const stripHtml = ( - html: string, - tagReplacement = '', - lineReplacement = '\n' -) => { +type StripHTMLOptions = { + tagReplacement?: string + lineReplacement?: string + ensureMentionTrailingSpace?: boolean +} + +export const stripHtml = (html: string, options?: StripHTMLOptions) => { + options = { + tagReplacement: '', + lineReplacement: '\n', + ensureMentionTrailingSpace: false, + ...options, + } + + const { tagReplacement, lineReplacement, ensureMentionTrailingSpace } = + options + html = String(html) || '' html = html.replace(/\ \;/g, ' ') // Replace block-level elements with newlines - html = html.replace(/<(\/?p|\/?blockquote|br\/?)>/gi, lineReplacement) + html = html.replace(/<(\/?p|\/?blockquote|br\/?)>/gi, lineReplacement!) + + // Handle @user mentions and appending a space + if (ensureMentionTrailingSpace) { + html = html.replace( + /]*class="mention"[^>]*>(.*?)<\/a>(.{1})/gi, + (_, p1, p2) => { + return `${p1}${p2 === ' ' ? ' ' : ` ${p2}`}` + } + ) + } // Remove remaining HTML tags - let plainText = html.replace(/<\/?[^>]+(>|$)/g, tagReplacement) + let plainText = html.replace(/<\/?[^>]+(>|$)/g, tagReplacement!) // Normalize multiple newlines and trim the result plainText = plainText.replace(/\n\s*\n/g, '\n').trim() return plainText } + /** * Return beginning of text in html as summary, split on sentence break within buffer range. * @param html - html string to extract summary @@ -36,7 +59,9 @@ export const stripHtml = ( */ export const makeSummary = (html: string, length = 140, buffer = 20) => { // split on sentence breaks - const sections = stripHtml(html, '', ' ') + const sections = stripHtml(html, { + lineReplacement: ' ', + }) .replace(/&[^;]+;/g, ' ') // remove html entities .replace(/([?!。?!]|(\.\s))\s*/g, '$1|') // split on sentence breaks .split('|') diff --git a/src/common/utils/text/notice.test.ts b/src/common/utils/text/notice.test.ts index a00efd1fb7..ffa88c42e0 100644 --- a/src/common/utils/text/notice.test.ts +++ b/src/common/utils/text/notice.test.ts @@ -12,7 +12,7 @@ const ENGLISH_WITH_NUMBERS_AND_PUNCTUATION = const MIXED = '看起來 10 拍,consectetur Lorem ipsum dolor sit amet, adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' const MENTIONS = - '看起來 10 拍 @用戶 @user Lorem ipsum dolor,快樂喜歡如其實也是我於有我的部分' + '看起來 10 拍 @用戶 @user @user+1 Lorem ipsum dolor,快樂喜歡如其實也是我於有我的部分' describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { it('should return the title for the default length of 10 words', () => { @@ -30,7 +30,7 @@ describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { '看起來 10 拍,consectetur Lorem ipsum dolor sit...' ) expect(truncateNoticeTitle(MENTIONS)).toEqual( - '看起來 10 拍 @用戶 @user Lorem ipsum dolor...' + '看起來 10 拍 @用戶 @user @user+1 Lorem ipsum...' ) }) @@ -40,6 +40,9 @@ describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { expect(truncateNoticeTitle(CHINESE_ONLY, maxLength)).toEqual( '这是一个标题...' ) + expect( + truncateNoticeTitle(CHINESE_ONLY.slice(0, maxLength), maxLength) + ).toEqual('这是一个标题') expect( truncateNoticeTitle(CHINESE_WITH_NUMBERS_AND_PUNCTUATION, maxLength) ).toEqual('看起來 10 拍,快...') diff --git a/src/common/utils/text/notice.ts b/src/common/utils/text/notice.ts index 244589df57..7e0b9ada85 100644 --- a/src/common/utils/text/notice.ts +++ b/src/common/utils/text/notice.ts @@ -1,4 +1,4 @@ -import { PUNCTUATION_ASCII, PUNCTUATION_CHINESE } from '../form' +import { PUNCTUATION_CHINESE } from '../form' /** * Truncates a title to a specified maximum length, while preserving tagged users. @@ -15,9 +15,11 @@ import { PUNCTUATION_ASCII, PUNCTUATION_CHINESE } from '../form' const REGEXP_CJK = '[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]' +const REGEXP_PUNCTUATION = `[${PUNCTUATION_CHINESE}/\x00-\x2f\x3a-\x3f\x41\x5b-\x60\x7a-\x7f/]` // without "@" + function countUnits(word: string) { - // Latin word - if (/^@\w+/.test(word) || new RegExp(`^@${REGEXP_CJK}+`).test(word)) { + // Tagged user + if (/^@[^\s]+/.test(word)) { return 1 } // CJK @@ -35,19 +37,14 @@ function countUnits(word: string) { function trimSpacesAndPunctuations(str: string) { return str.replace( - new RegExp( - `^[${PUNCTUATION_CHINESE}${PUNCTUATION_ASCII}]+|[${PUNCTUATION_CHINESE}${PUNCTUATION_ASCII}]+$`, - 'g' - ), + new RegExp(`^${REGEXP_PUNCTUATION}+|${REGEXP_PUNCTUATION}+$`, 'g'), '' ) } export const truncateNoticeTitle = (title: string, maxLength: number = 10) => { const components = - title.match( - new RegExp(`(@\\w+|@${REGEXP_CJK}+|\\w+|${REGEXP_CJK}|[^\w\s])`, 'g') - ) || [] + title.match(new RegExp(`(@[^\\s]+|\\w+|[^\w\s])`, 'g')) || [] let truncatedTitle = '' let currentLength = 0 @@ -56,8 +53,11 @@ export const truncateNoticeTitle = (title: string, maxLength: number = 10) => { const componentUnits = countUnits(component) if (currentLength + componentUnits > maxLength) { - if (index < components.length - 1) { + // if the current component is not the last one, add ellipsis + if (index <= components.length - 1) { truncatedTitle = trimSpacesAndPunctuations(truncatedTitle) + '...' + } else { + truncatedTitle = trimSpacesAndPunctuations(truncatedTitle) } break } diff --git a/src/components/Notice/NoticeMomentTitle.tsx b/src/components/Notice/NoticeMomentTitle.tsx index 928511d383..b7b06b8b3f 100644 --- a/src/components/Notice/NoticeMomentTitle.tsx +++ b/src/components/Notice/NoticeMomentTitle.tsx @@ -20,7 +20,9 @@ const NoticeMomentTitle = ({ moment, }) - const title = truncateNoticeTitle(stripHtml(moment.content || '')) + const title = truncateNoticeTitle( + stripHtml(moment.content || '', { ensureMentionTrailingSpace: true }) + ) const images = moment.assets.length ? intl .formatMessage({ defaultMessage: `[image]`, id: 'W3tqQO' }) From 2c6ef47d133c017c3c84d0524c57a3152ff55017 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:23:07 +0800 Subject: [PATCH 08/35] fix(Error): use SVG instead of image --- src/components/Error/index.tsx | 6 +++--- src/components/Icon/withIcon.tsx | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Error/index.tsx b/src/components/Error/index.tsx index 59bf7a4977..4788fed273 100644 --- a/src/components/Error/index.tsx +++ b/src/components/Error/index.tsx @@ -1,8 +1,8 @@ import { Alert } from '@reach/alert' import { useContext } from 'react' -import IMAGE_ILLUSTRATION_EMPTY from '@/public/static/images/illustration-empty.svg' -import { LanguageContext } from '~/components' +import { ReactComponent as IconIllustrationEmpty } from '@/public/static/images/illustration-empty.svg' +import { Icon, LanguageContext } from '~/components' import { UserLanguage } from '~/gql/graphql' import styles from './styles.module.css' @@ -62,7 +62,7 @@ export const Error: React.FC> = ({ aria-atomic="true" >
- illustration +
diff --git a/src/components/Icon/withIcon.tsx b/src/components/Icon/withIcon.tsx index 5b377b884d..03b8c68214 100644 --- a/src/components/Icon/withIcon.tsx +++ b/src/components/Icon/withIcon.tsx @@ -16,6 +16,7 @@ export type IconSize = | 48 | 64 | 88 + | 240 export type IconColor = | 'white' From d329a6d969dbf40ab88be087d05ff481bd27ff98 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:26:22 +0800 Subject: [PATCH 09/35] fix(MomentDetailDialog): update error state --- .../Dialogs/MomentDetailDialog/Content.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/components/Dialogs/MomentDetailDialog/Content.tsx b/src/components/Dialogs/MomentDetailDialog/Content.tsx index 95f049a879..f831c10864 100644 --- a/src/components/Dialogs/MomentDetailDialog/Content.tsx +++ b/src/components/Dialogs/MomentDetailDialog/Content.tsx @@ -2,7 +2,6 @@ import { useQuery } from '@apollo/react-hooks' import { Editor } from '@matters/matters-editor' import classNames from 'classnames' import { useEffect, useState } from 'react' -import { FormattedMessage } from 'react-intl' import { ADD_MOMENT_COMMENT_MENTION, @@ -95,24 +94,20 @@ const MomentDetailDialogContent = ({ } if (error) { - return - } - - if (data?.moment?.__typename !== 'Moment') { - return null + return ( +
+ +
+ ) } - if (data.moment.state === MomentState.Archived) { + if ( + data?.moment?.__typename !== 'Moment' || + data.moment.state === MomentState.Archived + ) { return (
- - } - > +
From 3f1bcf8b9647a6d53fdcd8346f980d0592b72a72 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:39:26 +0800 Subject: [PATCH 10/35] fix(Error): revise code --- src/components/Error/index.tsx | 8 +++++++- src/components/Icon/withIcon.tsx | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Error/index.tsx b/src/components/Error/index.tsx index 4788fed273..6c7e2b67f4 100644 --- a/src/components/Error/index.tsx +++ b/src/components/Error/index.tsx @@ -62,7 +62,13 @@ export const Error: React.FC> = ({ aria-atomic="true" >
- +
diff --git a/src/components/Icon/withIcon.tsx b/src/components/Icon/withIcon.tsx index 03b8c68214..5b377b884d 100644 --- a/src/components/Icon/withIcon.tsx +++ b/src/components/Icon/withIcon.tsx @@ -16,7 +16,6 @@ export type IconSize = | 48 | 64 | 88 - | 240 export type IconColor = | 'white' From 3803a660aad3c2721766c3cafd86bb13091ed81c Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:51:58 +0800 Subject: [PATCH 11/35] fix(notice): revise latin regexp --- src/common/utils/text/notice.test.ts | 20 ++++++++++---------- src/common/utils/text/notice.ts | 24 +++++++++++++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/common/utils/text/notice.test.ts b/src/common/utils/text/notice.test.ts index ffa88c42e0..07e104db3f 100644 --- a/src/common/utils/text/notice.test.ts +++ b/src/common/utils/text/notice.test.ts @@ -6,13 +6,13 @@ const CHINESE_ONLY = '这是一个标题这是一个标题这是一个标题这 const CHINESE_WITH_NUMBERS_AND_PUNCTUATION = '看起來 10 拍,快樂喜歡如其實也是我於有我的部分' const ENGLISH_ONLY = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + 'Lorem gustaría dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' const ENGLISH_WITH_NUMBERS_AND_PUNCTUATION = - 'Lorem ipsum 10 dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + 'Lorem gustaría 10 dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' const MIXED = - '看起來 10 拍,consectetur Lorem ipsum dolor sit amet, adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + '看起來 10 拍,consectetur Lorem gustaría dolor sit amet, adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' const MENTIONS = - '看起來 10 拍 @用戶 @user @user+1 Lorem ipsum dolor,快樂喜歡如其實也是我於有我的部分' + '看起來 10 拍 @用戶 @user @user+1 Lorem gustaría dolor,快樂喜歡如其實也是我於有我的部分' describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { it('should return the title for the default length of 10 words', () => { @@ -21,16 +21,16 @@ describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { '看起來 10 拍,快樂喜歡如...' ) expect(truncateNoticeTitle(ENGLISH_ONLY)).toEqual( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do...' + 'Lorem gustaría dolor sit amet, consectetur adipiscing elit, sed do...' ) expect(truncateNoticeTitle(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION)).toEqual( - 'Lorem ipsum 10 dolor sit amet, consectetur adipiscing elit, sed...' + 'Lorem gustaría 10 dolor sit amet, consectetur adipiscing elit, sed...' ) expect(truncateNoticeTitle(MIXED)).toEqual( - '看起來 10 拍,consectetur Lorem ipsum dolor sit...' + '看起來 10 拍,consectetur Lorem gustaría dolor sit...' ) expect(truncateNoticeTitle(MENTIONS)).toEqual( - '看起來 10 拍 @用戶 @user @user+1 Lorem ipsum...' + '看起來 10 拍 @用戶 @user @user+1 Lorem gustaría...' ) }) @@ -47,11 +47,11 @@ describe.concurrent('utils/text/collection/truncateNoticeTitle', () => { truncateNoticeTitle(CHINESE_WITH_NUMBERS_AND_PUNCTUATION, maxLength) ).toEqual('看起來 10 拍,快...') expect(truncateNoticeTitle(ENGLISH_ONLY, maxLength)).toEqual( - 'Lorem ipsum dolor sit amet, consectetur...' + 'Lorem gustaría dolor sit amet, consectetur...' ) expect( truncateNoticeTitle(ENGLISH_WITH_NUMBERS_AND_PUNCTUATION, maxLength) - ).toEqual('Lorem ipsum 10 dolor sit amet...') + ).toEqual('Lorem gustaría 10 dolor sit amet...') expect(truncateNoticeTitle(MIXED, maxLength)).toEqual( '看起來 10 拍,consectetur...' ) diff --git a/src/common/utils/text/notice.ts b/src/common/utils/text/notice.ts index 7e0b9ada85..b98ea9cafc 100644 --- a/src/common/utils/text/notice.ts +++ b/src/common/utils/text/notice.ts @@ -13,9 +13,11 @@ import { PUNCTUATION_CHINESE } from '../form' * @returns The truncated title with preserved tagged users. */ const REGEXP_CJK = - '[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]' + '\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f' -const REGEXP_PUNCTUATION = `[${PUNCTUATION_CHINESE}/\x00-\x2f\x3a-\x3f\x41\x5b-\x60\x7a-\x7f/]` // without "@" +const REGEXP_LATIN = 'A-Za-zÀ-ÖØ-öø-ÿ0-9' + +const REGEXP_PUNCTUATION = `[${PUNCTUATION_CHINESE}\x00-\x2f\x3a-\x3f\x41\x5b-\x60\x7a-\x7f]` // without "@" function countUnits(word: string) { // Tagged user @@ -23,11 +25,11 @@ function countUnits(word: string) { return 1 } // CJK - else if (new RegExp(REGEXP_CJK, 'g').test(word)) { + else if (new RegExp(`[${REGEXP_CJK}]`, 'g').test(word)) { return 1 } // Latin - else if (/^\w+/.test(word)) { + else if (new RegExp(`[${REGEXP_LATIN}]+`).test(word)) { return 1 } @@ -36,15 +38,19 @@ function countUnits(word: string) { } function trimSpacesAndPunctuations(str: string) { - return str.replace( - new RegExp(`^${REGEXP_PUNCTUATION}+|${REGEXP_PUNCTUATION}+$`, 'g'), - '' - ) + return str + .trim() + .replace( + new RegExp(`^${REGEXP_PUNCTUATION}+|${REGEXP_PUNCTUATION}+$`, 'g'), + '' + ) } export const truncateNoticeTitle = (title: string, maxLength: number = 10) => { const components = - title.match(new RegExp(`(@[^\\s]+|\\w+|[^\w\s])`, 'g')) || [] + title.match( + new RegExp(`(@[^\\s]+|[${REGEXP_LATIN}]+|[^${REGEXP_LATIN}\s])`, 'g') + ) || [] let truncatedTitle = '' let currentLength = 0 From 875aaf14888383c6882654a1e24672a873f05328 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:23:07 +0800 Subject: [PATCH 12/35] fix(Error): use SVG instead of image --- src/components/Error/index.tsx | 6 +++--- src/components/Icon/withIcon.tsx | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Error/index.tsx b/src/components/Error/index.tsx index 59bf7a4977..4788fed273 100644 --- a/src/components/Error/index.tsx +++ b/src/components/Error/index.tsx @@ -1,8 +1,8 @@ import { Alert } from '@reach/alert' import { useContext } from 'react' -import IMAGE_ILLUSTRATION_EMPTY from '@/public/static/images/illustration-empty.svg' -import { LanguageContext } from '~/components' +import { ReactComponent as IconIllustrationEmpty } from '@/public/static/images/illustration-empty.svg' +import { Icon, LanguageContext } from '~/components' import { UserLanguage } from '~/gql/graphql' import styles from './styles.module.css' @@ -62,7 +62,7 @@ export const Error: React.FC> = ({ aria-atomic="true" >
- illustration +
diff --git a/src/components/Icon/withIcon.tsx b/src/components/Icon/withIcon.tsx index 5b377b884d..03b8c68214 100644 --- a/src/components/Icon/withIcon.tsx +++ b/src/components/Icon/withIcon.tsx @@ -16,6 +16,7 @@ export type IconSize = | 48 | 64 | 88 + | 240 export type IconColor = | 'white' From 52c46f2e05ae95fe6070c47b8146abc7a03044a6 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:26:22 +0800 Subject: [PATCH 13/35] fix(MomentDetailDialog): update error state --- .../Dialogs/MomentDetailDialog/Content.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/components/Dialogs/MomentDetailDialog/Content.tsx b/src/components/Dialogs/MomentDetailDialog/Content.tsx index 95f049a879..f831c10864 100644 --- a/src/components/Dialogs/MomentDetailDialog/Content.tsx +++ b/src/components/Dialogs/MomentDetailDialog/Content.tsx @@ -2,7 +2,6 @@ import { useQuery } from '@apollo/react-hooks' import { Editor } from '@matters/matters-editor' import classNames from 'classnames' import { useEffect, useState } from 'react' -import { FormattedMessage } from 'react-intl' import { ADD_MOMENT_COMMENT_MENTION, @@ -95,24 +94,20 @@ const MomentDetailDialogContent = ({ } if (error) { - return - } - - if (data?.moment?.__typename !== 'Moment') { - return null + return ( +
+ +
+ ) } - if (data.moment.state === MomentState.Archived) { + if ( + data?.moment?.__typename !== 'Moment' || + data.moment.state === MomentState.Archived + ) { return (
- - } - > +
From c31fdfaeb54ad378a91fa486ac86ed0b4c61c410 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:33:21 +0800 Subject: [PATCH 14/35] feat(moment): make NavCreate popover appendTo parent; click banner to open menu; --- .../NavBar/{NavBanner.tsx => MomentNavBanner.tsx} | 10 +++++++--- src/components/Layout/NavBar/NavCreate.tsx | 13 +++++++++++-- .../ActivityPopover/ActivityPopover.stories.tsx | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) rename src/components/Layout/NavBar/{NavBanner.tsx => MomentNavBanner.tsx} (72%) diff --git a/src/components/Layout/NavBar/NavBanner.tsx b/src/components/Layout/NavBar/MomentNavBanner.tsx similarity index 72% rename from src/components/Layout/NavBar/NavBanner.tsx rename to src/components/Layout/NavBar/MomentNavBanner.tsx index e90dae2767..e208edd733 100644 --- a/src/components/Layout/NavBar/NavBanner.tsx +++ b/src/components/Layout/NavBar/MomentNavBanner.tsx @@ -5,9 +5,13 @@ import MATTY from '@/public/static/images/matty.png' import styles from './styles.module.css' -const NavBanner: React.FC = () => { +type MomentNavBannerProps = { + onClick: () => void +} + +const MomentNavBanner: React.FC = ({ onClick }) => { return ( -
+
@@ -22,4 +26,4 @@ const NavBanner: React.FC = () => { ) } -export default NavBanner +export default MomentNavBanner diff --git a/src/components/Layout/NavBar/NavCreate.tsx b/src/components/Layout/NavBar/NavCreate.tsx index f3226b00ea..8171db38ab 100644 --- a/src/components/Layout/NavBar/NavCreate.tsx +++ b/src/components/Layout/NavBar/NavCreate.tsx @@ -19,7 +19,7 @@ import { } from '~/components' import SideNavNavListItem from '../SideNav/NavListItem' -import NavBanner from './NavBanner' +import MomentNavBanner from './MomentNavBanner' import NavPopover from './NavPopover' export const NavCreate = () => { @@ -47,11 +47,19 @@ export const NavCreate = () => { arrow={true} onHidden={closeMomentBanner} visible={showMomentBanner} - content={} + content={ + { + closeMomentBanner() + openWriteDropdown() + }} + /> + } placement="top" onShown={hidePopperOnClick} offset={[0, 12]} // 16px - 4px (default tippy padding) theme="banner" + appendTo="parent" > {({ ref: bannerRef }) => ( { onShown={hidePopperOnClick} offset={[0, 12]} // 16px - 4px (default tippy padding) theme="mobile" + appendTo="parent" > {({ ref: navRef }) => ( diff --git a/src/stories/components/ActivityPopover/ActivityPopover.stories.tsx b/src/stories/components/ActivityPopover/ActivityPopover.stories.tsx index 22f581f038..623ef1db06 100644 --- a/src/stories/components/ActivityPopover/ActivityPopover.stories.tsx +++ b/src/stories/components/ActivityPopover/ActivityPopover.stories.tsx @@ -10,7 +10,7 @@ import { Icon, useDialogSwitch, } from '~/components' -import NavBanner from '~/components/Layout/NavBar/NavBanner' +import MomentNavBanner from '~/components/Layout/NavBar/MomentNavBanner' import NavPopover from '~/components/Layout/NavBar/NavPopover' import Activity from '~/components/Layout/SideNav/Activity' import NavListItem from '~/components/Layout/SideNav/NavListItem' @@ -94,7 +94,7 @@ export const ActivityBanner: StoryFn = () => { - + {}} /> } visible={show} From d8331424462ff7823e0616afc63dc3207b5b150a Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:51:56 +0800 Subject: [PATCH 15/35] fix(ArticleDetail): remove sleep time --- src/views/ArticleDetail/Edit/index.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/views/ArticleDetail/Edit/index.tsx b/src/views/ArticleDetail/Edit/index.tsx index 3633a08862..1b88e284ab 100644 --- a/src/views/ArticleDetail/Edit/index.tsx +++ b/src/views/ArticleDetail/Edit/index.tsx @@ -8,7 +8,6 @@ import { ENTITY_TYPE, MAX_ARTICLE_REVISION_COUNT, } from '~/common/enums' -import { sleep } from '~/common/utils' import { EmptyLayout, Layout, @@ -89,14 +88,7 @@ const BaseEdit = ({ article }: { article: Article }) => { const [assets, setAssets] = useState(article.assets || []) useEffect(() => { - const updateAssets = async () => { - // FIXME: newly uploaded images will return 404 in a short time - // https://community.cloudflare.com/t/new-uploaded-images-need-about-10-min-to-display-in-my-website/121568 - await sleep(300) - - setAssets(article.assets || []) - } - updateAssets() + setAssets(article.assets || []) }, [article.assets]) const [cover, setCover] = useState( assets.find((asset) => asset.path === article.cover) From ae719a4adf2debd9df1f9ca89c2281620258a263 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:28:20 +0800 Subject: [PATCH 16/35] fix(tabs): add gradient visibility logic to handle tab overflow scenarios --- src/components/SquareTabs/index.tsx | 52 +++++++++++++++++++-- src/components/SquareTabs/styles.module.css | 42 +++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/components/SquareTabs/index.tsx b/src/components/SquareTabs/index.tsx index b47b2b9126..7e775d5ec0 100644 --- a/src/components/SquareTabs/index.tsx +++ b/src/components/SquareTabs/index.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames' +import { useEffect, useRef, useState } from 'react' import styles from './styles.module.css' @@ -34,15 +35,60 @@ interface SquareTabsProps { export const SquareTabs: React.FC> & { Tab: typeof Tab } = ({ children, sticky }) => { + const navRef = useRef(null) + const containerRef = useRef(null) + const $nav = navRef.current + const $container = containerRef.current + const [showLeftGradient, setShowLeftGradient] = useState(false) + const [showRightGradient, setShowRightGradient] = useState(false) + + const isTabsOverflowing = () => { + if (!$nav || !$container) return false + return $nav.scrollWidth > $container.clientWidth + } + + const calculateGradient = () => { + if (!$nav || !$container) return + + const isAtLeftMost = $nav.scrollLeft <= 0 + const isAtRightMost = $nav.scrollLeft + $nav.clientWidth >= $nav.scrollWidth + + setShowLeftGradient(!isAtLeftMost) + setShowRightGradient(!isAtRightMost) + } + + useEffect(() => { + if (!isTabsOverflowing() || !$nav) return + + // initial gradient + calculateGradient() + + $nav.addEventListener('scroll', calculateGradient) + + return () => { + if (!$nav) return + + $nav.removeEventListener('scroll', calculateGradient) + } + }, [$nav]) + + const containerClasses = classNames({ + [styles.container]: true, + [styles.showLeftGradient]: showLeftGradient, + [styles.showRightGradient]: showRightGradient, + }) + const navClasses = classNames({ [styles.tabList]: true, [styles.sticky]: sticky, }) return ( -
    - {children} -
+
+
    + {children} +
+
) } diff --git a/src/components/SquareTabs/styles.module.css b/src/components/SquareTabs/styles.module.css index c98570d7b3..ec2c2a4078 100644 --- a/src/components/SquareTabs/styles.module.css +++ b/src/components/SquareTabs/styles.module.css @@ -1,3 +1,45 @@ +.container { + position: relative; + + &::before, + &::after { + position: absolute; + top: 0; + bottom: 0; + z-index: calc(var(--z-index-sticky-tabs) + 1); + width: 7.5rem; + pointer-events: none; + content: ''; + opacity: 0; /* Initially hidden */ + transition: opacity 0.3s; + } + + &.showLeftGradient::before, + &.showRightGradient::after { + opacity: 1; /* Show when scrollable */ + } + + &::before { + left: 0; + background: linear-gradient( + -90deg, + rgb(255 255 255 / 0%) 0%, + rgb(255 255 255 / 30%) 20%, + #fff 100% + ); + } + + &::after { + right: 0; + background: linear-gradient( + 90deg, + rgb(255 255 255 / 0%) 0%, + rgb(255 255 255 / 30%) 20%, + #fff 100% + ); + } +} + .tabList { @mixin hide-scrollbar; From 9fe006262d2e51fe5dafb1bc11f297bc1eeb976a Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:43:22 +0800 Subject: [PATCH 17/35] fix(MomentDetail): sanitize content --- src/views/MomentDetail/Edit/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/views/MomentDetail/Edit/index.tsx b/src/views/MomentDetail/Edit/index.tsx index 36fe6f8711..e7d4638547 100644 --- a/src/views/MomentDetail/Edit/index.tsx +++ b/src/views/MomentDetail/Edit/index.tsx @@ -8,6 +8,7 @@ import { CLEAR_MOMENT_FORM, MAX_MOMENT_CONTENT_LENGTH } from '~/common/enums' import { formStorage, parseFormSubmitErrors, + sanitizeContent, stripHtml, toPath, } from '~/common/utils' @@ -84,7 +85,7 @@ const Edit = () => { const { data } = await putMoment({ variables: { input: { - content, + content: sanitizeContent(content), assets: assets.map(({ assetId }) => assetId), }, }, From 29f6e9f29231be61d5a9a3a23f5bb2491d720088 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:27:31 +0800 Subject: [PATCH 18/35] fix(SetCover): update cache after image upload done --- src/components/Editor/SetCover/Uploader.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/Editor/SetCover/Uploader.tsx b/src/components/Editor/SetCover/Uploader.tsx index bc93bd0f8e..0015e10686 100644 --- a/src/components/Editor/SetCover/Uploader.tsx +++ b/src/components/Editor/SetCover/Uploader.tsx @@ -10,7 +10,7 @@ import { ASSET_TYPE, ENTITY_TYPE, } from '~/common/enums' -import { sleep, validateImage } from '~/common/utils' +import { validateImage } from '~/common/utils' import { DraftDetailStateContext, Icon, @@ -55,13 +55,14 @@ const Uploader: React.FC = ({ const [upload, { loading }] = useMutation( DIRECT_IMAGE_UPLOAD, + undefined, + { showToast: false } + ) + const [directImageUploadDone] = useMutation( + DIRECT_IMAGE_UPLOAD_DONE, { update: async (cache, { data }) => { if (data?.directImageUpload) { - // FIXME: newly uploaded images will return 404 in a short time - // https://community.cloudflare.com/t/new-uploaded-images-need-about-10-min-to-display-in-my-website/121568 - await sleep(300) - updateDraftAssets({ cache, id: entityId, @@ -72,11 +73,6 @@ const Uploader: React.FC = ({ }, { showToast: false } ) - const [directImageUploadDone] = useMutation( - DIRECT_IMAGE_UPLOAD_DONE, - undefined, - { showToast: false } - ) const { upload: uploadImage, uploading } = useDirectImageUpload() const { isInPath } = useRoute() From c2c140407e3323ebdffef431d93e041de2665e9e Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:28:20 +0800 Subject: [PATCH 19/35] fix(tabs): add gradient visibility logic to handle tab overflow scenarios --- src/components/SquareTabs/index.tsx | 52 +++++++++++++++++++-- src/components/SquareTabs/styles.module.css | 42 +++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/components/SquareTabs/index.tsx b/src/components/SquareTabs/index.tsx index b47b2b9126..7e775d5ec0 100644 --- a/src/components/SquareTabs/index.tsx +++ b/src/components/SquareTabs/index.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames' +import { useEffect, useRef, useState } from 'react' import styles from './styles.module.css' @@ -34,15 +35,60 @@ interface SquareTabsProps { export const SquareTabs: React.FC> & { Tab: typeof Tab } = ({ children, sticky }) => { + const navRef = useRef(null) + const containerRef = useRef(null) + const $nav = navRef.current + const $container = containerRef.current + const [showLeftGradient, setShowLeftGradient] = useState(false) + const [showRightGradient, setShowRightGradient] = useState(false) + + const isTabsOverflowing = () => { + if (!$nav || !$container) return false + return $nav.scrollWidth > $container.clientWidth + } + + const calculateGradient = () => { + if (!$nav || !$container) return + + const isAtLeftMost = $nav.scrollLeft <= 0 + const isAtRightMost = $nav.scrollLeft + $nav.clientWidth >= $nav.scrollWidth + + setShowLeftGradient(!isAtLeftMost) + setShowRightGradient(!isAtRightMost) + } + + useEffect(() => { + if (!isTabsOverflowing() || !$nav) return + + // initial gradient + calculateGradient() + + $nav.addEventListener('scroll', calculateGradient) + + return () => { + if (!$nav) return + + $nav.removeEventListener('scroll', calculateGradient) + } + }, [$nav]) + + const containerClasses = classNames({ + [styles.container]: true, + [styles.showLeftGradient]: showLeftGradient, + [styles.showRightGradient]: showRightGradient, + }) + const navClasses = classNames({ [styles.tabList]: true, [styles.sticky]: sticky, }) return ( -
    - {children} -
+
+
    + {children} +
+
) } diff --git a/src/components/SquareTabs/styles.module.css b/src/components/SquareTabs/styles.module.css index c98570d7b3..ec2c2a4078 100644 --- a/src/components/SquareTabs/styles.module.css +++ b/src/components/SquareTabs/styles.module.css @@ -1,3 +1,45 @@ +.container { + position: relative; + + &::before, + &::after { + position: absolute; + top: 0; + bottom: 0; + z-index: calc(var(--z-index-sticky-tabs) + 1); + width: 7.5rem; + pointer-events: none; + content: ''; + opacity: 0; /* Initially hidden */ + transition: opacity 0.3s; + } + + &.showLeftGradient::before, + &.showRightGradient::after { + opacity: 1; /* Show when scrollable */ + } + + &::before { + left: 0; + background: linear-gradient( + -90deg, + rgb(255 255 255 / 0%) 0%, + rgb(255 255 255 / 30%) 20%, + #fff 100% + ); + } + + &::after { + right: 0; + background: linear-gradient( + 90deg, + rgb(255 255 255 / 0%) 0%, + rgb(255 255 255 / 30%) 20%, + #fff 100% + ); + } +} + .tabList { @mixin hide-scrollbar; From 3c6037522515e51d9e33f8c5c754c7e23fca5e4d Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:02:42 +0800 Subject: [PATCH 20/35] fix(Drawer): add overscroll-behavior --- src/components/Drawer/styles.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Drawer/styles.module.css b/src/components/Drawer/styles.module.css index 06d3c536e6..1f130b6b60 100644 --- a/src/components/Drawer/styles.module.css +++ b/src/components/Drawer/styles.module.css @@ -17,6 +17,7 @@ flex-direction: column; padding: 0 var(--sp48); overflow-y: auto; + overscroll-behavior: contain; visibility: hidden; background: white; box-shadow: -8px 0 56px 0 rgb(0 0 0 / 8%) !important; From 33654bb33ecddf5abda7a5ffc60e692319c0c5b9 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Mon, 2 Sep 2024 18:52:04 +0800 Subject: [PATCH 21/35] feat(tabs): support horizontal scroll with mouse move --- src/components/SquareTabs/index.tsx | 39 ++++++++++++++++++--- src/components/SquareTabs/styles.module.css | 1 + 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/components/SquareTabs/index.tsx b/src/components/SquareTabs/index.tsx index 7e775d5ec0..b654a7c81f 100644 --- a/src/components/SquareTabs/index.tsx +++ b/src/components/SquareTabs/index.tsx @@ -41,8 +41,13 @@ export const SquareTabs: React.FC> & { const $container = containerRef.current const [showLeftGradient, setShowLeftGradient] = useState(false) const [showRightGradient, setShowRightGradient] = useState(false) + const [isDragging, setIsDragging] = useState(false) + const [startX, setStartX] = useState(0) + const [scrollLeft, setScrollLeft] = useState(0) const isTabsOverflowing = () => { + const $nav = navRef.current + const $container = containerRef.current if (!$nav || !$container) return false return $nav.scrollWidth > $container.clientWidth } @@ -57,6 +62,25 @@ export const SquareTabs: React.FC> & { setShowRightGradient(!isAtRightMost) } + const handleMouseDown = (e: React.MouseEvent) => { + if (!$nav) return + setIsDragging(true) + setStartX(e.pageX - $nav.offsetLeft) + setScrollLeft($nav.scrollLeft) + } + + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging || !$nav) return + const x = e.pageX - $nav.offsetLeft + const walk = (x - startX) * 2 // scroll-fast + $nav.scrollLeft = scrollLeft - walk + calculateGradient() + } + + const handleMouseUp = () => { + setIsDragging(false) + } + useEffect(() => { if (!isTabsOverflowing() || !$nav) return @@ -64,13 +88,15 @@ export const SquareTabs: React.FC> & { calculateGradient() $nav.addEventListener('scroll', calculateGradient) + window.addEventListener('mousemove', handleMouseMove) + window.addEventListener('mouseup', handleMouseUp) return () => { - if (!$nav) return - $nav.removeEventListener('scroll', calculateGradient) + window.removeEventListener('mousemove', handleMouseMove) + window.removeEventListener('mouseup', handleMouseUp) } - }, [$nav]) + }, [$nav, $container, isDragging]) const containerClasses = classNames({ [styles.container]: true, @@ -85,7 +111,12 @@ export const SquareTabs: React.FC> & { return (
-
    +
      {children}
diff --git a/src/components/SquareTabs/styles.module.css b/src/components/SquareTabs/styles.module.css index ec2c2a4078..1dd1b8f48d 100644 --- a/src/components/SquareTabs/styles.module.css +++ b/src/components/SquareTabs/styles.module.css @@ -65,6 +65,7 @@ line-height: 1.375rem; color: var(--color-grey-darker); cursor: pointer; + user-select: none; background: var(--color-grey-lighter); border-radius: 0.5rem; transition-property: background-color, color; From c0a142803cd74b265babe0a2148a686ce4dcb113 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:56:10 +0800 Subject: [PATCH 22/35] fix(ArticleDetail): fix author info in tablet --- src/views/ArticleDetail/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/views/ArticleDetail/index.tsx b/src/views/ArticleDetail/index.tsx index 83eed8e366..f52e849a8e 100644 --- a/src/views/ArticleDetail/index.tsx +++ b/src/views/ArticleDetail/index.tsx @@ -404,9 +404,11 @@ const BaseArticleDetail = ({ )} - + + + {article.comments.totalCount > 0 && (
From 599a3d0d7a39d811afa395f8963213b27dec3870 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:58:22 +0800 Subject: [PATCH 23/35] feat(tabs): listen mouse* events on $nav instead of window --- src/components/SquareTabs/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SquareTabs/index.tsx b/src/components/SquareTabs/index.tsx index 084f3b8a35..886768f992 100644 --- a/src/components/SquareTabs/index.tsx +++ b/src/components/SquareTabs/index.tsx @@ -86,13 +86,13 @@ export const SquareTabs: React.FC> & { calculateGradient() $nav.addEventListener('scroll', calculateGradient) - window.addEventListener('mousemove', handleMouseMove) - window.addEventListener('mouseup', handleMouseUp) + $nav.addEventListener('mousemove', handleMouseMove) + $nav.addEventListener('mouseup', handleMouseUp) return () => { $nav.removeEventListener('scroll', calculateGradient) - window.removeEventListener('mousemove', handleMouseMove) - window.removeEventListener('mouseup', handleMouseUp) + $nav.removeEventListener('mousemove', handleMouseMove) + $nav.removeEventListener('mouseup', handleMouseUp) } }, [$nav, $container, isDragging]) From cefaaea249d333929373b7ed637a36f0771aaa36 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:12:54 +0800 Subject: [PATCH 24/35] fix(ArticleCommentForm): fix visitor mode in form --- src/components/Forms/ArticleCommentForm/index.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/Forms/ArticleCommentForm/index.tsx b/src/components/Forms/ArticleCommentForm/index.tsx index 9e9f7bf2a6..b098a1173e 100644 --- a/src/components/Forms/ArticleCommentForm/index.tsx +++ b/src/components/Forms/ArticleCommentForm/index.tsx @@ -211,6 +211,17 @@ export const ArticleCommentForm: React.FC = ({ id: 'bTNYGv', description: 'src/components/Forms/ArticleCommentForm/index.tsx', })} + onClick={(event) => { + if (!viewer.isAuthed) { + event.preventDefault() + window.dispatchEvent( + new CustomEvent(OPEN_UNIVERSAL_AUTH_DIALOG, { + detail: { trigger: UNIVERSAL_AUTH_TRIGGER.collectArticle }, + }) + ) + return + } + }} >
Date: Tue, 3 Sep 2024 14:45:31 +0800 Subject: [PATCH 25/35] fix(NoticeComment): fix archive article comment --- lang/default.json | 15 ++++++----- lang/en.json | 15 ++++++----- lang/zh-Hans.json | 15 ++++++----- lang/zh-Hant.json | 15 ++++++----- src/components/Notice/NoticeComment.tsx | 34 +++++++++++-------------- 5 files changed, 47 insertions(+), 47 deletions(-) diff --git a/lang/default.json b/lang/default.json index f6246138b9..8b24dfc52c 100644 --- a/lang/default.json +++ b/lang/default.json @@ -121,10 +121,6 @@ "/usqHn": { "defaultMessage": "{displayName}'s creative space" }, - "/vyhs5": { - "defaultMessage": "Comment deleted", - "description": "src/components/Notice/NoticeComment.tsx" - }, "/wKyxw": { "defaultMessage": "Failed to republish" }, @@ -618,6 +614,10 @@ "defaultMessage": "Still quiet here. {br}Be the first one to say hello!", "description": "src/components/Empty/EmptyComment.tsx" }, + "7zn5ig": { + "defaultMessage": "Comment deleted", + "description": "src/components/Notice/NoticeComment.tsx/article" + }, "8+Z5E9": { "defaultMessage": "The badge signifies your participation and completion in the \"Free Write in 7 days\"." }, @@ -889,6 +889,10 @@ "defaultMessage": "Copy comment", "description": "src/components/Comment/DropdownActions/index.tsx" }, + "Ci7dxf": { + "defaultMessage": "Comment deleted", + "description": "src/components/Notice/NoticeComment.tsx/moment" + }, "CjKqYk": { "defaultMessage": "Share a story from your life" }, @@ -1486,9 +1490,6 @@ "N6PWfU": { "defaultMessage": "Forget Password" }, - "N8ISx8": { - "defaultMessage": "Oops! This comment has been deleted by author" - }, "NACY16": { "defaultMessage": "Why need to set up a wallet?", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" diff --git a/lang/en.json b/lang/en.json index 6958c1ba9e..82f5192358 100644 --- a/lang/en.json +++ b/lang/en.json @@ -121,10 +121,6 @@ "/usqHn": { "defaultMessage": "{displayName}'s creative space" }, - "/vyhs5": { - "defaultMessage": "Comment deleted", - "description": "src/components/Notice/NoticeComment.tsx" - }, "/wKyxw": { "defaultMessage": "Failed to republish" }, @@ -618,6 +614,10 @@ "defaultMessage": "Still quiet here. {br}Be the first one to say hello!", "description": "src/components/Empty/EmptyComment.tsx" }, + "7zn5ig": { + "defaultMessage": "Comment deleted", + "description": "src/components/Notice/NoticeComment.tsx/article" + }, "8+Z5E9": { "defaultMessage": "The badge signifies your participation and completion in the \"Free Write in 7 days\"." }, @@ -889,6 +889,10 @@ "defaultMessage": "Copy comment", "description": "src/components/Comment/DropdownActions/index.tsx" }, + "Ci7dxf": { + "defaultMessage": "Comment deleted", + "description": "src/components/Notice/NoticeComment.tsx/moment" + }, "CjKqYk": { "defaultMessage": "Share a story from your life" }, @@ -1486,9 +1490,6 @@ "N6PWfU": { "defaultMessage": "Forget Password" }, - "N8ISx8": { - "defaultMessage": "Oops! This comment has been deleted by author" - }, "NACY16": { "defaultMessage": "Why need to set up a wallet?", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" diff --git a/lang/zh-Hans.json b/lang/zh-Hans.json index 9987fe61cc..66f536ad7d 100644 --- a/lang/zh-Hans.json +++ b/lang/zh-Hans.json @@ -121,10 +121,6 @@ "/usqHn": { "defaultMessage": "{displayName} 的创作空间站" }, - "/vyhs5": { - "defaultMessage": "留言已删除", - "description": "src/components/Notice/NoticeComment.tsx" - }, "/wKyxw": { "defaultMessage": "发布失败" }, @@ -618,6 +614,10 @@ "defaultMessage": "暂无评论", "description": "src/components/Empty/EmptyComment.tsx" }, + "7zn5ig": { + "defaultMessage": "评论已删除", + "description": "src/components/Notice/NoticeComment.tsx/article" + }, "8+Z5E9": { "defaultMessage": "纪念你参与「七日书」并完成七天书写" }, @@ -889,6 +889,10 @@ "defaultMessage": "复制留言", "description": "src/components/Comment/DropdownActions/index.tsx" }, + "Ci7dxf": { + "defaultMessage": "留言已删除", + "description": "src/components/Notice/NoticeComment.tsx/moment" + }, "CjKqYk": { "defaultMessage": "分享今天开心或难过的小故事吧" }, @@ -1486,9 +1490,6 @@ "N6PWfU": { "defaultMessage": "忘记密码" }, - "N8ISx8": { - "defaultMessage": "Oops!该评论已被原作者删除" - }, "NACY16": { "defaultMessage": "为什么需要设定钱包 ?", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" diff --git a/lang/zh-Hant.json b/lang/zh-Hant.json index b50d7d0e83..2ccfd46a52 100644 --- a/lang/zh-Hant.json +++ b/lang/zh-Hant.json @@ -121,10 +121,6 @@ "/usqHn": { "defaultMessage": "{displayName} 的創作空間站" }, - "/vyhs5": { - "defaultMessage": "留言已刪除", - "description": "src/components/Notice/NoticeComment.tsx" - }, "/wKyxw": { "defaultMessage": "發布失敗" }, @@ -618,6 +614,10 @@ "defaultMessage": "暫無評論", "description": "src/components/Empty/EmptyComment.tsx" }, + "7zn5ig": { + "defaultMessage": "評論已刪除", + "description": "src/components/Notice/NoticeComment.tsx/article" + }, "8+Z5E9": { "defaultMessage": "紀念你參與「七日書」並完成七天書寫" }, @@ -889,6 +889,10 @@ "defaultMessage": "複製留言", "description": "src/components/Comment/DropdownActions/index.tsx" }, + "Ci7dxf": { + "defaultMessage": "留言已刪除", + "description": "src/components/Notice/NoticeComment.tsx/moment" + }, "CjKqYk": { "defaultMessage": "分享今天開心或難過的小故事吧" }, @@ -1486,9 +1490,6 @@ "N6PWfU": { "defaultMessage": "忘記密碼" }, - "N8ISx8": { - "defaultMessage": "Oops!該評論已被原作者刪除" - }, "NACY16": { "defaultMessage": "為什麼需要設定錢包 ?", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" diff --git a/src/components/Notice/NoticeComment.tsx b/src/components/Notice/NoticeComment.tsx index 43674952af..119eca7092 100644 --- a/src/components/Notice/NoticeComment.tsx +++ b/src/components/Notice/NoticeComment.tsx @@ -67,6 +67,8 @@ const NoticeComment = ({ return null } + console.log({ comment }) + if ( comment.state === 'banned' && ((comment.parentComment === null && comment.comments?.totalCount === 0) || @@ -98,8 +100,8 @@ const NoticeComment = ({ @@ -107,24 +109,18 @@ const NoticeComment = ({ ) } - if (comment.state === 'archived') { + if (comment.state === 'archived' && article) { return ( - +
+ +
) } From 6cd299f2add65dc0c83bad17d434912df0058a4b Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:36:37 +0800 Subject: [PATCH 26/35] feat(NoticeArticleTitle): update archived title --- lang/default.json | 4 ++++ lang/en.json | 4 ++++ lang/zh-Hans.json | 4 ++++ lang/zh-Hant.json | 4 ++++ src/components/Notice/NoticeArticleTitle.tsx | 15 +++++++++++++-- 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lang/default.json b/lang/default.json index 8b24dfc52c..2d10896945 100644 --- a/lang/default.json +++ b/lang/default.json @@ -3532,6 +3532,10 @@ "z3uIHQ": { "defaultMessage": "Undo upvote" }, + "z91BKe": { + "defaultMessage": "Archived Work", + "description": "src/components/Notice/NoticeArticleTitle.tsx" + }, "zAK5G+": { "defaultMessage": "The login link has been sent to {email}", "description": "src/components/Forms/Verification/LinkSent.tsx" diff --git a/lang/en.json b/lang/en.json index 82f5192358..65438ff0f7 100644 --- a/lang/en.json +++ b/lang/en.json @@ -3532,6 +3532,10 @@ "z3uIHQ": { "defaultMessage": "Undo upvote" }, + "z91BKe": { + "defaultMessage": "Archived Work", + "description": "src/components/Notice/NoticeArticleTitle.tsx" + }, "zAK5G+": { "defaultMessage": "The login link has been sent to {email}", "description": "src/components/Forms/Verification/LinkSent.tsx" diff --git a/lang/zh-Hans.json b/lang/zh-Hans.json index 66f536ad7d..7bddb86514 100644 --- a/lang/zh-Hans.json +++ b/lang/zh-Hans.json @@ -3532,6 +3532,10 @@ "z3uIHQ": { "defaultMessage": "取消点赞" }, + "z91BKe": { + "defaultMessage": "已归档作品", + "description": "src/components/Notice/NoticeArticleTitle.tsx" + }, "zAK5G+": { "defaultMessage": "登录链接已发送至 {email}", "description": "src/components/Forms/Verification/LinkSent.tsx" diff --git a/lang/zh-Hant.json b/lang/zh-Hant.json index 2ccfd46a52..c5bbdca94e 100644 --- a/lang/zh-Hant.json +++ b/lang/zh-Hant.json @@ -3532,6 +3532,10 @@ "z3uIHQ": { "defaultMessage": "取消點讚" }, + "z91BKe": { + "defaultMessage": "已封存作品", + "description": "src/components/Notice/NoticeArticleTitle.tsx" + }, "zAK5G+": { "defaultMessage": "登入連結已發送至 {email}", "description": "src/components/Forms/Verification/LinkSent.tsx" diff --git a/src/components/Notice/NoticeArticleTitle.tsx b/src/components/Notice/NoticeArticleTitle.tsx index 4206eba4f3..4c7b1934c6 100644 --- a/src/components/Notice/NoticeArticleTitle.tsx +++ b/src/components/Notice/NoticeArticleTitle.tsx @@ -1,10 +1,11 @@ import gql from 'graphql-tag' import Link from 'next/link' +import { FormattedMessage } from 'react-intl' import { TEST_ID } from '~/common/enums' import { toPath } from '~/common/utils' import { ArticleDigestTitle } from '~/components/ArticleDigest' -import { NoticeArticleTitleFragment } from '~/gql/graphql' +import { ArticleState, NoticeArticleTitleFragment } from '~/gql/graphql' import styles from './styles.module.css' @@ -24,6 +25,8 @@ const NoticeArticleTitle = ({ article, }) + const isArchived = article.articleState === ArticleState.Archived + if (!isBlock) { return ( @@ -31,7 +34,15 @@ const NoticeArticleTitle = ({ className={styles.noticeArticleTitle} data-test-id={TEST_ID.NOTICE_ARTICLE_TITLE} > - {article.title} + {isArchived ? ( + + ) : ( + article.title + )} ) From 481bc2ac00483445bbe529fb202b111a43d58403 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:39:59 +0800 Subject: [PATCH 27/35] fix(NoticeComment): update archived article comment --- src/components/Notice/NoticeComment.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Notice/NoticeComment.tsx b/src/components/Notice/NoticeComment.tsx index 119eca7092..f17c6ea196 100644 --- a/src/components/Notice/NoticeComment.tsx +++ b/src/components/Notice/NoticeComment.tsx @@ -19,6 +19,7 @@ const fragments = { id title slug + articleState: state shortHash author { id @@ -31,7 +32,7 @@ const fragments = { } ... on Moment { id - state + momentState: state shortHash } } @@ -67,8 +68,6 @@ const NoticeComment = ({ return null } - console.log({ comment }) - if ( comment.state === 'banned' && ((comment.parentComment === null && comment.comments?.totalCount === 0) || @@ -124,7 +123,11 @@ const NoticeComment = ({ ) } - if (comment.state === 'active' && moment && moment.state === 'archived') { + if ( + comment.state === 'active' && + ((moment && moment.momentState === 'archived') || + (article && article.articleState === 'archived')) + ) { return (
From 93093d4393c5c3d5d917451a6807e34b750deaca Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:49:09 +0800 Subject: [PATCH 28/35] feat(ArticleDetail): update archived state --- src/views/ArticleDetail/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/views/ArticleDetail/index.tsx b/src/views/ArticleDetail/index.tsx index f52e849a8e..17fba58490 100644 --- a/src/views/ArticleDetail/index.tsx +++ b/src/views/ArticleDetail/index.tsx @@ -582,18 +582,14 @@ const ArticleDetail = ({ - ) : article.state === 'banned' ? ( + article.state === 'banned' ? ( ) : null } + type="not_found" > From e0425f97b86feaf31107337ad6ed96ccfbe1e463 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:20:36 +0800 Subject: [PATCH 29/35] fix(ArticleComment): fix comment count cache --- .../CommentForm/index.tsx | 20 --------------- .../Forms/ArticleCommentForm/index.tsx | 25 +------------------ 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/src/components/Dialogs/ArticleCommentFormDialog/CommentForm/index.tsx b/src/components/Dialogs/ArticleCommentFormDialog/CommentForm/index.tsx index db6e5a1a59..4ab5bca237 100644 --- a/src/components/Dialogs/ArticleCommentFormDialog/CommentForm/index.tsx +++ b/src/components/Dialogs/ArticleCommentFormDialog/CommentForm/index.tsx @@ -9,13 +9,11 @@ import { SpinnerBlock, useEventListener, useMutation, - useRoute, ViewerContext, } from '~/components' import { PUT_ARTICLE_COMMENT } from '~/components/GQL/mutations/putComment' import { updateArticleComments, - updateArticlePublic, updateCommentDetail, } from '~/components/GQL/updates' import { PutArticleCommentMutation } from '~/gql/graphql' @@ -54,8 +52,6 @@ const CommentForm: React.FC = ({ }) => { const viewer = useContext(ViewerContext) const formRef = useRef(null) - const { getQuery, routerLang } = useRoute() - const shortHash = getQuery('shortHash') const [putComment] = useMutation(PUT_ARTICLE_COMMENT) @@ -120,22 +116,6 @@ const CommentForm: React.FC = ({ comment: mutationResult.data?.putComment, }) } - - if (!!parentId) { - updateArticlePublic({ - cache, - shortHash, - routerLang, - type: 'addSecondaryComment', - }) - } else { - updateArticlePublic({ - cache, - shortHash, - routerLang, - type: 'addComment', - }) - } }, }) diff --git a/src/components/Forms/ArticleCommentForm/index.tsx b/src/components/Forms/ArticleCommentForm/index.tsx index b098a1173e..00321ce7b6 100644 --- a/src/components/Forms/ArticleCommentForm/index.tsx +++ b/src/components/Forms/ArticleCommentForm/index.tsx @@ -15,15 +15,10 @@ import { useCommentEditorContext, useEventListener, useMutation, - useRoute, ViewerContext, } from '~/components' import CommentEditor from '~/components/Editor/Comment' -import { - updateArticleComments, - updateArticlePublic, - updateCommentDetail, -} from '~/components/GQL' +import { updateArticleComments, updateCommentDetail } from '~/components/GQL' import { PUT_ARTICLE_COMMENT } from '~/components/GQL/mutations/putComment' import { PutArticleCommentMutation } from '~/gql/graphql' @@ -63,14 +58,12 @@ export const ArticleCommentForm: React.FC = ({ }) => { const intl = useIntl() const viewer = useContext(ViewerContext) - const { getQuery, routerLang } = useRoute() const { setActiveEditor } = useCommentEditorContext() const [editor, localSetEditor] = useState(null) const setEditor = (editor: Editor | null) => { localSetEditor(editor) propsSetEditor?.(editor) } - const shortHash = getQuery('shortHash') const [putComment] = useMutation(PUT_ARTICLE_COMMENT) @@ -136,22 +129,6 @@ export const ArticleCommentForm: React.FC = ({ comment: mutationResult.data?.putComment, }) } - - if (!!parentId) { - updateArticlePublic({ - cache, - shortHash, - routerLang, - type: 'addSecondaryComment', - }) - } else { - updateArticlePublic({ - cache, - shortHash, - routerLang, - type: 'addComment', - }) - } }, }) From 5db16e79d7ddd6b98a5685b5920ccd0b21c88e53 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:43:23 +0800 Subject: [PATCH 30/35] fix(Comment): add commentCount field --- src/components/Comment/FooterActions/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Comment/FooterActions/index.tsx b/src/components/Comment/FooterActions/index.tsx index bad961a5aa..27be351f3b 100644 --- a/src/components/Comment/FooterActions/index.tsx +++ b/src/components/Comment/FooterActions/index.tsx @@ -64,6 +64,7 @@ const fragments = { id isBlocking } + commentCount } ... on Moment { From d01d6f1c50e7621ef2de92f1dfbbd8c1186259f0 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:07:01 +0800 Subject: [PATCH 31/35] fix(articlePublic): remove unused code --- src/components/GQL/updates/articlePublic.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/GQL/updates/articlePublic.ts b/src/components/GQL/updates/articlePublic.ts index f34e933746..54da6e07c7 100644 --- a/src/components/GQL/updates/articlePublic.ts +++ b/src/components/GQL/updates/articlePublic.ts @@ -22,12 +22,7 @@ export const updateArticlePublic = ({ routerLang: UserLanguage viewer?: Viewer txId?: string - type: - | 'deleteComment' - | 'addComment' - | 'addSecondaryComment' - | 'deleteSecondaryComment' - | 'updateDonation' + type: 'deleteComment' | 'deleteSecondaryComment' | 'updateDonation' }) => { // FIXME: circular dependencies const { @@ -68,17 +63,10 @@ export const updateArticlePublic = ({ let commentCount = data.article.commentCount let totalCount = data.article.comments.totalCount switch (type) { - case 'addComment': - totalCount += 1 - commentCount += 1 - break case 'deleteComment': totalCount -= 1 commentCount -= 1 break - case 'addSecondaryComment': - commentCount += 1 - break case 'deleteSecondaryComment': commentCount -= 1 break From b8d9481f571fd7869d740649049a072d9d81d8b4 Mon Sep 17 00:00:00 2001 From: devformatters2 <177856586+devformatters2@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:42:16 +0800 Subject: [PATCH 32/35] fix(tabs): add spacing right to every tab item --- src/components/SquareTabs/styles.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SquareTabs/styles.module.css b/src/components/SquareTabs/styles.module.css index 1dd1b8f48d..b20d95eb29 100644 --- a/src/components/SquareTabs/styles.module.css +++ b/src/components/SquareTabs/styles.module.css @@ -44,7 +44,6 @@ @mixin hide-scrollbar; display: flex; - gap: var(--sp16); overflow-x: auto; -webkit-overflow-scrolling: touch; @@ -61,6 +60,7 @@ flex-shrink: 0; padding: var(--sp5) var(--sp10); + margin-right: var(--sp16); font-size: var(--text14); line-height: 1.375rem; color: var(--color-grey-darker); From 325840f93fd93ed103b429d71ddeee96e8024b52 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:04:46 +0800 Subject: [PATCH 33/35] fix(Comment): add editable props --- src/components/Editor/Comment/index.tsx | 3 +++ src/components/Forms/ArticleCommentForm/index.tsx | 1 + 2 files changed, 4 insertions(+) diff --git a/src/components/Editor/Comment/index.tsx b/src/components/Editor/Comment/index.tsx index 553e77f426..36a0dc5bf7 100644 --- a/src/components/Editor/Comment/index.tsx +++ b/src/components/Editor/Comment/index.tsx @@ -30,6 +30,7 @@ interface Props { onFocused?: () => void isFallbackEditor?: boolean lockScroll?: boolean + editable?: boolean } const CommentEditor: React.FC = ({ @@ -41,6 +42,7 @@ const CommentEditor: React.FC = ({ onFocused, isFallbackEditor, lockScroll = true, + editable = true, }) => { const client = useApolloClient() const intl = useIntl() @@ -54,6 +56,7 @@ const CommentEditor: React.FC = ({ }) const editor = useEditor({ + editable, content: content || '', onUpdate: async ({ editor, transaction }) => { const content = editor.getHTML() diff --git a/src/components/Forms/ArticleCommentForm/index.tsx b/src/components/Forms/ArticleCommentForm/index.tsx index 00321ce7b6..614acafce0 100644 --- a/src/components/Forms/ArticleCommentForm/index.tsx +++ b/src/components/Forms/ArticleCommentForm/index.tsx @@ -210,6 +210,7 @@ export const ArticleCommentForm: React.FC = ({ setEditor={(editor) => { setEditor(editor) }} + editable={viewer.isAuthed} />
From c3e6fc0064e3ef8c737a26850c0ba1121d3d8ea5 Mon Sep 17 00:00:00 2001 From: bluecloud <96812901+pitb2022@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:10:30 +0800 Subject: [PATCH 34/35] fix(CampaignDetail): disable SSR at SideParticipants --- src/views/CampaignDetail/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/views/CampaignDetail/index.tsx b/src/views/CampaignDetail/index.tsx index a10039114a..9248a45153 100644 --- a/src/views/CampaignDetail/index.tsx +++ b/src/views/CampaignDetail/index.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@apollo/react-hooks' +import dynamic from 'next/dynamic' import { useContext } from 'react' import { toPath } from '~/common/utils' @@ -17,7 +18,11 @@ import { CampaignDetailQuery } from '~/gql/graphql' import ArticleFeeds from './ArticleFeeds' import { CAMPAIGN_DETAIL } from './gql' import InfoHeader from './InfoHeader' -import SideParticipants from './SideParticipants' + +const DynamicSideParticipants = dynamic(() => import('./SideParticipants'), { + loading: () => , + ssr: false, +}) const CampaignDetail = () => { const { lang } = useContext(LanguageContext) @@ -58,7 +63,7 @@ const CampaignDetail = () => { const path = toPath({ page: 'campaignDetail', campaign }) return ( - }> + }> Date: Wed, 4 Sep 2024 18:13:28 +0800 Subject: [PATCH 35/35] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e2a021cd7..4ee14b3f99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "5.6.1", + "version": "5.6.2", "description": "codebase of Matters' website", "author": "Matters ", "engines": {