diff --git a/src/i18n/en.json b/src/i18n/en.json index fc61f27f0..c7cafaf6f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -359,12 +359,12 @@ "storyCreation.hyperlink.form.linkTextLabel": "Customize link text", "storyCreation.hyperlink.form.linkTextPlaceholder": "Name your link", "storyCreation.hyperlink.form.linkTextDescription": "This text will show on the link instead of URL.", - "storyCreation.hyperlink.validation.invalidUrl": "Please enter a valid URL.", "storyCreation.hyperlink.form.removeButton": "Remove link", "storyCreation.hyperlink.removeConfirm.title": "Remove link", "storyCreation.hyperlink.removeConfirm.content": "This link will be removed from story.", "storyCreation.hyperlink.removeConfirm.cancel": "Cancel", "storyCreation.hyperlink.removeConfirm.confirm": "Remove", + "storyCreation.hyperlink.validation.error.invalidUrl": "Please enter a valid URL.", "storyCreation.hyperlink.validation.error.whitelisted": "Please enter a whitelisted URL.", "storyCreation.hyperlink.validation.error.blocked": "Your text contains a blocklisted word.", @@ -381,10 +381,15 @@ "storyViewer.commentSheet.disabled": "Comments are disabled for this story", "storyViewer.commentSheet.replyingTo": "Replying to", "storyViewer.toast.like.disabled": "Join community to interact with all stories", + "storyViewer.toast.comment.reported": "Comment reported", + "storyViewer.toast.comment.unreported": "Comment unreported", "storyViewer.commentComposeBar.submit": "Post", "storyDraft.button.shareStory": "Share story", + "storyDraft.notification.hyperlink.error": "Can’t add more than one link to your story.", + + "storyTab.title": "Story", "storyTab.title": "Story", diff --git a/src/social/components/CommentList/index.tsx b/src/social/components/CommentList/index.tsx index 0f4c71f02..5699d3225 100644 --- a/src/social/components/CommentList/index.tsx +++ b/src/social/components/CommentList/index.tsx @@ -1,7 +1,9 @@ import React, { memo } from 'react'; import { useIntl } from 'react-intl'; + import Comment from '~/social/components/Comment'; -import { NoCommentsContainer, TabIcon, TabIconContainer } from './styles'; + +import { TabIcon, TabIconContainer } from './styles'; import LoadMoreWrapper from '../LoadMoreWrapper'; import usePostSubscription from '~/social/hooks/usePostSubscription'; import { SubscriptionLevels } from '@amityco/ts-sdk'; @@ -11,6 +13,7 @@ interface CommentListProps { parentId?: string; referenceId?: string; referenceType: Amity.CommentReferenceType; + // filterByParentId?: boolean; readonly?: boolean; isExpanded?: boolean; limit?: number; @@ -21,6 +24,8 @@ const CommentList = ({ referenceId, referenceType, limit = 5, + // TODO: breaking change + // filterByParentId = false, readonly = false, isExpanded = true, }: CommentListProps) => { @@ -41,7 +46,7 @@ const CommentList = ({ }); const loadMoreText = isReplyComment - ? formatMessage({ id: 'collapsible.viewMoreReplies' }) + ? formatMessage({ id: 'collapsible.viewMoreReplies' }, { count: comments.length }) : formatMessage({ id: 'collapsible.viewMoreComments' }); const prependIcon = isReplyComment ? ( @@ -50,14 +55,6 @@ const CommentList = ({ ) : null; - if (comments.length === 0 && referenceType === 'story' && !isReplyComment) { - return ( - - {formatMessage({ id: 'storyViewer.commentSheet.empty' })} - - ); - } - if (comments.length === 0) return null; return ( diff --git a/src/social/components/CommentList/styles.tsx b/src/social/components/CommentList/styles.tsx index b50a2ba33..ce7e260c3 100644 --- a/src/social/components/CommentList/styles.tsx +++ b/src/social/components/CommentList/styles.tsx @@ -7,13 +7,3 @@ export const TabIconContainer = styled.div` display: flex; margin-right: 8px; `; - -export const NoCommentsContainer = styled.div` - ${({ theme }) => theme.typography.body}; - color: ${({ theme }) => theme.palette.base.shade2}; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; -`; diff --git a/src/social/components/EngagementBar/UIEngagementBar.tsx b/src/social/components/EngagementBar/UIEngagementBar.tsx index 08340c882..328a3c158 100644 --- a/src/social/components/EngagementBar/UIEngagementBar.tsx +++ b/src/social/components/EngagementBar/UIEngagementBar.tsx @@ -73,7 +73,7 @@ const UIEngagementBar = ({ - {latestComments.length > 0 ? ( + {latestComments?.length > 0 ? ( ) : null} @@ -91,7 +91,7 @@ const UIEngagementBar = ({ - {latestComments.length > 0 ? ( + {latestComments?.length > 0 ? ( { const post = usePost(postId); - const avatarFileUrl = useImage({ fileId: post?.creator?.avatarFileId, imageSize: 'small' }); - - const { isFlaggedByMe, flagPost, unflagPost } = usePostFlaggedByMe(post); - + const postedUser = useUser(post?.postedUserId); + const avatarFileUrl = useImage({ fileId: postedUser?.avatarFileId, imageSize: 'small' }); + const childrenPosts = usePostByIds(post?.children); + const { userRoles } = useSDK(); + const { isFlaggedByMe, toggleFlagPost } = usePostFlaggedByMe(post); const postRenderFn = usePostRenderer(post?.dataType); + const { currentUserId } = useSDK(); usePostSubscription({ postId, @@ -40,9 +43,7 @@ const Post = ({ postId, className, hidePostTarget, readonly, onDeleted }: PostPr shouldSubscribe: () => !!post, }); - const pollPost = (post?.latestComments || []).find( - (childPost: Amity.Post) => childPost.dataType === 'poll', - ); + const pollPost = (childrenPosts || []).find((childPost) => childPost.dataType === 'poll'); const poll = usePoll((pollPost?.data as Amity.ContentDataPoll)?.pollId); const isPollClosed = poll?.status === 'closed'; @@ -74,21 +75,21 @@ const Post = ({ postId, className, hidePostTarget, readonly, onDeleted }: PostPr return ( <> {postRenderFn({ - childrenPosts: post?.latestComments || [], + childrenPosts: childrenPosts || [], handleClosePoll, isPollClosed, avatarFileUrl, - user: post?.creator, + user: postedUser, poll, className, - currentUserId: post?.postedUserId || undefined, + currentUserId: currentUserId || undefined, hidePostTarget, post, - userRoles: post?.creator?.roles || [], + userRoles, readonly, isFlaggedByMe, - handleReportPost: flagPost, - handleUnreportPost: unflagPost, + handleReportPost: toggleFlagPost, + handleUnreportPost: toggleFlagPost, handleApprovePost, handleDeclinePost, handleDeletePost, diff --git a/src/social/hooks/collections/useCommentsCollection.ts b/src/social/hooks/collections/useCommentsCollection.ts index a65f100c7..ae64c716d 100644 --- a/src/social/hooks/collections/useCommentsCollection.ts +++ b/src/social/hooks/collections/useCommentsCollection.ts @@ -7,7 +7,6 @@ type useCommentsParams = { referenceId?: string | null; referenceType: Amity.CommentReferenceType; limit?: number; - shouldCall?: () => boolean; // breaking changes // first?: number; // last?: number; @@ -18,7 +17,6 @@ export default function useCommentsCollection({ referenceId, referenceType, limit = 10, - shouldCall = () => true, }: useCommentsParams) { const { items, ...rest } = useLiveCollection({ fetcher: CommentRepository.getComments, @@ -28,7 +26,7 @@ export default function useCommentsCollection({ referenceType, limit, }, - shouldCall: () => shouldCall() && !!referenceId && !!referenceType, + shouldCall: () => !!referenceId && !!referenceType, }); return { diff --git a/src/v4/chat/components/AmityLiveChatMessageComposeBar/index.tsx b/src/v4/chat/components/AmityLiveChatMessageComposeBar/index.tsx index b5ae3ec51..18441efca 100644 --- a/src/v4/chat/components/AmityLiveChatMessageComposeBar/index.tsx +++ b/src/v4/chat/components/AmityLiveChatMessageComposeBar/index.tsx @@ -129,7 +129,8 @@ export const AmityLiveChatMessageComposeBar = ({ multiline disabled={disabled} placeholder={ - componentConfig?.placeholder_text || + (typeof componentConfig?.placeholder_text === 'string' && + componentConfig?.placeholder_text) || formatMessage({ id: 'livechat.composebar.placeholder', }) diff --git a/src/v4/chat/components/MessageQuickReaction/index.tsx b/src/v4/chat/components/MessageQuickReaction/index.tsx index 33a350d58..4d59a2045 100644 --- a/src/v4/chat/components/MessageQuickReaction/index.tsx +++ b/src/v4/chat/components/MessageQuickReaction/index.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { useCustomization } from '~/v4/core/providers/CustomizationProvider'; -import { useCustomReaction } from '~/v4/core/providers/CustomReactionProvider'; +import { AmityReactionType, useCustomReaction } from '~/v4/core/providers/CustomReactionProvider'; import { QuickReactionIcon } from '~/v4/icons/QuickReactionIcon'; import { selectMessageReaction } from '~/v4/utils/selectMessageReaction'; import styles from './styles.module.css'; @@ -30,7 +30,10 @@ export const MessageQuickReaction = ({ elementConfig.reaction && reactionConfig.find((config) => config.name === elementConfig.reaction) ) { - selectMessageReaction({ reactionName: elementConfig.reaction, message }); + selectMessageReaction({ + reactionName: elementConfig.reaction as AmityReactionType['name'], + message, + }); } onSelectReaction && onSelectReaction(); diff --git a/src/v4/core/components/BottomSheet/BottomSheet.module.css b/src/v4/core/components/BottomSheet/BottomSheet.module.css index b258fba24..73ad9606f 100644 --- a/src/v4/core/components/BottomSheet/BottomSheet.module.css +++ b/src/v4/core/components/BottomSheet/BottomSheet.module.css @@ -4,29 +4,24 @@ Note that you might need to use !important for style overrides since the inner s which have higher specificity. */ -.react-modal-sheet-container { - color: var(--asc-color-base-default); -} - -.react-modal-sheet-backdrop { - background-color: var(--asc-color-base-inverse, rgba(0, 0, 0, 0.5)); +.bottomSheet__container { + background-color: var(--asc-color-base-background); } -.react-modal-sheet-header { +.bottomSheet__header { + padding: 1rem; display: flex; - padding-bottom: 1rem; justify-content: center; - align-items: center; - gap: 0.5rem; - align-self: stretch; - background-color: var(--asc-color-base-background); - color: var(--asc-color-base-default); - border-bottom: 1px solid var(--asc-color-base-shade4); } -.react-modal-sheet-content { - display: flex; +.bottomSheet__drag-indicator { +} + +.bottomSheet__content { background-color: var(--asc-color-base-background); padding: 1rem; - color: var(--asc-color-base-default); +} + +.bottomSheet__backdrop { + background-color: rgba(0, 0, 0, 0.5); } diff --git a/src/v4/core/components/BottomSheet/BottomSheet.tsx b/src/v4/core/components/BottomSheet/BottomSheet.tsx index d74a40450..5b5653ab9 100644 --- a/src/v4/core/components/BottomSheet/BottomSheet.tsx +++ b/src/v4/core/components/BottomSheet/BottomSheet.tsx @@ -14,32 +14,29 @@ interface BottomSheetProps { headerTitle?: string; cancelText?: string; okText?: string; + className?: string; } export const BottomSheet = ({ children, headerTitle, ...props }: BottomSheetProps) => { return ( - + - {headerTitle && ( - - {headerTitle} - - )} - {children} + > + + {headerTitle && ( + + {headerTitle} + + )} + + {children} - + ); }; diff --git a/src/v4/core/components/ConfirmModal/index.tsx b/src/v4/core/components/ConfirmModal/index.tsx index 7bb1f5804..346ae463f 100644 --- a/src/v4/core/components/ConfirmModal/index.tsx +++ b/src/v4/core/components/ConfirmModal/index.tsx @@ -42,7 +42,7 @@ const Confirm = ({ } onCancel={onCancel} > -
{content}
+
{content}
); @@ -61,7 +61,7 @@ export const ConfirmComponent = () => { confirmData?.onOk && confirmData.onOk(); }; - return ; + return ; }; export default Confirm; diff --git a/src/v4/core/components/ConfirmModal/styles.module.css b/src/v4/core/components/ConfirmModal/styles.module.css index 75d341c64..da6c634b9 100644 --- a/src/v4/core/components/ConfirmModal/styles.module.css +++ b/src/v4/core/components/ConfirmModal/styles.module.css @@ -2,6 +2,10 @@ max-width: 22.5rem !important; } +.background { + background-color: var(--asc-color-base-background); +} + .footer { display: flex; justify-content: flex-end; diff --git a/src/v4/core/components/Modal/styles.module.css b/src/v4/core/components/Modal/styles.module.css index 90ffcdc37..9e1d71960 100644 --- a/src/v4/core/components/Modal/styles.module.css +++ b/src/v4/core/components/Modal/styles.module.css @@ -39,10 +39,11 @@ .modalWindow { margin: auto; - background-color: var(--asc-color-base-background); border-radius: var(--asc-border-radius-lg); max-width: 32.5rem; min-width: 20rem; + /* TOFIX --asc-color-base-background is not defined some how */ + background-color: var(--asc-color-white); } .modalWindow:focus { @@ -71,4 +72,4 @@ .footer { padding: var(--asc-spacing-m2) var(--asc-spacing-s2); padding-top: var(--asc-spacing-xxs2); -} \ No newline at end of file +} diff --git a/src/v4/core/hooks/uikit/index.tsx b/src/v4/core/hooks/uikit/index.tsx new file mode 100644 index 000000000..af69bde3a --- /dev/null +++ b/src/v4/core/hooks/uikit/index.tsx @@ -0,0 +1,47 @@ +import { getDefaultConfig, useCustomization } from '~/v4/core/providers/CustomizationProvider'; +import { useGenerateStylesShadeColors } from '~/v4/core/providers/ThemeProvider'; + +export const useAmityElement = ({ + pageId, + componentId, + elementId, +}: { + pageId: string; + componentId: string; + elementId: string; +}) => { + const uiReference = `${pageId}/${componentId}/${elementId}`; + const { getConfig, isExcluded } = useCustomization(); + const config = getConfig(uiReference); + const defaultConfig = getDefaultConfig(uiReference); + const themeStyles = useGenerateStylesShadeColors(config); + const isComponentExcluded = isExcluded(uiReference); + const accessibilityId = uiReference; + + return { + config, + defaultConfig, + uiReference, + accessibilityId, + themeStyles, + isExcluded: isComponentExcluded, + }; +}; + +export const useAmityComponent = ({ + pageId, + componentId, +}: { + pageId: string; + componentId: string; +}) => { + const elementId = '*'; + return useAmityElement({ pageId, componentId, elementId }); +}; + +export const useAmityPage = ({ pageId }: { pageId: string }) => { + const componentId = '*'; + const elementId = '*'; + + return useAmityElement({ pageId, componentId, elementId }); +}; diff --git a/src/v4/core/providers/CustomizationProvider.tsx b/src/v4/core/providers/CustomizationProvider.tsx index fde68dca0..82bc01bcb 100644 --- a/src/v4/core/providers/CustomizationProvider.tsx +++ b/src/v4/core/providers/CustomizationProvider.tsx @@ -1,11 +1,18 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { AmityReactionType } from './CustomReactionProvider'; +export type GetConfigReturnValue = IconConfiguration & + TextConfiguration & + ThemeConfiguration & + CustomConfiguration; + interface CustomizationContextValue { config: Config | null; parseConfig: (config: Config) => void; isExcluded: (path: string) => boolean; - getConfig: (path: string) => Record; + getConfig: ( + path: string, + ) => IconConfiguration & TextConfiguration & ThemeConfiguration & CustomConfiguration; } export type Theme = { @@ -35,6 +42,14 @@ export type Theme = { }; }; +type ThemeConfiguration = { + preferred_theme?: 'light' | 'dark' | 'default'; + theme?: { + light?: Partial>; + dark?: Partial>; + }; +}; + export interface Config { preferred_theme?: 'light' | 'dark' | 'default'; theme?: { @@ -44,202 +59,22 @@ export interface Config { excludes?: string[]; message_reactions?: AmityReactionType[]; customizations?: { - 'select_target_page/*/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - }; - title?: string; - }; - 'select_target_page/*/back_button'?: { - back_icon?: string; - }; - 'camera_page/*/*'?: { - resolution?: string; - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - 'camera_page/*/close_button'?: { - close_icon?: string; - background_color?: string; - }; - 'create_story_page/*/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - 'create_story_page/*/back_button'?: { - back_icon?: string; - background_color?: string; - }; - 'create_story_page/*/aspect_ratio_button'?: { - aspect_ratio_icon?: string; - background_color?: string; - }; - 'create_story_page/*/story_hyperlink_button'?: { - hyperlink_button_icon?: string; - background_color?: string; - }; - 'create_story_page/*/hyper_link'?: { - hyper_link_icon?: string; - background_color?: string; - }; - 'create_story_page/*/share_story_button'?: { - share_icon?: string; - background_color?: string; - hide_avatar?: boolean; - }; - 'story_page/*/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - 'story_page/*/progress_bar'?: { - progress_color?: string; - background_color?: string; - }; - 'story_page/*/overflow_menu'?: { - overflow_menu_icon?: string; - }; - 'story_page/*/close_button'?: { - close_icon?: string; - }; - 'story_page/*/story_impression_button'?: { - impression_icon?: string; - }; - 'story_page/*/story_comment_button'?: { - comment_icon?: string; - background_color?: string; - }; - 'story_page/*/story_reaction_button'?: { - reaction_icon?: string; - background_color?: string; - }; - 'story_page/*/create_new_story_button'?: { - create_new_story_icon?: string; - background_color?: string; - }; - 'story_page/*/speaker_button'?: { - mute_icon?: string; - unmute_icon?: string; - background_color?: string; - }; - '*/edit_comment_component/*'?: { - theme?: { - light_theme?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - '*/edit_comment_component/cancel_button'?: { - cancel_icon?: string; - cancel_button_text?: string; - background_color?: string; - }; - '*/edit_comment_component/save_button'?: { - save_icon?: string; - save_button_text?: string; - background_color?: string; - }; - '*/hyper_link_config_component/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - '*/hyper_link_config_component/done_button'?: { - done_icon?: string; - done_button_text?: string; - background_color?: string; - }; - '*/hyper_link_config_component/cancel_button'?: { - cancel_icon?: string; - cancel_button_text?: string; - }; - '*/comment_tray_component/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - '*/story_tab_component/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - '*/story_tab_component/story_ring'?: { - progress_color?: string[]; - background_color?: string; - }; - '*/story_tab_component/create_new_story_button'?: { - create_new_story_icon?: string; - background_color?: string; - }; - '*/*/close_button'?: { - close_icon?: string; - }; - 'live_chat/*/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - dark?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - 'live_chat/chat_header/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - dark?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - 'live_chat/message_list/*'?: { - theme?: { - light?: { - primary_color?: string; - secondary_color?: string; - }; - dark?: { - primary_color?: string; - secondary_color?: string; - }; - }; - }; - 'live_chat/message_composer/*'?: { - placeholder_text?: 'Write a message'; - }; + [key: string]: IconConfiguration & TextConfiguration & ThemeConfiguration & CustomConfiguration; }; } +type DefaultConfig = { + preferred_theme: 'light' | 'dark' | 'default'; + theme: { + light: Theme['light']; + dark: Theme['dark']; + }; + excludes: string[]; + customizations?: { + [key: string]: IconConfiguration & TextConfiguration & ThemeConfiguration & CustomConfiguration; + }; +}; + const CustomizationContext = createContext({ config: null, parseConfig: () => {}, @@ -260,7 +95,17 @@ interface CustomizationProviderProps { initialConfig: Config; } -export const defaultConfig: Config = { +type IconConfiguration = { + icon?: string; +}; +type TextConfiguration = { + text?: string; +}; +type CustomConfiguration = { + [key: string]: string | undefined | boolean | Array | number | Record; +}; + +export const defaultConfig: DefaultConfig = { preferred_theme: 'default', theme: { light: { @@ -396,9 +241,171 @@ export const defaultConfig: Config = { '*/*/close_button': { close_icon: 'close.png', }, + 'social_home_page/top_navigation/header_label': { + text: 'Community', + }, + 'social_home_page/top_navigation/global_search_button': { + icon: 'searchButtonIcon', + }, + 'social_home_page/top_navigation/post_creation_button': { + icon: 'postCreationIcon', + }, + 'social_home_page/*/newsfeed_button': { + text: 'Newsfeed', + }, + 'social_home_page/*/explore_button': { + text: 'Explore', + }, + 'social_home_page/*/my_communities_button': { + text: 'My Communities', + }, + 'social_home_page/empty_newsfeed/illustration': { + icon: 'emptyFeedIcon', + }, + 'social_home_page/empty_newsfeed/title': { + text: 'Your Feed is empty', + }, + 'social_home_page/empty_newsfeed/description': { + text: 'Find community or create your own', + }, + 'social_home_page/empty_newsfeed/explore_communities_button': { + icon: 'exploreCommunityIcon', + text: 'Explore Community', + }, + 'social_home_page/empty_newsfeed/create_community_button': { + icon: 'createCommunityIcon', + }, + 'social_home_page/my_communities/community_avatar': {}, + 'social_home_page/my_communities/community_display_name': {}, + 'social_home_page/my_communities/community_private_badge': { + icon: 'lockIcon', + }, + 'social_home_page/my_communities/community_official_badge': { + icon: 'officalBadgeIcon', + }, + 'social_home_page/my_communities/community_category_name': {}, + 'social_home_page/my_communities/community_members_count': {}, + 'social_home_page/newsfeed_component/*': {}, + 'social_home_page/global_feed_component/*': {}, + 'global_search_page/*/*': {}, + 'post_detail_page/*/back_button': { + icon: 'backButtonIcon', + }, + 'post_detail_page/*/menu_button': { + icon: 'menuIcon', + }, + '*/*/moderator_badge': { + icon: 'badgeIcon', + text: 'Moderator', + }, + '*/post_content/moderator_badge': { + icon: 'badgeIcon', + text: 'Moderator', + theme: { + light: { + primary_color: '#FA4D30', + secondary_color: '#292B32', + }, + dark: { + primary_color: '#00FF00', + secondary_color: '#292B32', + }, + }, + }, + '*/post_comment/*': { + preferred_theme: 'default', + theme: { + light: { + primary_color: '#FFC0CB', + secondary_color: '#292B32', + }, + dark: { + primary_color: '#FFFF00', + secondary_color: '#292B32', + }, + }, + }, + '*/post_content/timestamp': {}, + '*/post_content/menu_button': { + icon: 'menuIcon', + }, + '*/post_content/post_content_view_count': {}, + '*/post_content/reaction_button': { + icon: 'likeButtonIcon', + text: 'Like', + }, + '*/post_content/comment_button': { + icon: 'commentButtonIcon', + text: 'Comment', + }, + '*/post_content/share_button': { + icon: 'shareButtonIcon', + text: 'Share', + }, + 'social_global_search_page/*/*': {}, + 'social_global_search_page/top_search_bar/*': {}, + 'social_global_search_page/top_search_bar/search_icon': { + icon: 'search', + }, + 'social_global_search_page/top_search_bar/clear_button': { + icon: 'clear', + }, + 'social_global_search_page/top_search_bar/cancel_button': { + text: 'Cancel', + }, + 'social_global_search_page/community_search_result/community_avatar': {}, + 'social_global_search_page/community_search_result/community_display_name': {}, + 'social_global_search_page/community_search_result/community_private_badge': { + icon: 'lockIcon', + }, + 'social_global_search_page/community_search_result/community_official_badge': { + icon: 'officialBadgeIcon', + }, + 'social_global_search_page/community_search_result/community_category_name': {}, + 'social_global_search_page/community_search_result/community_members_count': {}, }, }; +export const getDefaultConfig: CustomizationContextValue['getConfig'] = (path: string) => { + const [page, component, element] = path.split('/'); + + const customizationKeys = (() => { + if (element !== '*') { + return [ + `${page}/${component}/${element}`, + `${page}/*/${element}`, + `${page}/${component}/*`, + `${page}/*/*`, + `*/${component}/${element}`, + `*/*/${element}`, + `*/${component}/*`, + `*/*/*`, + ]; + } else if (component !== '*') { + return [`${page}/${component}/*`, `${page}/*/*`, `*/${component}/*`, `*/*/*`]; + } else if (page !== '*') { + return [`${page}/*/*`, `*/*/*`]; + } + + return []; + })(); + + return new Proxy< + IconConfiguration & TextConfiguration & { theme?: Partial } & CustomConfiguration + >( + {}, + { + get(target, prop: string) { + for (const key of customizationKeys) { + if (defaultConfig?.customizations?.[key]?.[prop]) { + return defaultConfig.customizations[key][prop]; + } + } + }, + }, + ); +}; + export const CustomizationProvider: React.FC = ({ children, initialConfig, @@ -439,9 +446,56 @@ export const CustomizationProvider: React.FC = ({ }); }; - const getConfig = (path: string) => { - if (!config?.customizations) return {}; - return config?.customizations[path as keyof Config['customizations']] || {}; + const getConfig: CustomizationContextValue['getConfig'] = (path: string) => { + const [page, component, element] = path.split('/'); + + const customizationKeys = (() => { + if (element !== '*') { + return [ + `${page}/${component}/${element}`, + `${page}/*/${element}`, + `${page}/${component}/*`, + `${page}/*/*`, + `*/${component}/${element}`, + `*/*/${element}`, + `*/${component}/*`, + `*/*/*`, + ]; + } else if (component !== '*') { + return [`${page}/${component}/*`, `${page}/*/*`, `*/${component}/*`, `*/*/*`]; + } else if (page !== '*') { + return [`${page}/*/*`, `*/*/*`]; + } + + return []; + })(); + + return new Proxy< + IconConfiguration & TextConfiguration & { theme?: Partial } & CustomConfiguration + >( + {}, + { + get(target, prop: string) { + for (const key of customizationKeys) { + if (config?.customizations?.[key]?.[prop]) { + return config.customizations[key][prop]; + } + } + for (const key of customizationKeys) { + if (defaultConfig?.customizations?.[key]?.[prop]) { + return defaultConfig.customizations[key][prop]; + } + } + + if (prop === 'theme') { + return defaultConfig.theme; + } + if (prop === 'preferred_theme') { + return defaultConfig.preferred_theme; + } + }, + }, + ); }; const contextValue: CustomizationContextValue = { diff --git a/src/v4/core/providers/ThemeProvider.tsx b/src/v4/core/providers/ThemeProvider.tsx index 928d15037..646b3966c 100644 --- a/src/v4/core/providers/ThemeProvider.tsx +++ b/src/v4/core/providers/ThemeProvider.tsx @@ -1,15 +1,12 @@ -import React, { createContext, useEffect, useState } from 'react'; +import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { lighten, parseToHsl, darken, hslToColorString } from 'polished'; -import { useCustomization, Config, Theme } from './CustomizationProvider'; +import { defaultConfig, GetConfigReturnValue } from './CustomizationProvider'; const SHADE_PERCENTAGES = [0.25, 0.4, 0.5, 0.75]; -const setCSSVariable = (variable: string, value?: string) => { - if (!value) return; - document.documentElement.style.setProperty(variable, value); -}; +const generateShades = (hexColor?: string, isDarkMode = false): string[] => { + if (!hexColor) return Array(SHADE_PERCENTAGES.length).fill(''); -const generateShades = (hexColor: string, isDarkMode = false): string[] => { const hslColor = parseToHsl(hexColor); const shades = SHADE_PERCENTAGES.map((percentage) => { @@ -23,93 +20,104 @@ const generateShades = (hexColor: string, isDarkMode = false): string[] => { return shades; }; -const generatePaletteByConfig = ({ - themeConfig, - configId, - isDarkMode, -}: { - themeConfig: Theme['light'] | Theme['dark']; - configId?: string; - isDarkMode?: boolean; -}) => { - const primaryColorShades = generateShades(themeConfig.primary_color, isDarkMode); - const secondaryColorShades = generateShades(themeConfig.secondary_color, isDarkMode); - - const prefix = configId ? configId + '-' : ''; - - setCSSVariable(`--${prefix}asc-color-primary-default`, themeConfig.primary_color); - setCSSVariable(`--${prefix}asc-color-primary-shade1`, primaryColorShades[0]); - setCSSVariable(`--${prefix}asc-color-primary-shade2`, primaryColorShades[1]); - setCSSVariable(`--${prefix}asc-color-primary-shade3`, primaryColorShades[2]); - setCSSVariable(`--${prefix}asc-color-primary-shade4`, primaryColorShades[3]); - - setCSSVariable(`--${prefix}asc-color-secondary-default`, themeConfig.secondary_color); - setCSSVariable(`--${prefix}asc-color-secondary-shade1`, secondaryColorShades[0]); - setCSSVariable(`--${prefix}asc-color-secondary-shade2`, secondaryColorShades[1]); - setCSSVariable(`--${prefix}asc-color-secondary-shade3`, secondaryColorShades[2]); - setCSSVariable(`--${prefix}asc-color-secondary-shade4`, secondaryColorShades[3]); - - setCSSVariable(`--${prefix}asc-color-base-default`, themeConfig.base_color); - setCSSVariable(`--${prefix}asc-color-base-shade1`, themeConfig.base_shade1_color); - setCSSVariable(`--${prefix}asc-color-base-shade2`, themeConfig.base_shade2_color); - setCSSVariable(`--${prefix}asc-color-base-shade3`, themeConfig.base_shade3_color); - setCSSVariable(`--${prefix}asc-color-base-shade4`, themeConfig.base_shade4_color); - - setCSSVariable(`--${prefix}asc-color-alert`, themeConfig.alert_color); - setCSSVariable(`--${prefix}asc-color-base-background`, themeConfig.background_color); - setCSSVariable(`--${prefix}asc-color-base-inverse`, themeConfig.base_inverse_color); -}; +export function useGenerateStylesShadeColors(inputConfig?: GetConfigReturnValue) { + const currentTheme = useTheme(); + + const inputThemeConfig = inputConfig?.theme; -const generateComponentPalette = (config: Config, currentTheme: 'light' | 'dark') => { - const configurables = [ - { - pageId: 'live_chat', - componentIds: ['chat_header', 'message_list', 'message_composer'], - }, - ]; - - configurables.forEach((configurable) => { - const pageConfig = (config.customizations as { [key: string]: { theme: Theme } })?.[ - `${configurable.pageId}/*/*` - ]?.theme; - - if (pageConfig) { - const themeToGenerate = currentTheme === 'light' ? pageConfig.light : pageConfig.dark; - const configId = configurable.pageId.replace('_', '-'); - - if (themeToGenerate) { - generatePaletteByConfig({ - themeConfig: themeToGenerate, - configId, - isDarkMode: currentTheme === 'dark', - }); - } + const preferredTheme = useMemo(() => { + if (inputConfig?.preferred_theme && inputConfig?.preferred_theme !== 'default') { + return inputConfig.preferred_theme; } - if (configurable.componentIds.length === 0) return; - - configurable.componentIds.forEach((componentId) => { - const componentConfig = (config.customizations as { [key: string]: { theme: Theme } })?.[ - `${configurable.pageId}/${componentId}/*` - ]?.theme; - if (componentConfig) { - const themeToGenerate = - currentTheme === 'light' ? componentConfig.light : componentConfig.dark; - - const configId = - configurable.pageId.replace('_', '-') + '-' + componentId.replace('_', '-'); - - if (themeToGenerate) { - generatePaletteByConfig({ - themeConfig: themeToGenerate, - configId, - isDarkMode: currentTheme === 'dark', - }); - } - } - }); - }); -}; + return 'default'; + }, [inputConfig?.preferred_theme, currentTheme]); + + const generatedLightColors = (() => { + if (inputThemeConfig?.light) { + const lightThemeConfig = inputThemeConfig?.light || defaultConfig.theme.light; + + const lightPrimary = generateShades(lightThemeConfig.primary_color); + const lightSecondary = generateShades(lightThemeConfig.secondary_color); + return { + '--asc-color-primary-default': lightThemeConfig.primary_color, + '--asc-color-primary-shade1': lightPrimary[0], + '--asc-color-primary-shade2': lightPrimary[1], + '--asc-color-primary-shade3': lightPrimary[2], + '--asc-color-primary-shade4': lightPrimary[3], + + '--asc-color-secondary-default': lightThemeConfig.secondary_color, + '--asc-color-secondary-shade1': lightSecondary[0], + '--asc-color-secondary-shade2': lightSecondary[1], + '--asc-color-secondary-shade3': lightSecondary[2], + '--asc-color-secondary-shade4': lightSecondary[3], + '--asc-color-secondary-shade5': '#f9f9fa', + + '--asc-color-alert': '#fa4d30', + '--asc-color-black': '#000000', + '--asc-color-white': '#ffffff', + + '--asc-color-base-inverse': '#000000', + + '--asc-color-base-default': '#292b32', + '--asc-color-base-shade1': '#636878', + '--asc-color-base-shade2': '#898e9e', + '--asc-color-base-shade3': '#a5a9b5', + '--asc-color-base-shade4': '#ebecef', + '--asc-color-base-shade5': '#f9f9fa', + + '--asc-color-base-background': defaultConfig.theme.light.background_color, + }; + } + + return {}; + })(); + + const generatedDarkColors = (() => { + if (inputThemeConfig?.dark) { + const darkThemeConfig = inputThemeConfig?.dark || defaultConfig.theme.dark; + const darkPrimary = generateShades(darkThemeConfig.primary_color, true); + const darkSecondary = generateShades(darkThemeConfig.secondary_color, true); + + return { + '--asc-color-primary-default': darkThemeConfig.primary_color, + '--asc-color-primary-shade1': darkPrimary[0], + '--asc-color-primary-shade2': darkPrimary[1], + '--asc-color-primary-shade3': darkPrimary[2], + '--asc-color-primary-shade4': darkPrimary[3], + + '--asc-color-secondary-default': darkThemeConfig.secondary_color, + '--asc-color-secondary-shade1': darkSecondary[0], + '--asc-color-secondary-shade2': darkSecondary[1], + '--asc-color-secondary-shade3': darkSecondary[2], + '--asc-color-secondary-shade4': darkSecondary[3], + '--asc-color-secondary-shade5': '#f9f9fa', + + '--asc-color-alert': '#fa4d30', + '--asc-color-black': '#000000', + '--asc-color-white': '#ffffff', + + '--asc-color-base-inverse': '#ffffff', + + '--asc-color-base-default': '#ebecef', + '--asc-color-base-shade1': '#a5a9b5', + '--asc-color-base-shade2': '#6e7487', + '--asc-color-base-shade3': '#40434e', + '--asc-color-base-shade4': '#292b32', + '--asc-color-base-shade5': '#f9f9fa', + + '--asc-color-base-background': defaultConfig.theme.dark.background_color, + }; + } + return {}; + })(); + + const computedTheme = preferredTheme === 'default' ? currentTheme : preferredTheme; + + return { + ...(computedTheme === 'light' ? generatedLightColors : generatedDarkColors), + } as React.CSSProperties; +} export const ThemeContext = createContext<{ currentTheme: 'light' | 'dark'; @@ -120,43 +128,20 @@ export const ThemeContext = createContext<{ }); export const ThemeProvider: React.FC = ({ children }) => { - const { config } = useCustomization(); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>( - config?.preferred_theme && config?.preferred_theme !== 'default' - ? config.preferred_theme - : mediaQuery.matches - ? 'dark' - : 'light', + mediaQuery.matches ? 'dark' : 'light', ); useEffect(() => { - if (!config) return; - const themeToGenerate = currentTheme === 'light' ? config.theme?.light : config.theme?.dark; - - if (themeToGenerate) { - generatePaletteByConfig({ - themeConfig: themeToGenerate, - isDarkMode: currentTheme === 'dark', - }); - } - - generateComponentPalette(config, currentTheme); - }, [currentTheme, config]); - - useEffect(() => { - if (!config) return; + const handleChange = (e: MediaQueryListEvent) => { + setCurrentTheme(e.matches ? 'dark' : 'light'); + }; - if (config.preferred_theme === 'default') { - const handleChange = (e: MediaQueryListEvent) => { - setCurrentTheme(e.matches ? 'dark' : 'light'); - }; - - mediaQuery.addEventListener('change', handleChange); - return () => mediaQuery.removeEventListener('change', handleChange); - } - }, [config?.preferred_theme]); + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); const toggleTheme = () => { setCurrentTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); @@ -166,3 +151,9 @@ export const ThemeProvider: React.FC = ({ children }) => { {children} ); }; + +export const useTheme = () => { + const { currentTheme } = useContext(ThemeContext); + + return currentTheme; +}; diff --git a/src/v4/social/components/CommentTray/CommentTray.tsx b/src/v4/social/components/CommentTray/CommentTray.tsx index 5dd1d940d..0e8c62583 100644 --- a/src/v4/social/components/CommentTray/CommentTray.tsx +++ b/src/v4/social/components/CommentTray/CommentTray.tsx @@ -42,6 +42,7 @@ export const CommentTray = ({ onClickReply={onClickReply} shouldAllowInteraction={shouldAllowInteraction} limit={REPLIES_PER_PAGE} + includeDeleted />
diff --git a/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.module.css b/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.module.css index 5e5e68d50..de86cacdf 100644 --- a/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.module.css +++ b/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.module.css @@ -1,5 +1,5 @@ .hyperlinkFormContainer { - border-radius: var(--asc-border-radius-md); + background-color: transparent; } .form { @@ -8,6 +8,44 @@ gap: var(--asc-spacing-l1); } +.bottomSheet { +} + +.bottomSheet .react-modal-sheet-backdrop { + background-color: rgba(0, 0, 0, 0.5); +} + +.bottomSheet .react-modal-sheet-container { + background-color: white; + border-top-left-radius: 16px; + border-top-right-radius: 16px; + max-height: 80%; + overflow: auto; +} + +.bottomSheet .react-modal-sheet-content { + background-color: var(--asc-color-base-background); +} + +.bottomSheet .react-modal-sheet-header { + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.bottomSheet .react-modal-sheet-drag-indicator { + width: 40px; + height: 4px; + background-color: #ccc; + border-radius: 2px; + margin-bottom: 8px; +} + +.bottomSheet .react-modal-sheet-content { + padding: 16px; +} + .inputContainer { display: flex; flex-direction: column; @@ -73,10 +111,18 @@ color: var(--asc-color-base-default); } -.styledSecondaryButton { +.cancelButton { border: none; color: var(--asc-color-base-default); - background: var(--asc-color-base-background); +} + +.doneButton { + border: none; +} + +.doneButton:disabled { + cursor: not-allowed; + color: var(--asc-color-primary-shade2); } .removeIcon { diff --git a/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.tsx b/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.tsx index 26b7f13d2..e806c0d35 100644 --- a/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.tsx +++ b/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.tsx @@ -14,7 +14,7 @@ import { useConfirmContext } from '~/v4/core/providers/ConfirmProvider'; import { Button } from '~/v4/core/components/Button'; interface HyperLinkConfigProps { - pageId: '*'; + pageId: string; isHaveHyperLink: boolean; isOpen: boolean; onClose: () => void; @@ -33,10 +33,12 @@ export const HyperLinkConfig = ({ onRemove, }: HyperLinkConfigProps) => { const { confirm } = useConfirmContext(); + const componentId = 'hyper_link_config_component'; - const { getConfig } = useCustomization(); + const { getConfig, isExcluded } = useCustomization(); + const componentConfig = getConfig(`${pageId}/${componentId}/*`); - const componentTheme = componentConfig?.theme.light || {}; + const componentTheme = componentConfig?.theme?.light || {}; const cancelButtonConfig = getConfig(`*/hyper_link_config_component/cancel_button`); const doneButtonConfig = getConfig(`*/hyper_link_config_component/done_button`); @@ -45,11 +47,43 @@ export const HyperLinkConfig = ({ const { client } = useSDK(); const schema = z.object({ - url: z.string().refine(async (value) => { - if (!value) return true; - const hasWhitelistedUrls = await client?.validateUrls([value]).catch(() => false); - return hasWhitelistedUrls; - }, formatMessage({ id: 'storyCreation.hyperlink.validation.error.whitelisted' })), + url: z + .string() + .refine( + (value) => { + if (!value) return true; + try { + const urlObj = new URL(value); + return ['http:', 'https:'].includes(urlObj.protocol); + } catch (error) { + // Check if the value starts with "www." + if (value.startsWith('www.')) { + try { + const urlObj = new URL(`https://${value}`); + return ['http:', 'https:'].includes(urlObj.protocol); + } catch (error) { + return false; + } + } + return false; + } + }, + { + message: formatMessage({ id: 'storyCreation.hyperlink.validation.error.invalidUrl' }), + }, + ) + .refine( + async (value) => { + if (!value) return true; + // Prepend "https://" to the value if it starts with "www." + const urlToValidate = value.startsWith('www.') ? `https://${value}` : value; + const hasWhitelistedUrls = await client?.validateUrls([urlToValidate]).catch(() => false); + return hasWhitelistedUrls; + }, + { + message: formatMessage({ id: 'storyCreation.hyperlink.validation.error.whitelisted' }), + }, + ), customText: z .string() .optional() @@ -63,10 +97,12 @@ export const HyperLinkConfig = ({ type HyperLinkFormInputs = z.infer; const { + trigger, watch, register, handleSubmit, formState: { errors }, + reset, } = useForm({ resolver: zodResolver(schema), }); @@ -77,6 +113,7 @@ export const HyperLinkConfig = ({ }; const confirmDiscardHyperlink = () => { + reset(); onRemove(); onClose(); }; @@ -98,31 +135,34 @@ export const HyperLinkConfig = ({ rootId="asc-uikit-create-story" isOpen={isOpen} onClose={onClose} + className={styles.bottomSheet} >
- {formatMessage({ id: 'storyCreation.hyperlink.bottomSheet.title' })} @@ -148,7 +188,9 @@ export const HyperLinkConfig = ({ id="asc-uikit-hyperlink-input-url" placeholder={formatMessage({ id: 'storyCreation.hyperlink.form.urlPlaceholder' })} className={clsx(styles.input, errors?.url && styles.hasError)} - {...register('url')} + {...register('url', { + onChange: () => trigger('url'), + })} /> {errors?.url && {errors?.url?.message}}
diff --git a/src/v4/social/elements/ActionButton/ActionButton.module.css b/src/v4/social/elements/ActionButton/ActionButton.module.css new file mode 100644 index 000000000..5cca3c61b --- /dev/null +++ b/src/v4/social/elements/ActionButton/ActionButton.module.css @@ -0,0 +1,15 @@ +.actionButton { + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + border: none; + background-color: rgba(0, 0, 0, 0.5); + color: var(--base-inverse-default); + cursor: pointer; + padding: 0.1875rem 0rem; + border-radius: 50%; + transition: background-color 0.3s; + flex-shrink: 0; +} diff --git a/src/v4/social/elements/ActionButton/ActionButton.tsx b/src/v4/social/elements/ActionButton/ActionButton.tsx index 12b2446ff..f32980862 100644 --- a/src/v4/social/elements/ActionButton/ActionButton.tsx +++ b/src/v4/social/elements/ActionButton/ActionButton.tsx @@ -1,26 +1,15 @@ import React from 'react'; -import styled from 'styled-components'; +import clsx from 'clsx'; +import styles from './ActionButton.module.css'; interface ActionButtonProps extends React.ButtonHTMLAttributes { icon: React.ReactNode; } -const StyledActionButton = styled.button` - width: 2rem; - height: 2rem; - display: flex; - align-items: center; - justify-content: center; - border: none; - background-color: ${({ theme }) => theme.v4.colors.actionButton.default}; - color: ${({ theme }) => theme.v4.colors.baseInverse.default}; - cursor: pointer; - padding: 0.1875rem 0rem; - border-radius: 50%; - transition: background-color 0.3s; - flex-shrink: 0; -`; - -export const ActionButton: React.FC = ({ icon, ...rest }) => { - return {icon}; +export const ActionButton: React.FC = ({ icon, className, ...rest }) => { + return ( + + ); }; diff --git a/src/v4/social/elements/HyperLink/HyperLink.module.css b/src/v4/social/elements/HyperLink/HyperLink.module.css index e16c56d0e..0fca5ea50 100644 --- a/src/v4/social/elements/HyperLink/HyperLink.module.css +++ b/src/v4/social/elements/HyperLink/HyperLink.module.css @@ -15,6 +15,7 @@ max-width: 200px; border: var(--asc-color-base-shade4); text-decoration: none; + cursor: pointer; } .hyperlinkIcon { @@ -29,7 +30,7 @@ align-items: center; max-width: calc(100% - 2rem); overflow: hidden; - color: var(--asc-color-secondary-shade4); + color: var(--asc-color-secondary-default); } .text { diff --git a/src/v4/social/elements/HyperLink/HyperLink.tsx b/src/v4/social/elements/HyperLink/HyperLink.tsx index e4dff3e56..f3252c1b0 100644 --- a/src/v4/social/elements/HyperLink/HyperLink.tsx +++ b/src/v4/social/elements/HyperLink/HyperLink.tsx @@ -3,7 +3,7 @@ import { LinkIcon } from '~/v4/social/icons'; import styles from './HyperLink.module.css'; interface LinkButtonProps extends React.AnchorHTMLAttributes { - href: string; + href?: string; } export const HyperLink: React.FC = ({ href, children, ...rest }) => { diff --git a/src/v4/social/elements/HyperLinkButton/HyperLinkButton.tsx b/src/v4/social/elements/HyperLinkButton/HyperLinkButton.tsx index 51d784007..c7b005af1 100644 --- a/src/v4/social/elements/HyperLinkButton/HyperLinkButton.tsx +++ b/src/v4/social/elements/HyperLinkButton/HyperLinkButton.tsx @@ -1,50 +1,34 @@ import React from 'react'; -import { useTheme } from 'styled-components'; import { ActionButton } from '../ActionButton'; import { LinkIcon } from '~/icons'; -import { useCustomization } from '~/v4/core/providers/CustomizationProvider'; +import { useAmityElement } from '~/v4/core/hooks/uikit/index'; -interface BackButtonProps { - pageId: 'create_story_page'; - componentId: '*'; +interface HyperLinkButtonProps { + pageId?: string; + componentId?: string; onClick: (e: React.MouseEvent) => void; } export const HyperLinkButton = ({ - pageId = 'create_story_page', + pageId = '*', componentId = '*', onClick = () => {}, -}: BackButtonProps) => { - const theme = useTheme(); +}: HyperLinkButtonProps) => { const elementId = 'story_hyperlink_button'; - const { getConfig, isExcluded } = useCustomization(); - const elementConfig = getConfig(`${pageId}/${componentId}/${elementId}`); - const backgroundColor = elementConfig?.background_color; - const customIcon = elementConfig?.hyperlink_button_icon; - const isElementExcluded = isExcluded(`${pageId}/${componentId}/${elementId}`); + const { accessibilityId, config, defaultConfig, isExcluded, uiReference, themeStyles } = + useAmityElement({ + pageId, + componentId, + elementId, + }); - if (isElementExcluded) return null; - - const renderIcon = () => { - if (customIcon) { - if (customIcon.startsWith('http://') || customIcon.startsWith('https://')) { - return ( - {elementId} - ); - } - } - - return ; - }; + if (isExcluded) return null; return ( } onClick={onClick} - style={{ - backgroundColor: backgroundColor || theme.v4.colors.actionButton.default, - }} /> ); }; diff --git a/src/v4/social/elements/ShareStoryButton/ShareStoryButton.tsx b/src/v4/social/elements/ShareStoryButton/ShareStoryButton.tsx index 3b66ac7b8..297d0dacc 100644 --- a/src/v4/social/elements/ShareStoryButton/ShareStoryButton.tsx +++ b/src/v4/social/elements/ShareStoryButton/ShareStoryButton.tsx @@ -4,7 +4,7 @@ import { useIntl } from 'react-intl'; import { isValidHttpUrl } from '~/utils'; import { useCustomization } from '~/v4/core/providers/CustomizationProvider'; import { Icon } from '~/v4/core/components/Icon'; -import { backgroundImage as communityBackgroundImage } from '~/icons/Community'; +import { backgroundImage as communityBackgroundImage } from '~/v4/icons/Community'; import styles from './ShareStoryButton.module.css'; import { Avatar } from '~/v4/core/components'; diff --git a/src/v4/social/hooks/collections/useCommentsCollection.ts b/src/v4/social/hooks/collections/useCommentsCollection.ts new file mode 100644 index 000000000..b642f9be5 --- /dev/null +++ b/src/v4/social/hooks/collections/useCommentsCollection.ts @@ -0,0 +1,38 @@ +import { CommentRepository } from '@amityco/ts-sdk'; + +import useLiveCollection from '~/core/hooks/useLiveCollection'; + +type useCommentsParams = { + parentId?: string | null; + referenceId?: string | null; + referenceType: Amity.CommentReferenceType; + limit?: number; + shouldCall?: () => boolean; + includeDeleted?: boolean; +}; + +export default function useCommentsCollection({ + parentId, + referenceId, + referenceType, + limit = 10, + shouldCall = () => true, + includeDeleted = false, +}: useCommentsParams) { + const { items, ...rest } = useLiveCollection({ + fetcher: CommentRepository.getComments, + params: { + parentId, + referenceId: referenceId as string, + referenceType, + limit, + includeDeleted, + }, + shouldCall: () => shouldCall() && !!referenceId && !!referenceType, + }); + + return { + comments: items, + ...rest, + }; +} diff --git a/src/v4/social/hooks/collections/useCommunityMembersCollection.tsx b/src/v4/social/hooks/collections/useCommunityMembersCollection.tsx new file mode 100644 index 000000000..be5668978 --- /dev/null +++ b/src/v4/social/hooks/collections/useCommunityMembersCollection.tsx @@ -0,0 +1,15 @@ +import { CommunityRepository } from '@amityco/ts-sdk'; +import useLiveCollection from '~/core/hooks/useLiveCollection'; + +export default function useCommunityMembersCollection(communityId?: string, limit: number = 5) { + const { items, ...rest } = useLiveCollection({ + fetcher: CommunityRepository.Membership.getMembers, + params: { communityId: communityId as string, limit, memberships: ['member'] }, + shouldCall: () => !!communityId, + }); + + return { + members: items, + ...rest, + }; +} diff --git a/src/v4/social/hooks/index.ts b/src/v4/social/hooks/index.ts index 38695fed4..e19849fde 100644 --- a/src/v4/social/hooks/index.ts +++ b/src/v4/social/hooks/index.ts @@ -1,2 +1,3 @@ export * from './collections/useReactionsCollection'; export { useGetActiveStoriesByTarget } from './useGetActiveStories'; +export { useCommentFlaggedByMe } from './useCommentFlaggedByMe'; diff --git a/src/v4/social/hooks/useCommentFlaggedByMe.tsx b/src/v4/social/hooks/useCommentFlaggedByMe.tsx new file mode 100644 index 000000000..f318ebd33 --- /dev/null +++ b/src/v4/social/hooks/useCommentFlaggedByMe.tsx @@ -0,0 +1,71 @@ +import { CommentRepository } from '@amityco/ts-sdk'; +import { useQuery } from '@tanstack/react-query'; +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useNotifications } from '~/v4/core/providers/NotificationProvider'; + +export const useCommentFlaggedByMe = (commentId?: string) => { + const notification = useNotifications(); + const [isFlaggedByMe, setIsFlaggedByMe] = useState(false); + + const { data, isLoading, refetch } = useQuery({ + queryKey: ['asc-uikit', 'CommentRepository', 'isCommentFlaggedByMe', commentId], + queryFn: () => { + return CommentRepository.isCommentFlaggedByMe(commentId as string); + }, + enabled: commentId != null, + }); + + useEffect(() => { + if (data != null) { + setIsFlaggedByMe(data); + } + }, [data]); + + const flagComment = async () => { + if (commentId == null) return; + try { + setIsFlaggedByMe(true); + await CommentRepository.flagComment(commentId); + } catch (_error) { + setIsFlaggedByMe(false); + } finally { + refetch(); + } + }; + + const unflagComment = async () => { + if (commentId == null) return; + try { + setIsFlaggedByMe(false); + await CommentRepository.unflagComment(commentId); + } catch (_error) { + setIsFlaggedByMe(true); + } finally { + refetch(); + } + }; + + const toggleFlagComment = async () => { + if (commentId == null) return; + if (isFlaggedByMe) { + await unflagComment(); + notification.success({ + content: , + }); + } else { + await flagComment(); + notification.success({ + content: , + }); + } + }; + + return { + isLoading, + isFlaggedByMe, + flagComment, + unflagComment, + toggleFlagComment, + }; +}; diff --git a/src/v4/social/hooks/useGetStoryByStoryId.tsx b/src/v4/social/hooks/useGetStoryByStoryId.tsx new file mode 100644 index 000000000..403a6b911 --- /dev/null +++ b/src/v4/social/hooks/useGetStoryByStoryId.tsx @@ -0,0 +1,15 @@ +import { StoryRepository } from '@amityco/ts-sdk'; + +import useLiveObject from '~/core/hooks/useLiveObject'; + +const useGetStoryByStoryId = (storyId: string | undefined) => { + const story = useLiveObject({ + fetcher: StoryRepository.getStoryByStoryId, + params: storyId, + shouldCall: () => !!storyId, + }); + + return story; +}; + +export default useGetStoryByStoryId; diff --git a/src/v4/social/icons/index.ts b/src/v4/social/icons/index.ts index ee745d9a9..bbc1d791b 100644 --- a/src/v4/social/icons/index.ts +++ b/src/v4/social/icons/index.ts @@ -6,3 +6,4 @@ export { default as TrashIcon } from './trash'; export { default as FlagIcon } from './flag'; export { default as ChevronDownIcon } from './chevron_down'; export { default as LinkIcon } from './link'; +export { default as MinusCircleIcon } from './minus_circle'; diff --git a/src/v4/social/icons/minus_circle.tsx b/src/v4/social/icons/minus_circle.tsx new file mode 100644 index 000000000..852e2252d --- /dev/null +++ b/src/v4/social/icons/minus_circle.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +function Icon(props: React.SVGProps) { + return ( + + + + ); +} + +export default Icon; diff --git a/src/v4/social/internal-components/Comment/Comment.module.css b/src/v4/social/internal-components/Comment/Comment.module.css index 327387304..e7bf63915 100644 --- a/src/v4/social/internal-components/Comment/Comment.module.css +++ b/src/v4/social/internal-components/Comment/Comment.module.css @@ -19,3 +19,21 @@ .replyContainer { margin-left: 3.25rem; } + +.deletedCommentBlock { + display: flex; + justify-content: flex-start; + align-items: center; + gap: var(--asc-spacing-s2); + padding: var(--asc-spacing-s2) var(--asc-spacing-m1); + border-radius: var(--asc-border-radius-sm); + text-align: center; + color: var(--asc-color-base-shade2); + background-color: var(--asc-color-base-background); + border-top: 1px solid var(--asc-color-base-shade4); +} + +.deletedCommentBlock:first-child { + padding: 0 var(--asc-spacing-m1) var(--asc-spacing-s2) var(--asc-spacing-m1); + border-top: none; +} diff --git a/src/v4/social/internal-components/Comment/UIComment.module.css b/src/v4/social/internal-components/Comment/UIComment.module.css index 879e00e65..fe9e36cd3 100644 --- a/src/v4/social/internal-components/Comment/UIComment.module.css +++ b/src/v4/social/internal-components/Comment/UIComment.module.css @@ -127,6 +127,7 @@ .reactionListButton { border: none; color: var(--asc-color-primary-default); + padding: 0; } .content { @@ -143,3 +144,11 @@ gap: 0.125rem; color: var(--asc-color-base-default); } + +.deletedCommentBlock { + padding: var(--asc-spacing-m1); + background-color: var(--asc-color-base-background); + border-radius: var(--asc-border-radius-sm); + text-align: center; + color: var(--asc-color-base-shade2); +} diff --git a/src/v4/social/internal-components/Comment/UIComment.tsx b/src/v4/social/internal-components/Comment/UIComment.tsx index 8ded27d94..927ff22d1 100644 --- a/src/v4/social/internal-components/Comment/UIComment.tsx +++ b/src/v4/social/internal-components/Comment/UIComment.tsx @@ -59,6 +59,7 @@ interface StyledCommentProps { }[]; }) => void; queryMentionees: QueryMentioneesFnType; + isMember?: boolean; isLiked?: boolean; isReported?: boolean; isReplyComment?: boolean; @@ -94,6 +95,7 @@ const UIComment = ({ isEditing, onChange, queryMentionees, + isMember = false, isBanned, isLiked, mentionees, @@ -145,8 +147,8 @@ const UIComment = ({
)} - {!isEditing && (canLike || canReply || options.length > 0) && ( -
+
+ {!isEditing && isMember && (canLike || canReply || options.length > 0) && (
{formatTimeAgo(createdAt)} @@ -188,36 +190,36 @@ const UIComment = ({
- {reactionsCount > 0 && ( - - )} -
- )} +
+ + )} +
); diff --git a/src/v4/social/internal-components/Comment/index.tsx b/src/v4/social/internal-components/Comment/index.tsx index 4f12418fe..00fc0a068 100644 --- a/src/v4/social/internal-components/Comment/index.tsx +++ b/src/v4/social/internal-components/Comment/index.tsx @@ -6,9 +6,9 @@ import useMention from '~/v4/chat/hooks/useMention'; import { extractMetadata, + isCommunityMember, isNonNullable, Mentioned, - Mentionees, Metadata, parseMentionsMarkup, } from '~/v4/helpers/utils'; @@ -17,7 +17,6 @@ import useSDK from '~/core/hooks/useSDK'; import useUser from '~/core/hooks/useUser'; import { CommentRepository, ReactionRepository } from '@amityco/ts-sdk'; -import useCommentFlaggedByMe from '~/social/hooks/useCommentFlaggedByMe'; import useCommentPermission from '~/social/hooks/useCommentPermission'; import useCommentSubscription from '~/social/hooks/useCommentSubscription'; import useImage from '~/core/hooks/useImage'; @@ -34,8 +33,10 @@ import { useNotifications } from '~/v4/core/providers/NotificationProvider'; import { Button, BottomSheet, Typography } from '~/v4/core/components'; import styles from './Comment.module.css'; -import { TrashIcon, PenIcon, FlagIcon } from '~/v4/social/icons'; +import { TrashIcon, PenIcon, FlagIcon, MinusCircleIcon } from '~/v4/social/icons'; import { LoadingIndicator } from '~/v4/social/internal-components/LoadingIndicator'; +import useCommunityMembersCollection from '~/v4/social/hooks/collections/useCommunityMembersCollection'; +import { useCommentFlaggedByMe } from '~/v4/social/hooks'; const REPLIES_PER_PAGE = 5; @@ -63,6 +64,8 @@ interface CommentProps { export const Comment = ({ commentId, readonly, onClickReply }: CommentProps) => { const comment = useComment(commentId); const story = useGetStoryByStoryId(comment?.referenceId); + const { members } = useCommunityMembersCollection(story?.community?.communityId); + const [bottomSheet, setBottomSheet] = useState(false); const [selectedCommentId, setSelectedCommentId] = useState(''); const { confirm } = useConfirmContext(); @@ -184,6 +187,10 @@ export const Comment = ({ commentId, readonly, onClickReply }: CommentProps) => }); }; + const { currentUserId } = useSDK(); + const currentMember = members.find((member) => member.userId === currentUserId); + const isMember = isCommunityMember(currentMember); + const options = [ canEdit ? { @@ -216,6 +223,15 @@ export const Comment = ({ commentId, readonly, onClickReply }: CommentProps) => if (comment == null) return null; + if (comment?.isDeleted) { + return isReplyComment ? null : ( +
+ + +
+ ); + } + const renderedComment = ( isLiked={isLiked} isReported={isFlaggedByMe} isReplyComment={isReplyComment} + isMember={isMember} onChange={onChange} onClickOverflowMenu={toggleBottomSheet} options={options} @@ -312,7 +329,7 @@ export const Comment = ({ commentId, readonly, onClickReply }: CommentProps) => mountPoint={document.getElementById('asc-uikit-stories-viewer') as HTMLElement} detent="full-height" > - + ); diff --git a/src/v4/social/internal-components/CommentList/CommentList.tsx b/src/v4/social/internal-components/CommentList/CommentList.tsx index 00718965e..2582dbd8a 100644 --- a/src/v4/social/internal-components/CommentList/CommentList.tsx +++ b/src/v4/social/internal-components/CommentList/CommentList.tsx @@ -1,11 +1,11 @@ import React, { memo } from 'react'; import { useIntl } from 'react-intl'; -import useCommentsCollection from '~/social/hooks/collections/useCommentsCollection'; + import { Comment } from '../Comment'; import styles from './CommentList.module.css'; import { ExpandIcon } from '~/v4/social/icons'; -import { Button } from '~/v4/core/components'; import { LoadMoreWrapper } from '~/v4/core/components/LoadMoreWrapper/LoadMoreWrapper'; +import useCommentsCollection from '~/v4/social/hooks/collections/useCommentsCollection'; interface CommentListProps { parentId?: string; @@ -17,6 +17,7 @@ interface CommentListProps { onClickReply?: (comment: Amity.Comment) => void; style?: React.CSSProperties; shouldAllowInteraction?: boolean; + includeDeleted?: boolean; } export const CommentList = ({ @@ -28,12 +29,14 @@ export const CommentList = ({ isExpanded = true, onClickReply, shouldAllowInteraction, + includeDeleted = false, }: CommentListProps) => { const { comments, hasMore, loadMore } = useCommentsCollection({ parentId, referenceId, referenceType, limit, + includeDeleted, }); const { formatMessage } = useIntl(); diff --git a/src/v4/social/internal-components/StoryCommentComposeBar/StoryCommentComposeBar.tsx b/src/v4/social/internal-components/StoryCommentComposeBar/StoryCommentComposeBar.tsx index 3b3c5878d..455a4ac17 100644 --- a/src/v4/social/internal-components/StoryCommentComposeBar/StoryCommentComposeBar.tsx +++ b/src/v4/social/internal-components/StoryCommentComposeBar/StoryCommentComposeBar.tsx @@ -60,38 +60,11 @@ export const StoryCommentComposeBar = ({ }); }; - if (isJoined && shouldAllowCreation) { - return ( - <> - {isReplying && ( -
-
- {' '} - {replyTo?.userId} -
- -
- )} - {!isReplying ? ( - handleAddComment(text, mentionees, metadata)} - /> - ) : ( - { - handleReplyToComment(replyText, mentionees, metadata); - onCancelReply?.(); - }} - /> - )} - - ); + if (!isJoined) { + return null; } - if (isJoined && shouldAllowCreation) { + if (!shouldAllowCreation) { return (
@@ -100,5 +73,32 @@ export const StoryCommentComposeBar = ({ ); } - return null; + return ( + <> + {isReplying && ( +
+
+ {' '} + {replyTo?.userId} +
+ +
+ )} + {!isReplying ? ( + handleAddComment(text, mentionees, metadata)} + /> + ) : ( + { + handleReplyToComment(replyText, mentionees, metadata); + onCancelReply?.(); + }} + /> + )} + + ); }; diff --git a/src/v4/social/internal-components/StoryViewer/Renderers/Image.tsx b/src/v4/social/internal-components/StoryViewer/Renderers/Image.tsx index 55612a5cd..62fad1c09 100644 --- a/src/v4/social/internal-components/StoryViewer/Renderers/Image.tsx +++ b/src/v4/social/internal-components/StoryViewer/Renderers/Image.tsx @@ -225,7 +225,6 @@ export const renderer: CustomRenderer = ({ story, action, config }) => { className={styles.actionButton} onClick={() => { bottomSheetAction.action(); - closeBottomSheet(); }} variant="secondary" > diff --git a/src/v4/social/internal-components/StoryViewer/Renderers/Video.tsx b/src/v4/social/internal-components/StoryViewer/Renderers/Video.tsx index f2f7bafa7..f536ea8c0 100644 --- a/src/v4/social/internal-components/StoryViewer/Renderers/Video.tsx +++ b/src/v4/social/internal-components/StoryViewer/Renderers/Video.tsx @@ -266,7 +266,6 @@ export const renderer: CustomRenderer = ({ story, action, config, messageHandler className={rendererStyles.actionButton} onClick={() => { bottomSheetAction.action(); - closeBottomSheet(); }} variant="secondary" > @@ -335,12 +334,6 @@ const storyContentStyles = { position: 'relative' as const, }; -const videoContainerStyles = { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}; - export const tester: Tester = (story) => { return { condition: story.type === 'video', diff --git a/src/v4/social/pages/DraftsPage/DraftsPage.tsx b/src/v4/social/pages/DraftsPage/DraftsPage.tsx index 70163b802..09ba86a42 100644 --- a/src/v4/social/pages/DraftsPage/DraftsPage.tsx +++ b/src/v4/social/pages/DraftsPage/DraftsPage.tsx @@ -5,7 +5,6 @@ import { readFileAsync } from '~/helpers'; import styles from './DraftsPage.module.css'; import { SubmitHandler } from 'react-hook-form'; -import Truncate from 'react-truncate-markup'; import { usePageBehavior } from '~/v4/core/providers/PageBehaviorProvider'; import { @@ -52,7 +51,15 @@ const AmityDraftStoryPage = ({ targetId, targetType, mediaType }: AmityDraftStor data: { url: string; customText: string }; type: Amity.StoryItemType; }[] - >([]); + >([ + { + data: { + url: '', + customText: '', + }, + type: 'hyperlink' as Amity.StoryItemType, + }, + ]); const handleHyperLinkBottomSheetClose = () => { setIsHyperLinkBottomSheetOpen(false); @@ -164,6 +171,16 @@ const AmityDraftStoryPage = ({ targetId, targetType, mediaType }: AmityDraftStor ]); }; + const handleOnClickHyperLinkActionButton = () => { + if (hyperLink[0]?.data?.url) { + notification.info({ + content: formatMessage({ id: 'storyDraft.notification.hyperlink.error' }), + }); + return; + } + setIsHyperLinkBottomSheetOpen(true); + }; + useEffect(() => { const extractColorsFromImage = async (fileTarget: File) => { const img = await readFileAsync(fileTarget); @@ -211,7 +228,7 @@ const AmityDraftStoryPage = ({ targetId, targetType, mediaType }: AmityDraftStor setIsHyperLinkBottomSheetOpen(true)} + onClick={handleOnClickHyperLinkActionButton} />
@@ -250,15 +267,7 @@ const AmityDraftStoryPage = ({ targetId, targetType, mediaType }: AmityDraftStor ) : null} {hyperLink[0]?.data?.url && (
- + setIsHyperLinkBottomSheetOpen(true)}> {hyperLink[0]?.data?.customText || hyperLink[0].data.url.replace(/^https?:\/\//, '')}
@@ -270,7 +279,7 @@ const AmityDraftStoryPage = ({ targetId, targetType, mediaType }: AmityDraftStor onClose={handleHyperLinkBottomSheetClose} onSubmit={onSubmitHyperLink} onRemove={onRemoveHyperLink} - isHaveHyperLink={hyperLink.length > 0} + isHaveHyperLink={hyperLink?.[0]?.data?.url !== ''} />
@@ -278,7 +287,7 @@ const AmityDraftStoryPage = ({ targetId, targetType, mediaType }: AmityDraftStor pageId="create_story_page" componentId="*" onClick={() => - onCreateStory(file, imageMode, {}, hyperLink.length > 0 ? hyperLink : []) + onCreateStory(file, imageMode, {}, hyperLink[0]?.data?.url ? hyperLink : []) } avatar={community.avatarFileUrl} /> diff --git a/src/v4/social/pages/StoryPage/CommunityFeedStory.tsx b/src/v4/social/pages/StoryPage/CommunityFeedStory.tsx index 159309e4f..250e7ae11 100644 --- a/src/v4/social/pages/StoryPage/CommunityFeedStory.tsx +++ b/src/v4/social/pages/StoryPage/CommunityFeedStory.tsx @@ -91,9 +91,7 @@ export const CommunityFeedStory = ({ communityId }: CommunityFeedStoryProps) => okText: formatMessage({ id: 'delete' }), onOk: async () => { previousStory(); - if (isLastStory) { - onBack(); - } + if (isLastStory) onBack(); await StoryRepository.softDeleteStory(storyId); notification.success({ content: formatMessage({ id: 'storyViewer.notification.deleted' }), diff --git a/src/v4/social/pages/StoryPage/GlobalFeedStory.tsx b/src/v4/social/pages/StoryPage/GlobalFeedStory.tsx index f5109aa64..4404ef090 100644 --- a/src/v4/social/pages/StoryPage/GlobalFeedStory.tsx +++ b/src/v4/social/pages/StoryPage/GlobalFeedStory.tsx @@ -90,9 +90,7 @@ export const GlobalFeedStory: React.FC = () => { okText: formatMessage({ id: 'delete' }), onOk: async () => { previousStory(); - if (isLastStory) { - onChangePage(PageTypes.NewsFeed); - } + if (isLastStory) onChangePage(PageTypes.NewsFeed); await StoryRepository.softDeleteStory(storyId); notification.success({ content: formatMessage({ id: 'storyViewer.notification.deleted' }), diff --git a/src/v4/social/pages/StoryPage/ViewStoryPage.tsx b/src/v4/social/pages/StoryPage/ViewStoryPage.tsx index 2a452ef3c..1551ac395 100644 --- a/src/v4/social/pages/StoryPage/ViewStoryPage.tsx +++ b/src/v4/social/pages/StoryPage/ViewStoryPage.tsx @@ -13,24 +13,12 @@ interface AmityViewStoryPageProps { const AmityViewStoryPage: React.FC = ({ type }) => { const { page } = useNavigation(); - const renderContent = () => { - switch (type) { - case 'communityFeed': - if (page.type === PageTypes.ViewStory && page.targetId) { - return ; - } - return null; - case 'globalFeed': - if (page.type === PageTypes.ViewStory && page.targetId) { - return ; - } - return null; - default: - return null; - } - }; + if (page.type !== PageTypes.ViewStory || !page.targetId) return null; - return
{renderContent()}
; + if (type === 'communityFeed') return ; + if (type === 'globalFeed') return ; + + return null; }; export default AmityViewStoryPage;