From 35a204ccb13415c1c1f1a6f3c4af0498ff206d5a Mon Sep 17 00:00:00 2001 From: "Pitchaya T." <33589608+ptchayap@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:12:40 +0700 Subject: [PATCH] Release/v4.0.0 beta.15 (#83) * fix: to support un config value (#340) * fix: hyperlink long text (#335) * fix: hyperlink custom text input max length (#336) * fix: ASC-220001 - share story button (#337) * fix: share story button * fix: use avatar v4 * fix: remove unused * fix: ASC-21590 - hyperlink ui (#347) * fix: hyperlink long text * fix: story hyperlink * fix: story commu condition (#346) * feat: ASC-22133 - custom reaction provider (#325) * feat: add custom reaction provider * feat: add to use reactionsContext * fix: rename provider * feat: ASC-22127 - reaction preview message bubble (#331) * feat: add reaction preview container * feat: add abbreviateCount function * feat: add fallback reaction * feat: update pr --------- Co-authored-by: Kiattirat S * feat: ASC-22124 - message reaction picker (#332) * feat: add reaction container and button * feat: add active and hover state * fix: storybook display * fix: remove unused * feat: add checking for active state * feat: add open state on reaction picker * feat: add hover state on Quick reaction button * feat: seperate components * feat: close picker when clicking outside * feat: add handling on click reaction button * fix: hover state * Update src/v4/chat/components/LiveChatMessageContent/MessageReaction/index.tsx * Delete src/v4/chat/hooks/useMessageReactions.ts --------- Co-authored-by: Kiattirat Sujjapongse * feat: ASC-22130 - add quick reaction (#339) * feat: add handling for quick reaction button * fix: add await to wait for add and remove reaction * fix: reaction pickert style * fix: storybook name * fix: reaction picker storybook * feat: add pageId, componentId and elementId * fix: change internal component name and export * fix: export from components * fix: message reaction components name * feat: add static file to include in storybook build * fix: remove duplicated file * fix: new design on my reaction * fix: not use z-index * fix: position of each reaction * fix: remove border on reaction preview for my reaction style * fix: remove unused * feat(reaction): ASC-22136 - reaction panel (#344) * feat: add handling for quick reaction button * fix: add await to wait for add and remove reaction * fix: reaction pickert style * fix: storybook name * fix: reaction picker storybook * feat: add pageId, componentId and elementId * fix: remove comment * feat(reaction-list): add empty files * feat(reaction): reuse reaction tab from social * feat(reaction): pass openReaction func to child * feat(reaction): render data from config * feat(reaction): update panel style * feat(reaction): update reaction list * feat(reaction): update reaction list * feat(reaction): add all state for reaction list * feat(reaction): change unit * feat(reaction): remove unused files * feat(reaction): update PR * feat(reaction): update PR * feat(reaction): update PR * fix: remove unused * feat(reaction): update PR --------- Co-authored-by: ptchaya_p Co-authored-by: Pitchaya T <33589608+ptchayap@users.noreply.github.com> --------- Co-authored-by: Kiattirat S * fix: ASC-22623 - story video (#349) * fix: story video * fix: video * fix: remove console.log * feat(reaction): update condition (#350) * fix(reaction): ASC-22611 - update missing ui and action to remove reaction from message (#348) * fix: ASC-22611 - update ui * fix(reaction): missing ui and action to remove reaction from message * fix: ASC-22611 - update ui * fix: ASC-22611 - update ui * fix(reaction): hide 0 reaction count * fix(reaction): update pr * fix(reaction): export reaction list * fix: create story condition (#352) * fix: ASC-222740 - create story condition for global-admin role (#354) * fix: create story condition * fix: condition * fix: condition * fix: condition * fix(reaction): ASC-22622 - fix reaction UI bugs (#355) * fix(reaction): update pagination * fix(reaction): use total reactor count from message / story / post / comments * fix(reaction): update ui * fix(reaction): update ui * fix: ASC-22740 - create story permission condition to create story (#356) * fix: create story condition * fix: condition * fix: condition * fix: condition * fix: condition * feat(SDK): test build * feat(SDK): upgrade ts-sdk version * fix: ASC-21529 - view story wrapper css (#359) * fix: css * refactor: view story page condition * fix: ASC-20521 - story delete condition (#360) * fix: story delete condition * fix: condition * fix: ASC-20505 - condition for non member (#361) * fix: condition for non member * fix: hyperlink config * fix: condition * fix: hook (#362) * fix: ASC-22947 - hyperlink config bottom sheet condition (#367) * fix: hyperlink action condition * fix: hyperlink css * fix: remove unused * fix: hyperlink config condition * fix: ASC-22949 - video story bug when delete (#366) * fix: video story bug when delete * fix: remove console.log * fix: condition * fix: close bottom sheet condition (#365) * fix: ASC-20502 - comment condition (#363) * fix: comment condition * fix: story comment compose bar condition * fix: ASC-22484 - hyperlink config css (#345) * fix: hyperlink config css * fix: remove unused * fix: remove unused * fix: hyperlink * fix: color bg * fix: remove console log * fix: type * fix: type * fix: hyperlink * fix: ASC-20532 - deleted comment block (#364) * fix: deleted comment block * fix: css * fix: remove unused props * fix: css var * fix: category card responsive styles (#372) * fix: ASC-22947 - story can't add hyperlink (#373) * fix: hyperlink action condition * fix: hyperlink css * fix: remove unused * fix: hyperlink config condition * fix: css * fix: ASC-00000 - bring back v3 code (#358) * Revert "fix: ASC-21481 - remove unnecessary request to prevent rate limit (#230)" This reverts commit e9c82ec82889773dc875f812e7688e4363d20569. # Conflicts: # pnpm-lock.yaml # src/core/providers/UiKitProvider/index.tsx # src/social/components/CommentList/index.tsx # src/social/components/CommentList/styles.tsx # src/social/components/EngagementBar/UIEngagementBar.tsx * fix: reply comment text * Revert "fix: ASC-21481 - remove unnecessary request to prevent rate limit (#230)" This reverts commit e9c82ec82889773dc875f812e7688e4363d20569. # Conflicts: # pnpm-lock.yaml # src/core/providers/UiKitProvider/index.tsx # src/social/components/CommentList/index.tsx # src/social/components/CommentList/styles.tsx # src/social/components/EngagementBar/UIEngagementBar.tsx * fix: reply comment text * fix: undefined .length * fix: video should pause when click item in action menu (#375) * fix: hyperlink text color (#376) * fix: ASC-21507 - reset form when confirm remove hyperlink (#368) * fix: hyperlink action condition * fix: hyperlink css * fix: remove unused * fix: hyperlink config condition * fix: reset form when confirm remove hyperlink * chore: ASC-22035 - customizations (#378) * feat: ThemeProvider, CustomizationProvider and IconComponent * chore: remove unused code * chore: useAmityComponentProps * feat: useAmityPage, useAmityComponent, useAmityElement * chore: update foundation * fix: uikit hook bug * fix: update core foundation (theme, modal) * fix: default color palette * fix: types * fix: fix isExcluded * chore: remove unused code * fix: typing * fix: fix a testing workflow * chore: Apply suggestions from code review Co-authored-by: Pitchaya T. <33589608+ptchayap@users.noreply.github.com> --------- Co-authored-by: Pitchaya T. <33589608+ptchayap@users.noreply.github.com> * fix: story tab show with member only (#383) * chore: ASC-22036 - stylelint (#379) * chore: stylelint * chore: add postcss * chore: update lint scripts * chore: ASC-22039 - browserslist (#380) * chore: setup browserslist * chore: run prettier * fix: draft video should loop (#369) * chore: ASC-22040 - node 20 (#381) * chore: add asdf and change engines field of node version to 20 * chore: pnpm v9 * chore: remove pull_request_template.md * chore: upgrade github actions * chore: update node version --------- Co-authored-by: Chaiwat Trisuwan * fix: fix a staging workflow (#389) * fix: add pnpm install step (#390) * feat: ASC-22340 - CommunitySearchResults (#391) * feat: CommunitySearchResults * fix: update avatar size * feat: ASC-22294 - PostContent (#392) * feat: PostContent * fix: lint * chore: update package.json * fix: elements/index * feat: TopSearchBar (#393) * feat: SocialHomePage (#394) * feat: SocialGlobalSearchPage (#395) * feat: MyCommunities (#396) * feat: PostDetailPage (#397) * feat: ASC-22335 - update routes (#398) * feat: update route * chore: remove unused code * chore: Drawer (#402) * fix: text overflow (#400) * chore: ASC-22335 - update SocialGlobalSearchPage route (#399) * feat: add SocialGlobalSearchPage routing * fix: update useUserQueryByDisplayName * fix: ASC-22335 - VideoViewer styles (#401) * fix: fix VideoViewer * chore: remove console.log * fix: ASC-21508 - hyperlink to show confirm when back with data (#404) * fix: hyperlink to show confirm when back with data * fix: bring bank old cold * fix: pageId no need to pass prop * fix: ASC-20694 - react story condition for non member (#370) * fix: comment condition * fix: story react condition for non member * fix: import * fix: params * fix: ASC-22312 - moderator badge (#384) * fix: moderator badge * fix: moderator badge * fix: moderator badge * fix: use css var * fix: remove unused * fix: remove package manager * fix: comment tray stories * fix: remove unused * fix: remove console.log * fix: sub story community rte * feat: ASC-22893 - create post menu & select post target page (#405) * style: change create menu color * feat: implement ui CreatePostMenu * feat: add event when click createPostMenuButton * feat: implement CreateStoryButtons * feat: implement CreatePollButton * feat: implement CreateLivestreamButton * fix: text Livestream * fix: changeable text * feat: implement SelectPostTargetPage * feat: onBack * fix: pageId * fix: text static * style: pointer * feat: integrate API communites to SelectPostTargetPage * feat: custom communities avatar size * feat: add UserAvatar * feat: apply infinity scroll * fix: map key * feat: add userId * style: change px to rem * fix: dark theme styles * reafactor: remove blank line * style: add pointer * fix: pr comments * refactor: wrap button in IconComponent * style: adapt global var * fix: optional pageId * refactor: remove comment * feat: ASC-22888 - hide create post menu for current release (#413) * feat: comment code create post menu for current release * fix: remove SelectPostTargetPage * fix: hyperlink confirm remove link (#406) * fix: ASC-23219 - story view page onClose (#408) * fix: story view page onClose * fix: to v4 * fix: removed deprecated * fix: import * fix: ASC-20522 - navigate view story in mobile overlay (#409) * fix: story view page onClose * fix: to v4 * fix: overlay bug * fix: remove console.log * fix: remove styled component * fix: add story arrow left right to customization provider * fix: remove unused * fix: import * fix: import * fix: global story hook import * fix: ASC-22720 - view story full width and height (#410) * fix: story view page onClose * fix: to v4 * fix: overlay bug * fix: remove console.log * fix: remove styled component * fix: add story arrow left right to customization provider * fix: view story full width and height * feat(message): handle optimistic on message creation (#377) * fix: ASC-22081 - notification v4 (#411) * fix: story view page onClose * fix: to v4 * fix: overlay bug * fix: remove console.log * fix: remove styled component * fix: add story arrow left right to customization provider * fix: view story full width and height * fix: notification v4 * fix: file name * fix: ASC-20521 - delete first multiple story go to next story (#412) * fix: story view page onClose * fix: to v4 * fix: overlay bug * fix: remove console.log * fix: remove styled component * fix: add story arrow left right to customization provider * fix: view story full width and height * fix: notification v4 * fix: delete first multiple stories go to next story * fix: ASC-23136 - fix social v3 issues (#388) * fix: bring back reply i18n i18n * fix: icon size * fix: comments do not load * chore: story in v3 * fix: isFlaggedByMe rate limit issue * fix: fix PageBehavior * fix: ViewStoryPage navigation * fix: remove duplicate * fix: import * fix: draft page prop * fix: draft story page storyType prop --------- Co-authored-by: Chaiwat Trisuwan * feat: ASC-00000 - tech debt livechat (#416) * fix: unconfigurable style * fix: modify avatar v4 to support defaultImage * fix: livechat avatar to use avatar component v4 * fix: user icon in message bubble to use v4 compo * fix: change defaul image for livechat * fix: use avatar v4 component on story preview * fix: to use v4 components in reply message placeholder * fix: mention item to use v4 avatar components * fix: defaultImage on share story button * fix: theme on AmityLiveChatHeader * fix: compose bar to use useAmityComponent * fix: LiveChatMessageList to use useAmityComponent * fix: remove condition variable * fix: configuration value * fix: message reaction to use useAmityElement * fix: remove unused * fix: to use same value on light and dark theme for livechat * fix: remove /index * fix: view story page context (#420) * fix: build include css (#421) * fix: ASC-23233 - disabled button when file is uploading and fix 429 issue (#422) * fix: disabled remove button when file is uploading * fix: add handle document event when menu opened * fix: ASC-23288 - disabled submit vote button (#424) * fix: disabled submit vote button * chore: remove console.log * fix: formatDuration (#429) * fix: link text color (#426) * fix: reduce comment api network call amount (#427) * fix: like button color (#425) * fix: play icon (#428) * feat: ASC-23090 - LinkPreview (#414) * feat: LinkPreview * fix: error handling * fix: types * fix: move post creator out of infinite scroll (#430) * fix: append a created poll post (#432) * fix: ASC-22315 - error noti when user upload unsupported file (#417) * fix: story tab background color * fix: update navigate * fix: styled to css module * fix: remove console.log * fix: remove console.log * fix: story tab * fix: type * fix: remove console.log * fix: story tab type * fix: noti css * fix: story avatar * fix: remove unused * fix: remove unused * fix: share story button use amityElement * fix: icon * fix: add default communityProfileImageBackground * fix: story tab community * fix: story tab * fix: navigate * fix: story navigate * fix: remove unused * fix: red border story tab * fix: import and type * fix: import and type * fix: remove error icon z-index * fix: official badge condition * fix: remove unused * fix: height * fix: story avatar * fix: share story button * fix: ASC-20535 - deleted reply block (#418) * fix: story view page onClose * fix: to v4 * fix: overlay bug * fix: remove console.log * fix: remove styled component * fix: add story arrow left right to customization provider * fix: view story full width and height * fix: notification v4 * fix: delete first multiple stories go to next story * fix: story tab background color * fix: update navigate * fix: styled to css module * fix: remove console.log * fix: remove console.log * fix: story tab * fix: type * fix: remove console.log * fix: story tab type * fix: noti css * fix: story avatar * fix: remove unused * fix: remove unused * fix: share story button use amityElement * fix: icon * fix: add default communityProfileImageBackground * fix: story tab community * fix: story tab * fix: navigate * fix: story navigate * fix: remove unused * fix: red border story tab * fix: remove ts-sdk peer dep * fix: delete comment * fix: i18n * fix: add margin-bottom to container * fix: remove packagemanager * fix: story ring empty state color * fix: import and type * fix: import and type * fix: remove error icon z-index * fix: official badge condition * fix: remove i18n * fix: no comment word * fix: ASC-20356 - story tab should navigate to unseen (#419) * fix: story view page onClose * fix: to v4 * fix: overlay bug * fix: remove console.log * fix: remove styled component * fix: add story arrow left right to customization provider * fix: view story full width and height * fix: notification v4 * fix: delete first multiple stories go to next story * fix: story tab background color * fix: update navigate * fix: styled to css module * fix: remove console.log * fix: remove console.log * fix: story tab * fix: type * fix: remove console.log * fix: story tab type * fix: noti css * fix: story avatar * fix: remove unused * fix: remove unused * fix: share story button use amityElement * fix: icon * fix: add default communityProfileImageBackground * fix: story tab community * fix: story tab * fix: navigate * fix: story navigate * fix: remove unused * fix: red border story tab * fix: remove ts-sdk peer dep * fix: delete comment * fix: i18n * fix: add margin-bottom to container * fix: remove packagemanager * fix: story ring empty state color * fix: story tab should navigate to unseen * fix: type * fix: import and type * fix: type * fix: background image * fix: element * fix: community story tab condition * fix: remove unused * fix: create story button * fix: community story tab render condition * fix: delete community story condition * fix: discard create story navigate condition * fix: add create new story button * fix: export create new story button * chore: add ui story for create new story button * fix: share story button * fix: remove unused * fix: close button color * fix: remove unused * fix: remove unused function * fix: remove unused * fix: flicker render * fix: loading overlay width height * fix: draft page container * fix: remove inline function * fix: story wrapper height * fix: community story tab condition * fix: aspect ratio button to use useAmityElement * fix: elements * fix: remove inline function * fix: elements * fix: import and type * fix: remove error icon z-index * fix: official badge condition * fix: prop * fix: import * fix: community avatar * fix: story tab condition * fix: type * fix: remove console.log * fix: story tab condition * fix: remove i18n * fix: no comment word * fix: elements * fix: type * fix: add pageId to confirm * fix: ASC-21809 - upload video story on mobile device (#423) * fix: story view page onClose * fix: to v4 * fix: overlay bug * fix: remove console.log * fix: remove styled component * fix: add story arrow left right to customization provider * fix: view story full width and height * fix: notification v4 * fix: delete first multiple stories go to next story * fix: story tab background color * fix: update navigate * fix: styled to css module * fix: remove console.log * fix: remove console.log * fix: story tab * fix: type * fix: remove console.log * fix: story tab type * fix: noti css * fix: story avatar * fix: remove unused * fix: remove unused * fix: share story button use amityElement * fix: icon * fix: add default communityProfileImageBackground * fix: story tab community * fix: story tab * fix: navigate * fix: story navigate * fix: remove unused * fix: red border story tab * fix: remove ts-sdk peer dep * fix: delete comment * fix: i18n * fix: add margin-bottom to container * fix: remove packagemanager * fix: story ring empty state color * fix: story tab should navigate to unseen * fix: type * fix: import and type * fix: type * fix: background image * fix: element * fix: official badge condition * fix: condition * chore: add react-aria-component * fix: hooks * fix: community story tab condition * fix: remove unused * fix: create story button * fix: community story tab render condition * fix: delete community story condition * fix: discard create story navigate condition * fix: add create new story button * fix: export create new story button * chore: add ui story for create new story button * fix: share story button * fix: remove unused * fix: close button color * fix: remove unused * fix: remove unused function * fix: remove unused * fix: flicker render * fix: loading overlay width height * fix: draft page container * fix: remove inline function * fix: story wrapper height * fix: community story tab condition * fix: aspect ratio button to use useAmityElement * fix: elements * fix: remove inline function * fix: elements * fix: import and type * fix: remove error icon z-index * fix: official badge condition * fix: prop * fix: import * fix: community avatar * fix: condition * fix: story tab condition * fix: type * fix: story tab * fix: community * fix: type and layout (#434) * fix: ASC-21494 - non member can react in comment tray (#433) * fix: story tab background color * fix: update navigate * fix: styled to css module * fix: remove console.log * fix: remove console.log * fix: story tab * fix: type * fix: remove console.log * fix: story tab type * fix: noti css * fix: story avatar * fix: remove unused * fix: remove unused * fix: share story button use amityElement * fix: icon * fix: add default communityProfileImageBackground * fix: story tab community * fix: story tab * fix: navigate * fix: story navigate * fix: remove unused * fix: red border story tab * fix: import and type * fix: import and type * fix: remove error icon z-index * fix: official badge condition * fix: reaction list * fix: remove console.log * fix: import * fix: pass pageId and componentId * fix: avatar * fix: community avatar * fix: play pause button * fix: story tab condition * fix: remove unused * fix: remove unused * fix: story tab * fix: onPress * fix: wrapper * fix: rem * fix: remove package manager * feat: skeleton loader (#407) * fix: ASC-23278 - view story container (#435) * fix: create new story button prop * fix: story view * fix: remove package manager * fix: remove console * fix: unnecessary code * fix: elements * fix: ASC-23385 - view story comment tray close after comment (#438) * fix: create new story button prop * fix: story view * fix: remove package manager * fix: remove console * fix: unnecessary code * fix: elements * fix: story comment button width * fix: useGetActiveStoriesByTarget hook * fix: add story progress bar * fix: view story comment tray close after comment * fix: story progress bar * fix: community story * fix: remove unused * fix: community * fix: type * fix: speaker button condition ui * fix: story sub * fix: remove unused * fix: button css * Release/v4.0.0 beta.7 (#439) * chore(release): 4.0.0-beta.5 * chore(release): 4.0.0-beta.6 * fix: build include css (#421) * chore(release): 4.0.0-beta.7 --------- Co-authored-by: bmo-amity-bot * Release/v4.0.0 beta.8 (#440) * build: upgrade version ts-sdk * build: upgrade dependencies * chore(release): 4.0.0-beta.8 --------- Co-authored-by: bmo-amity-bot * feat: ASC-22898 - create post page (#437) * feat: show select post menu * feat: create PostComposerPage * feat: themeStyle * style: change create menu color * feat: implement ui CreatePostMenu * feat: add event when click createPostMenuButton * feat: implement CreateStoryButtons * feat: implement CreatePollButton * feat: implement CreateLivestreamButton * fix: changeable text * feat: implement SelectPostTargetPage * fix: pageId * fix: text static * style: pointer * feat: integrate API communites to SelectPostTargetPage * feat: add UserAvatar * fix: map key * style: change px to rem * fix: dark theme styles * reafactor: remove blank line * feat: create new post page * feat: DetailMediaAttachment * feat: implement media attachment * feat: show mention options * feat: mention * feat: disable post button * feat: add @ in mention text editor * feat: lexical * feat: add goToPostComposerPage * style: truncate * style: text size * feat: post to feed * style: editor * feat: limit max length 5000 cha * fix: follow comment pr * fix: error class * feat: onSubmit form * fix: query search mention from hooks * fix: remove try catch and remove comments * fix: mention post * fix: build failed * fix: remove log (#445) * feat: ASC-00000 - Add empty user (#447) * feat: add user7 * fix: remove mock userId * feat: ASC-23125 - ads on post (#448) * feat: add AdsPost Component * feat: add TODO * feat: add use themeStyle from config * feat: align theme by component * fix: pr comments * fix: refactor Avatar V4 * feat: add TODO * fix: delete first multiple segment story should navigate to next story (#441) * fix: ASC-20694 - wrong notification content (#442) * fix: delete first multiple segment story should navigate to next story * fix: wrong notification text content * fix: ASC-22312 - comment moderator badge condition (#443) * fix: delete first multiple segment story should navigate to next story * fix: wrong notification text content * fix: comment author * fix: remove isBanned prop * fix: remove console.log * feat: sync api v5 (#455) * fix: use preferred theme as light on v3 (#454) * fix: select file to pause story progress bar (#456) * fix: ASC-23557 - scroll mention list (#453) * style: scroll mention list * fix: style and ref * refactor: rename MentionTextInput * fix: ASC-20957 - story tab ring loading state (#444) * fix: delete first multiple segment story should navigate to next story * fix: wrong notification text content * fix: comment author * fix: remove isBanned prop * fix: remove unused * fix: apply useAmityComponent to StoryTab * fix: story tab v4 * fix: remove unused * fix: type * fix: ASC-22315 - failed noti to use BE message (#446) * fix: delete first multiple segment story should navigate to next story * fix: wrong notification text content * fix: comment author * fix: remove isBanned prop * fix: remove unused * fix: apply useAmityComponent to StoryTab * fix: story tab v4 * fix: failed noti should use BE response message * fix: use accessibilityId instead of uiReference * fix: remove unused * fix: remove unused * fix: showImpression condition (#451) * chore: ASC-00000 - uikit core api (#457) * chore: change shouldCall type * chore: update IconComponent api * fix: fix button in story from CancelButton to EditCancelButton * chore: update configuration * feat: MyCommunitiesSearchPage (#459) * fix: ASC-23544 - global search UI (#460) * fix: fix SocialGlobalSearchPage behavior * fix: fix post reaction * feat: ASC-22903 - media attachment (#449) * feat: swipe bottom menu * feat. bottom menu position * feat: hide file button * style: overflow auto * style: hide scroll bar form * feat: add asc-uikit class * fix: show text condition * fix: ASC-23543 - fix PostDetailPage navigation and layout (#461) * fix: fix PostDetailPage navigation and layout * fix: style * fix: post container style * fix: shouldCall useEffect logic (#462) * fix: ASC-22949 - view story navigate logic (#458) * fix: select file to pause story progress bar * fix: story navigate * fix: remove unused * fix: remove unused * fix: add ref to image * fix: remove unused * fix: package * fix: member query (#466) * fix: moderator badge logic (#464) * fix: ASC-00000 - tanstack query cache key (#465) * fix: tanstack query cache key * fix: image viewer, video viewer and image content * fix: remove timestamp interaction (#463) * fix: ASC-21809 - upload story video in android device (#467) * fix: upload story video in android device * fix: prop type * fix: remove console.log * fix: play pause function * fix: ASC-23419 - like reaction in story reaction list show unknown reaction (#469) * fix: upload story video in android device * fix: prop type * fix: reaction list reactions * fix: rte * fix: console.log * fix: swipe down (#470) * fix: ASC-00000 - css broken in v3 (#473) * fix: css * fix: css * fix: ASC-23389 - disable overlay when desktop screen (#477) * fix: swipe down * fix: css * fix: next story condition * fix: css * style: ASC-23581 - add width full button (#476) * style: add width full * fix: type create post * fix: ASC-23552 - fix reaction button interaction (#472) * fix: fix reaction button interaction * chore: remove unused code and apply configuration * fix: type error (#471) * fix: ASC-23659 - newsfeed layout (#474) * fix: newsfeed layout * chore: Update SocialHomePage.module.css * fix: ASC-23007 - community member collection limit (#478) * fix: swipe down * fix: css * fix: next story condition * fix: limit * fix: type * fix: hooks * fix(sdk): ASC-22474 - story preview thumbnail hyperlink in console (#480) * fix: swipe down * fix: css * fix: next story condition * fix: limit * fix: story preview thumbnail for console * fix: story preview * fix: npmrc * fix: export * fix: export * fix: export * fix: export * fix: export * fix: ASC-23583 - discard post modal (#483) * style: modal * style: modal * fix: ASC-23600 - create post params (#475) * fix: push userIds * fix: prevent empty mentionees * fix: ASC-23590 - add toast duration (#479) * fix: add toast duration * feat: prevent click when loading * feat: check offline post * refactor: remove comment code * feat: import isOnline hooks * refactor: remove log * fix: ASC-23599 - mention member in private community (#468) * feat: member mention * refactor: import * fix: query params * refactor: rename component * fix: undefined type * fix: shouldCall * fix: query mention (#484) * feat: ASC-00000 - story preview skeleton (#486) * feat: story preview skeleton * fix: remove z-index * feat: ASC-22898 - create postProvider (#485) * feat: create postProvider * refactor: accessibility * fix: story bugs (#487) * feat: ASC-00000 - new comment (#482) * chore: comments * fix: cannot see comment in storybook * feat: add ui for composer * feat: export PostCommentComposer * feat: add view reply and view all comments UI * fix: post comment input style * fix: change to new comment compose bar * fix: comment compose bar style * fix: remove compose bar on globalFeed page * fix: change className * feat: add PostReplyComment component * feat: set max line height to 10 * fix: add margin to align the item vertical center at first * feat: add comment mention input in composer * feat: add query mentionable users hook * feat: fix mention list position * fix: remove unused * feat: add reply comment ui * fix: placeholder position * fix: to clear textInput state * feat: handle loadMore in post comment list * feat: add post reply comment list * fix: load more replies button in reply list * feat: add bottomsheet for action on PostComment * feat: add bottom sheet for PostReplyComment action * feat: add to show delete comment * feat add handling action * fix: delete comment text style * feat: add delete state ui on PostReplyComment * feat: add edit text in comment details * fix: clear editor state when comment create success * feat: add edit ui * fix: button style * fix: PostCommentInput prop * feat: add logic to convert data text to edior state * fix: cannot mention user * fix: edit text styling * feat: add pointer styling * fix: change to wrap options inside component to reduce calling check isFlaggedByMe * feat: handle update comment * feat: update comment in reply comment * feat: add subscription to each comment * feat: add ui text with mention * fix: remove unused * fix: sending too many request when intersect * feat: subscribe to get new comment data * feat: fix mention panel position --------- Co-authored-by: Bonn * fix: ASC-00000 - story bugs (#491) * fix: story bugs * fix: button style * fix: image mode * fix: story bugs * fix: remove console.log * fix: onPress * fix: css * fix: ASC-22508 - impression count condition (#492) * fix: story bugs * fix: button style * fix: image mode * fix: story bugs * fix: remove console.log * fix: onPress * fix: css * fix: impression condition only creator and community mod can see * feat: ASC-23125 - global feed ad integration (#489) * feat: AdEngine * chore: AdEngineProvider * feat: integrate with Newsfeed * chore: refactoring * feat: update pagination logic * feat: update Paginator logic * feat: PostAd integration * chore: add data-qa-anchor * fix: avatarUrl and adImageUrl * chore: remove packageManager field * chore: remove unused code * chore: Update src/v4/social/internal-components/PostAd/PostAd.module.css * fix: update code by review * chore: add storybook users (#494) * feat: comment ad (#493) * chore: update ci (#495) * fix: fix slice index (#498) * fix: ad live collection integration (#503) * fix: ASC-00000 - fix react error (#502) * fix: fix svg props * fix: fix react error * fix: svg props * fix: ASC-00000 - done button (#496) * fix: done button * fix: add todo comment for type * fix: submit form function * fix: remove console.log * fix: apply themeStyle * fix: type * fix: formId prop * fix: formId to be optional type * fix: done button type * fix: ASC-00000 - modal overlay (#497) * fix: done button * fix: add todo comment for type * fix: submit form function * fix: remove console.log * fix: apply themeStyle * fix: type * fix: modal overlay * fix: remove console.log * fix: ad information drawer (#500) * fix: ASC-24074 - comment ad style (#504) * fix: comment ad style * feat: change sponsor badge text * fix: ASC-00000 - theme (#501) * fix: configuration background not change in livechat compose bar * fix: cannot use theme configuration * fix: theme logic * feat: update a customization key ordering * fix: update css variable names to align with a design * css var * fix: theme --------- Co-authored-by: ptchaya_p * fix: ASC-24074 - fix an incorrect text position (#499) * fix: fix an incorrect text position * fix: update classname * feat: ASC-23131 - story premium ads (#508) * feat: story premium ads * chore: remove commented code * feat: ASC-23312 - support image video upload (#506) * feat: upload images * feat: handle failed * feat: add upload image * feat: upload video * feat: prevent more than 10 uploads * feat: handle error * feat: apply detail media attachment * fix: resolution thumbnail * feat: camera upload * fix: remove not used file * fix: remove log * style: data-attribute * refactor: isMobile utils * fix: params * refactor: onChange * fix: ASC-23693 - fix premium ads global feed (#512) * fix: fix usePaginatorCore logic * fix: remove poll liveStream file posts from a global feed * fix: reduce a request amount * fix: time-window ad and type error * fix: child post * fix: filter liveStream poll file post * fix: react rerender issue * fix: observer display * fix: retain scroll position (#513) * fix: remove button hover color (#517) * fix: reaction count (#509) * fix: comment ad styles (#518) * fix: ASC-00000 - fix paginator (#516) * fix: return an empty array when its length is 0 * chore: rename usePagination to usePaginator * fix: ASC-23591 - image ratio (#515) * chore: remove unused code * fix: fix image ratio * fix: ASC-24193 - fix a background color css variable (#510) * fix: asc-color-base-background to asc-color-background-default * fix: update PostAd footer background color * chore: ASC-00000 - premium ads pure components (#522) * feat: export CommentAd and PostAd as a pure component # Conflicts: # src/index.ts # src/v4/social/internal-components/CommentAd/CommentAd.tsx # src/v4/social/internal-components/PostAd/PostAd.tsx * chore: rename AdInformation dir * fix: componentId * chore: ASC-00000 - console story ad ui (#521) * chore: console story ad ui * chore: add TODO * fix: reactions count formatting (#519) * style: fix height (#523) * fix: ASC-24020 - load more mention list (#514) * fix: load more mention list * refactor: load more Co-authored-by: Bonn * fix: error ci --------- Co-authored-by: Bonn * fix: button style unset (#524) * style: change height (#525) * fix: ASC-00000 - create post menu position (#526) * fix: z-index * style: remove z-index * feat: ASC-00000 - AmityStoryTargetSelectionPage (#528) * feat: AmityStoryTargetSelectionPage * fix: remove console.log * fix: comment ad style (#527) * fix: ASC-23586 - livechat customization (#520) * feat: lexical * fix: restructure LiveChat # Conflicts: # src/v4/core/providers/AmityUIKitProvider.tsx # src/v4/social/components/PostContent/ImageContent/ImageContent.tsx # src/v4/social/components/PostContent/VideoContent/VideoContent.tsx * fix: a new MessageComposer * chore: remove packageManager field * fix: fix import * fix: type error * fix: enter to send only on h/w keyboard * chore: update LiveChate storybook * fix: update livechat configuration * chore: update src/v4/chat/elements/SenderMessageBubble/SenderMessageBubble.tsx Co-authored-by: Pitchaya T. <33589608+ptchayap@users.noreply.github.com> * chore: apply suggestions from code review Co-authored-by: Pitchaya T. <33589608+ptchayap@users.noreply.github.com> --------- Co-authored-by: Pitchaya T. <33589608+ptchayap@users.noreply.github.com> * fix: ASC-24263 - stories bug (#533) * fix: i18n error * fix: global feed story index * fix: CommunityFeedStory * fix: image ratio to 1 (#532) * fix: ASC-00000 - minor bugs (#531) * fix: change unit to dvw and dvh * fix: change p to span because an error in a browser * fix: globalfeed key * fix: stop drag default and remove unnecessary preventDefault * fix: cannot click on mobille * fix: prevent drag event * fix: UIPostAd * chore: add TODO * fix: style on mobile * fix: Linkify * fix: draft video story (#530) * fix: ASC-00000 - story (#507) * fix: story uploading state * fix: remove unnessescary useMeme * fix: remove unused * fix: bottom sheet story * fix: remove unused * fix: done button type * fix: type * fix: align with api (#534) * feat: ASC-24094 - update code snippet (#529) * feat: create post button * feat: code snippet * feat: update MyCommunitiesSearchPage * fest: export components and page * fix: ci error * feat: export TopNavigation * fix: types * feat: export AmitySocialHomePageTab --------- Co-authored-by: Bonn * fix: ASC-21809 - story video duration (#511) * fix: story uploading state * fix: remove unnessescary useMeme * fix: remove unused * fix: bottom sheet story * fix: video duration * fix: remove unused * fix: type * fix: done button type * fix: type * fix: ignore type * fix duration * fix: story image bottom sheet * fix: ASC-21809 - image local (#535) * fix: story uploading state * fix: remove unnessescary useMeme * fix: remove unused * fix: bottom sheet story * fix: video duration * fix: remove unused * fix: type * fix: done button type * fix: type * fix: ignore type * fix duration * fix: story image bottom sheet * fix: image local * fix: video ratio (#536) * fix: fix Linkify (#539) * fix: StoryAd layout (#537) * feat: combine v3 code (#541) * refactor: ASC-00000 - upload media (#540) * refactor: upload media * fix: spelling * refactor: props name * fix: props name * refactor: props name * fix: split into RoundedBackButton and BackButton (#543) * fix: useFeed (#542) * chore: ASC-23077 - eslint 9 (#387) * chore: import rule lint * chore: eslint 9 * chore: add packageManager field * fix: remove pnpm version from ci * chore: removed unused file * chore: update packageManager field in package.json * chore: update pnpm-lock.yaml * chore: bring back the old code * fix: lint * chore: upgrade pnpm to 9.5.0 * chore: bring back .npmrc * fix: css path * fix: ASC-00000 - v4 comment list component (#505) * fix: done button * fix: add todo comment for type * fix: submit form function * fix: remove console.log * fix: apply themeStyle * fix: type * fix: modal overlay * fix: remove console.log * fix: integrate comment v4 * fix: comment list * fix: remove pkg version * fix: type * fix: type * fix: comment component * fix: comment component * fix: remove console.log * fix: css * fix: import name * feat: ASC-00000 - comment skeleton loading and see more (#546) * feat: add skeletons on comment list * feat: change limit * feat: add see more button * feat: use line-clamp * fix: use max height instead * fix: mention panel position * fix: flex item gap on mention user * fix: remove unused * fix: reply offset bottom * fix: use variable * fix: react error * fix: use variable instead of inline style * fix: remove inline style * fix: use uuid to be key * fix: ASC-23324 - combine v3 and v4 (#548) * fix: add button for user * refactor: condition Co-authored-by: Bonn * fix: build fail * fix: handle navigation cross version * fix: remove console * fix: combine story --------- Co-authored-by: Bonn * fix: ASC-24390 - mention is not show in comment (#549) * fix: mention is not show * fix: remove post props * fix: onPress button (#550) * fix: ASC-24396 - comment list pagination (#553) * fix: update threshold * fix: remove height 100% * chore: typo * fix: ASC-24024 - mention list and media attachment position (#551) * fix: position media attachment * style: adapt data attribute * fix: query params * fix: mention list position * fix: z-index * fix: mention list position * fix: position mention * fix: unit rem * fix: query community sort (#552) * docs: update example env (#554) * Release/v4.0.0 beta.10 (#555) * chore: v4.0.0-beta.9 * chore(release): 4.0.0-beta.10 --------- Co-authored-by: bmo-amity-bot * feat: ASC-24349 - edit post (#557) * feat: edit menu * feat: default value edit post * refactor: remove log * feat: edit image * feat: edit video * refactor: thumbnail component * refactor: remove log * feat: update items * fix: combineItemsWithAds * fix: post data selector * fix: mention enter new line * refactor: button * fix: element button --------- Co-authored-by: Bonn * feat: ASC-24665 - check labels ci (#558) * feat: ASC-24666 - create pull_request_template.md (#559) * Create pull_request_template.md Create pull request template * Update pull_request_template.md Add checklist * feat: ASC-23982 - post impression (#561) * feat: add post impression * refactor: remove log * refactor: changed to hooks * refactor: hooks * refactor: ref div * fix: undefined metadata (#564) * fix: ASC-24780 - handle no change edit post (#562) * fix: handle no change edit * fix: remove img change save * fix: compare array function * fix: type params Co-authored-by: Bonn * fix: error text --------- Co-authored-by: Bonn * fix: ASC-24778 - add edit tag (#563) * fix: edit tag * style: reduce spacing * fix: add getAuthToken param (#567) * fix: ASC-24831 - profile blinking (#569) * chore: v3.6.0 * fix: remove expose v4 component * chore: v3.6.0 * fix: remove expose v4 component * chore(release): 3.7.0 * chore: update pnpm-lock.yaml * chore(release): 3.8.0 * chore: update pnpmplock * fix: ASC-24831 - profile blinking * fix: ASC-24831 - profile blinking * fix: avoid channel object change affect to avatarUrl from calling reading api while reading the channel * fix: button size * fix: lint --------- Co-authored-by: bmo-amity-bot Co-authored-by: Bonn * fix: change text fail edit (#571) * fix: ASC-23280 - showing long post (#580) * fix: truncate * style: truncate * style: add spacing (#581) * fix: ASC-24779 - onClick go to post detail (#584) * fix: add onClick redirect * fix: use Button * fix: fix ui (#589) * fix: ASC-24857 - link preview button (#570) * fix: use button instead div * refactor: onBack * fix: add button Ad link * chore(release): 4.0.0-beta.11 (#601) Co-authored-by: bmo-amity-bot * fix: ASC-00000 - layout global feed (#603) * fix: scroll horizontal * style: remove padding * chore: change default screen to fullscreen (#598) * chore: ASC-00000 - configurable storybook (#595) * chore: receive apiKey, apiRegion, userId and displayName as a free text * chore: add submit toggle * feat: ASC-24796 - fix community profile (#568) * fix: merge from develop * fix: remove unused * fix: community profile component * fix: post content to use bottom sheet * fix: community tab control state * feat: add community pin * fix: community profile style * feat: navigate back in community cover * fix: use millify for count value * fix: coomunity profile post gap * fix: verify badge * fix: community name description * fix: remove unused * fix: community description max lines * fix: community categories * fix: comunity cover to use bem convention * fix: community cover add max-height * fix: export page and components * fix: update navigation and page behavior * fix: add community info * feat: community feed post skeleton * fix: remove unused * fix: community tab active color state * fix: page behavior comment unused * fix: css * fix: pin * fix: type * fix: css * Revert "fix: css" This reverts commit d10c48f94a79d0ab4dcd876a35553457ad6abbc3. * fix: css * style: z index * fix: category UI * fix: handle long description * fix: max length * fix: cover icon color * fix: private commu hide join btn * feat: no post UI * feat: empty pinpost * feat: lock private content * fix: condition show lock private content * fix: condition lock content * feat: edit page v3 * fix: export error * fix: context type * fix: redirect edit post * style: pass fill default icon as props * style: remove props * fix: skeleton feed * refactor: icons and conditions * style: lock icon size * fix: locate files * fix: import error * fix: duplicated code * fix: edit update post * fix: review comments * refactor: spinner pull to refresh * fix: navigate post detail * fix: hide post impression when not member * style: spacing * fix: prevent comment for non member * style: color text empty post * fix: UI post title * fix: post detail click and non member access * fix: threshold * style: menu button size --------- Co-authored-by: Chaiwat Trisuwan * fix: ASC-00000 - ulta fixes (#577) * fix: ASC-24926 - responsive image carousel (#574) * style: fix color item (#572) * fix: ASC-24930 - user profile about color (#573) * style: add bg * fix: color-scheme light * style: ratio edit profile * style: change rem to px * style: spacing * fix: ASC-24929 - mobile responsive edit community profile (#578) * style: mobile responsive * style: center * fix: ASC-24927 - mobile create community page (#576) * fix: mobile create community page * chore: update form layout * chore: ASC-24925 - post permissions (#579) * chore: add skeleton loader * chore: * fix: remove skeleton loader * fix: post permissions * fix: ASC-24931 - navigation (#582) * fix: hamburger navigation * fix: add hideSideMenu prop * fix: ASC-24924 - update each attachment type (#575) * fix: update each attachment type * fix: modal style on mobile screen * fix: not return just setPosts Co-authored-by: Bonn --------- Co-authored-by: Bonn * fix: ASC-24922 - remove subscription (#583) * fix: remove subscription on post and comment * chore: upgreade ts-sdk * fix: remove unused * fix: ASC-24930 - edit user profile ratio (#585) * style: mobile responsive * fix: responsive edit user button * fix: ASC-23387 - discard post (#586) * style: text modal color * fix: clear input * fix: modal block * fix: redirect page * fix: user comment link (#588) * fix: avatar click link (#591) * style: fix sticky side (#587) * fix: ASC-24978 - permission typo (#592) * fix: permission typo * chore: remove console.log * fix: ASC-24972 - handle error submit (#590) * fix: handle error submit * fix: remove log * fix: error type Co-authored-by: Bonn * fix: type any --------- Co-authored-by: Bonn * chore: upgrade sdk version (#593) * fix: bring back isModerator (#594) * fix: move hook to a top of a component * chore: upgrade sdk version (#596) * fix: export v3 provider * fix: remove unused code * chore: remove default font * chore: circular std font * chore: update max-width * fix: remove max-width on Explore * fix: remove min-width and min-height from a community card * fix: update CategoriesCard and RecommendedList layout * fix: ASC-25140 - active and hover state sidebar V3 (#604) * fix: z-index (#600) * style: active and hover state * fix: ASC-25143 - gallery grid (#605) * fix: grid 3 * fix: remove grid-gap * fix: ASC-00000 - skeleton css (#609) * fix: skeleton css * fix: lint # Conflicts: # src/core/components/Button/styles.tsx # src/core/components/SideMenuActionItem/styles.tsx # src/icons/Verified.tsx # src/social/components/CommunitiesList/index.tsx # src/social/components/CommunitiesList/styles.tsx # src/social/components/community/AllCommunities/index.tsx # src/social/components/community/Card/UICommunityCard.tsx # src/social/components/community/Header/UICommunityHeader.tsx # src/social/components/community/Header/styles.tsx # src/social/components/community/Name/index.tsx # src/social/components/community/Name/styles.tsx # src/social/components/community/TrendingItem/UITrendingItem.tsx # src/social/pages/Application/index.tsx # src/social/pages/Explore/styles.tsx # src/social/pages/UserFeed/index.tsx # src/v4/social/pages/Application/index.tsx * chore: revert fonts * Revert "chore: remove default font" This reverts commit b62f1aa5e77828772cd1fffd5a46d84480b35626. # Conflicts: # src/core/providers/UiKitProvider/index.css # src/v4/styles/global.css * chore: change sdk version to a latest one * fix: bring back v4 export --------- Co-authored-by: ChayanitBm Co-authored-by: Pitchaya T. <33589608+ptchayap@users.noreply.github.com> * feat: ASC-23849 - announcement post (#610) * fix: merge from develop * fix: remove unused * fix: community profile component * fix: community tab control state * fix: community profile style * fix: verify badge * fix: community description max lines * fix: community categories * fix: comunity cover to use bem convention * fix: community cover add max-height * fix: export page and components * fix: update navigation and page behavior * fix: add community info * feat: community feed post skeleton * fix: remove unused * fix: community tab active color state * fix: pin * fix: css * Revert "fix: css" This reverts commit d10c48f94a79d0ab4dcd876a35553457ad6abbc3. * fix: css * style: z index * fix: cover icon color * feat: empty pinpost * feat: lock private content * fix: redirect edit post * style: pass fill default icon as props * style: remove props * fix: skeleton feed * refactor: icons and conditions * fix: locate files * fix: duplicated code * feat: featured tag * feat: announcement icon * feat: integrate annouce post with mock data * feat: add SDK getPinnedPosts * feat: integrate announcement post * chore: install stg SDK * feat: show tag category in post detail * refactor: remove mock data * refactor: PR comment * refactor: remove class not used * fix: duplicate css * fix: error unknow type --------- Co-authored-by: Chaiwat Trisuwan * fix: ASC-25190 - delete announcement post (#612) * fix: refresh live collection * fix: type * fix: SDK version stg * fix: add dot * fix: add dot * fix: comments demo * fix: update permission to create story * fix: story permission * fix: story permission * refactor: typo * fix: ASC-25195 - hide chat input for normal user (#617) * feat: hide chat input for normal user * fix: condition to hide compose bar * fix: ASC-00000 - sdk version prod (#623) * fix: sdk version * fix: version sdk * fix: ASC-235231 - video thumbnail (#626) * fix: video thumbanil * fix: position toast * chore: update sdk * refactor: upload from camera * chore: revert sdk * fix: add remove drawer (#627) * fix: ASC-25204 - post mention (#630) * chore: update PostTextField * fix: mention layout * fix: layout and removed unused code * chore: remove console.log * fix: intersection node * fix: condition show story (#631) * fix: ASC-24623 - add redirect user feed v3[Repeat PR to DEV] (#632) * fix: add redirect user feed * fix: remove code not use * fix: ASC-24486 - All users show in comment's mention list (#622) * chore: rattata * chore: applied includeDeleted * chore: update tssdk version * fix: update SDK version * chore: revert sdk version * fix: sdk version * fix: ci * fix: ci and upgrade pnpm to 9.9 * chore: update ci * fix: force ci to install specific sdk version * fix: user navigation * fix: pass community to CommentComposer * feat: add includeDeleted filter * fix: background coloer * fix: use searchMembers instead of getMembers * fix: sdk version on devDependencies * fix: installed SDK version * chore: rattata * chore: applied includeDeleted * chore: update tssdk version * fix: update SDK version * chore: revert sdk version * fix: sdk version * fix: ci * fix: ci and upgrade pnpm to 9.9 * chore: update ci * fix: force ci to install specific sdk version * fix: user navigation * fix: pass community to CommentComposer * feat: add includeDeleted filter * fix: background coloer * fix: use searchMembers instead of getMembers * fix: sdk version on devDependencies * fix: installed SDK version * fix: revert change --------- Co-authored-by: Bonn Co-authored-by: Chayanit Manop * chore(sdk): rattata (#611) * chore: rattata * chore: applied includeDeleted * chore: update tssdk version * fix: update SDK version * chore: revert sdk version * fix: ASC-24486 - All users show in comment's mention list (#622) * chore: rattata * chore: applied includeDeleted * chore: update tssdk version * fix: update SDK version * chore: revert sdk version * fix: sdk version * fix: ci * fix: ci and upgrade pnpm to 9.9 * chore: update ci * fix: force ci to install specific sdk version * fix: user navigation * fix: pass community to CommentComposer * feat: add includeDeleted filter * fix: background coloer * fix: use searchMembers instead of getMembers * fix: sdk version on devDependencies * fix: installed SDK version * chore: rattata * chore: applied includeDeleted * chore: update tssdk version * fix: update SDK version * chore: revert sdk version * fix: sdk version * fix: ci * fix: ci and upgrade pnpm to 9.9 * chore: update ci * fix: force ci to install specific sdk version * fix: user navigation * fix: pass community to CommentComposer * feat: add includeDeleted filter * fix: background coloer * fix: use searchMembers instead of getMembers * fix: sdk version on devDependencies * fix: installed SDK version * fix: revert change --------- Co-authored-by: Bonn Co-authored-by: Chayanit Manop * chore: rattata * chore: update tssdk version * fix: update SDK version * chore: revert sdk version * fix: sdk version * fix: ci * fix: ci and upgrade pnpm to 9.9 * chore: update ci * fix: force ci to install specific sdk version * fix: user navigation * fix: ASC-25204 - post mention (#618) * chore: update PostTextField * fix: mention layout * fix: layout and removed unused code * chore: remove console.log * chore: update tssdk version * fix: update SDK version * chore: revert sdk version * chore: upgrade sdk version * chore: update tssdk version * fix: update SDK version * chore: revert sdk version * chore: upgrade sdk version * fix: revert workflow and readme --------- Co-authored-by: Chayanit Manop Co-authored-by: Pitchaya T. <33589608+ptchayap@users.noreply.github.com> Co-authored-by: ptchaya_p * chore(release): 4.0.0-beta.12 (#638) Co-authored-by: bmo-amity-bot * feat: ASC-24795 - pinned post (#621) * fix: comments demo * refactor: typo * feat: pinned post * chore: update package * feat: pinned post filter * refactor: remove limit pinned post * fix: props fill * fix: fix PostTextField (#643) * fix: prevent pr to be merged if it is labeled with do not merge (#644) * fix: skeleton style (#646) * fix: community feed render post (#651) * feat: ASC-25653 - add accessibilityId (#650) * feat: add accessibility id * feat: add accessibilityId * fix: ASC-25689 - add condition to show moderator badge (#652) * fix: add condition to show moderator badge * fix: hooks moderator * feat: ASC-25040 - post as brand (#608) * chore: post as brand * feat: add renderer prop into Typography * feat: add BrandBadge for PostContent * feat: add BrandBadge into Mention * fix: fix layout * fix: isBrand logic * chore: remove unused code * chore: update tssdk version * fix: postContent layout * fix: fix UserSearchResult layout * chore: remove unused code * chore: upgrade sdk version * fix: add targetType for StoryCommentComposeBar * chore: refactor CommentComposer * fix: postcontent brand badge * chore: upgrade lexical to a latest version * fix: add error handling for login fn * fix: add brand badge on UserHeader v3 component * fix: type * fix: type * fix: bring back import css * fix: version * chore: update sdk version * chore(release): 4.0.0-beta.13 (#653) Co-authored-by: bmo-amity-bot * fix: dedup mention data (#655) * fix: loadmore (#660) * feat: ASC-25653 - add accessibility (#659) * feat: add accessibilityId * feat: add accessibilityId * fix: change to use accessibilityId * fix: hooks render * feat: ASC-25247 - explore page (#656) * feat: explore page * feat: explore page * feat: explore * feat: a new CommunityJoinedButton * feat: update skeleton and CommunitySearchResult * fix: change trending limit to 5 * fix: change default tab back to newsfeed * fix: add key and fix style * fix: explore error logic * fix: fix ClickableArea type * chore: custom sdk version * fix: fix style * chore: remove unused code * fix: fix community category render logic * fix: search click logic * fix: spacing * fix: css * fix: fix TopSearchBar style * fix: truncate logic and CommunityRowItem on CommunitiesByCategoryPage * fix: update code due to comments * fix: fix build * fix: remove React error and update component to align with a tech spec * fix: fix ClickableArea style * fix: customization key * fix: notification typo * fix: seemore button * fix: isBrand logic * fix: remove join button from MycommunitiesPage * fix: fix recommended community card layout * fix: scrolling behavior * fix: fix bugs * fix: navigation bugs * fix: community create page * fix: ASC-26256 - fix lexical utils (#661) * chore: upgrade lexical to 0.18 * fix: url parsing logic * chore: remove console.log * fix: ASC-26255 - refetch recommended communities after join (#662) * fix: refetch recommended communities after join * chore: bring back old code * fix: condition check target community (#663) * Release/v4.0.0 beta.14 (#664) * chore(release): 4.0.0-beta.14 * chore: upgrade sdk version --------- Co-authored-by: bmo-amity-bot * chore(release): 4.0.0-beta.15 (#665) Co-authored-by: bmo-amity-bot * chore(release): 4.0.0-beta.15 --------- Co-authored-by: Chaiwat Trisuwan Co-authored-by: Kiattirat S Co-authored-by: Bonn Co-authored-by: ChayanitBm Co-authored-by: bmo-amity-bot --- CHANGELOG.md | 2 + package.json | 10 +- pnpm-lock.yaml | 303 +++++++------- readme.md | 131 +++++- .../CommunityForm/EditCommunityForm.tsx | 8 + .../SideSectionMyCommunity/index.tsx | 1 - .../components/UserHeader/UIUserHeader.tsx | 5 +- src/social/components/UserHeader/index.tsx | 1 + src/social/components/UserHeader/styles.tsx | 1 + src/social/pages/CommunityEdit/index.tsx | 4 +- .../MessageComposer/MessageComposer.tsx | 4 +- src/v4/core/components/Avatar/Avatar.tsx | 21 +- src/v4/core/components/ConfirmModal/index.tsx | 2 +- src/v4/core/components/Modal/index.tsx | 21 +- .../core/components/Typography/Typography.tsx | 242 ++++++----- .../collections/useCategoriesCollection.ts | 22 + .../useRecommendedCommunitiesCollection.ts | 21 + .../useTrendingCommunitiesCollection.ts | 24 ++ src/v4/core/hooks/objects/useUser.ts | 10 +- src/v4/core/hooks/useCategory.ts | 22 + .../ClickableArea/ClickableArea.module.css | 5 + .../natives/ClickableArea/ClickableArea.ts | 41 ++ src/v4/core/natives/ClickableArea/index.ts | 5 + src/v4/core/natives/Img/Img.tsx | 22 + src/v4/core/natives/Img/index.tsx | 1 + src/v4/core/providers/AmityUIKitProvider.tsx | 189 +++++---- .../core/providers/CustomizationProvider.tsx | 42 ++ src/v4/core/providers/NavigationProvider.tsx | 61 +++ src/v4/helpers/utils.ts | 8 +- src/v4/icons/Brand.tsx | 36 ++ src/v4/icons/ChevronRight.tsx | 16 + src/v4/icons/People.tsx | 32 ++ .../components/Comment/Comment.module.css | 11 + src/v4/social/components/Comment/Comment.tsx | 38 +- .../CommentComposer.module.css | 25 +- .../CommentComposer/CommentComposer.tsx | 138 +++--- .../CommentComposer/CommentInput.module.css | 30 +- .../CommentComposer/CommentInput.tsx | 394 +++++++++--------- .../components/CommentList/CommentList.tsx | 15 +- .../MentionUser.module.css | 13 + .../CommentMentionInput/MentionUser.tsx | 8 +- .../CommentOptions/CommentOptions.tsx | 8 + .../CommunityFeed/CommunityFeed.tsx | 69 +-- .../CommunityHeader.module.css | 11 + .../CommunityHeader/CommunityHeader.tsx | 15 +- .../CommunitySearchResult.module.css | 1 - .../CommunitySearchResult.tsx | 50 ++- .../EmptyNewsFeed/EmptyNewsFeed.tsx | 21 +- .../ExploreCommunityCategories.module.css | 44 ++ .../ExploreCommunityCategories.stories.tsx | 15 + .../ExploreCommunityCategories.tsx | 87 ++++ .../ExploreCommunityCategories/index.tsx | 1 + .../ExploreCommunityEmpty.module.css | 15 + .../ExploreCommunityEmpty.tsx | 43 ++ .../components/ExploreCommunityEmpty/index.ts | 1 + .../ExploreEmpty/ExploreEmpty.module.css | 15 + .../components/ExploreEmpty/ExploreEmpty.tsx | 39 ++ .../social/components/ExploreEmpty/index.ts | 1 + .../GlobalFeed/GlobalFeed.module.css | 4 + .../components/GlobalFeed/GlobalFeed.tsx | 19 +- .../components/Newsfeed/Newsfeed.module.css | 4 + .../social/components/Newsfeed/Newsfeed.tsx | 37 +- .../PostContent/ImageContent/ImageContent.tsx | 7 + .../PostContent/LinkPreview/LinkPreview.tsx | 10 +- .../PostContent/PostContent.module.css | 86 +++- .../components/PostContent/PostContent.tsx | 225 ++++++---- .../PostContent/TextContent/TextContent.tsx | 65 ++- .../PostContent/VideoContent/VideoContent.tsx | 9 +- .../RecommendedCommunities.module.css | 91 ++++ .../RecommendedCommunities.stories.tsx | 15 + .../RecommendedCommunities.tsx | 178 ++++++++ ...ecommendedCommunityCardSkeleton.module.css | 63 +++ .../RecommendedCommunityCardSkeleton.tsx | 40 ++ .../RecommendedCommunities/index.tsx | 1 + .../components/ReplyComment/ReplyComment.tsx | 26 +- .../ReplyCommentList/ReplyCommentList.tsx | 8 +- .../components/StoryTab/StoryTabCommunity.tsx | 7 +- .../components/StoryTab/StoryTabItem.tsx | 10 +- .../TopNavigation/TopNavigation.tsx | 3 +- .../TopSearchBar/TopSearchBar.module.css | 1 + .../components/TopSearchBar/TopSearchBar.tsx | 3 +- .../TrendingCommunities.module.css | 6 + .../TrendingCommunities.stories.tsx | 15 + .../TrendingCommunities.tsx | 84 ++++ .../components/TrendingCommunities/index.tsx | 1 + .../UserSearchItem.module.css | 13 +- .../UserSearchResult/UserSearchItem.tsx | 27 +- .../UserSearchResult/UserSearchResult.tsx | 13 +- .../AllCategoriesTitle.module.css | 8 + .../AllCategoriesTitle/AllCategoriesTitle.tsx | 33 ++ .../elements/AllCategoriesTitle/index.ts | 1 + .../CategoryChip/CategoryChip.module.css | 29 ++ .../elements/CategoryChip/CategoryChip.tsx | 56 +++ .../CategoryChipSkeleton.module.css | 22 + .../CategoryChip/CategoryChipSkeleton.tsx | 7 + src/v4/social/elements/CategoryChip/index.ts | 1 + .../CategoryTitle/CategoryTitle.module.css | 5 + .../elements/CategoryTitle/CategoryTitle.tsx | 35 ++ src/v4/social/elements/CategoryTitle/index.ts | 1 + .../elements/CommentButton/CommentButton.tsx | 5 +- .../CommunityCardImage.module.css | 20 + .../CommunityCardImage/CommunityCardImage.tsx | 46 ++ .../elements/CommunityCardImage/index.ts | 1 + .../CommunityCategory.module.css | 21 +- .../CommunityCategory/CommunityCategory.tsx | 43 +- .../CommunityCategoryName.module.css | 6 +- .../CommunityEmptyImage.tsx | 46 ++ .../elements/CommunityEmptyImage/index.ts | 1 + .../CommunityEmptyTitle.module.css | 3 + .../CommunityEmptyTitle.tsx | 36 ++ .../elements/CommunityEmptyTitle/index.ts | 1 + .../elements/CommunityInfo/CommunityInfo.tsx | 6 +- .../CommunityJoinButton.module.css | 27 ++ .../CommunityJoinButton.tsx | 7 +- .../CommunityJoinedButton.module.css | 35 ++ .../CommunityJoinedButton.tsx | 54 +++ .../elements/CommunityJoinedButton/index.ts | 1 + .../CommunityOfficialBadge.tsx | 15 +- .../CommunityProfileTab.tsx | 18 + .../CommunityRowImage.module.css | 7 + .../CommunityRowImage/CommunityRowImage.tsx | 64 +++ .../elements/CommunityRowImage/index.ts | 1 + .../CreateCommunityButton.tsx | 2 +- .../CreateNewPostButton.tsx | 11 +- .../elements/EditPostTitle/EditPostTitle.tsx | 4 +- .../ExploreCreateCommunity.module.css | 20 + .../ExploreCreateCommunity.tsx | 42 ++ .../elements/ExploreCreateCommunity/index.tsx | 1 + .../ExploreEmptyImage/ExploreEmptyImage.tsx | 191 +++++++++ .../elements/ExploreEmptyImage/index.tsx | 1 + .../ExploreRecommendedTitle.module.css | 3 + .../ExploreRecommendedTitle.tsx | 36 ++ .../ExploreRecommendedTitle/index.tsx | 1 + .../ExploreTrendingTitle.module.css | 3 + .../ExploreTrendingTitle.tsx | 36 ++ .../elements/ExploreTrendingTitle/index.tsx | 1 + .../MyTimelineAvatar/MyTimelineAvatar.tsx | 7 +- .../elements/PostTextField/PostTextField.tsx | 32 +- src/v4/social/hooks/useCategoriesByIds.ts | 9 +- src/v4/social/hooks/useCommunityActions.ts | 57 +++ .../BrandBadge/BrandBadge.module.css | 32 ++ .../BrandBadge/BrandBadge.stories.tsx | 10 + .../BrandBadge/BrandBadge.tsx | 10 + .../internal-components/BrandBadge/index.tsx | 1 + .../CategoryImage/CategoryImage.module.css | 7 + .../CategoryImage/CategoryImage.tsx | 52 +++ .../CategoryImage/index.tsx | 1 + .../internal-components/Comment/index.tsx | 9 +- .../CommentAd/UICommentAd.tsx | 11 +- .../CommentComposeBar/CommentComposeBar.tsx | 4 +- .../CommunityCategories.module.css | 13 + .../CommunityCategories.tsx | 74 ++++ .../CommunityCategories/index.ts | 1 + .../CommunityMember.module.css | 13 + .../CommunityMember/CommunityMember.tsx | 14 +- .../CommunityRowItem.module.css | 93 +++++ .../CommunityRowItem/CommunityRowItem.tsx | 142 +++++++ .../CommunityRowItemDivider.module.css | 12 + .../CommunityRowItemDivider.tsx | 5 + .../CommunityRowItemSkeleton.module.css | 53 +++ .../CommunityRowItemSkeleton.tsx | 48 +++ .../CommunityRowItem/index.tsx | 1 + .../internal-components/EditPost/EditPost.tsx | 2 +- .../EditPost/Thumbnail.tsx | 3 + .../Lexical/MentionItem.module.css | 13 + .../Lexical/MentionItem.tsx | 29 +- .../Lexical/__snapshots__/utils.test.ts.snap | 292 +++++++++++++ .../internal-components/Lexical/utils.test.ts | 97 +++++ .../internal-components/Lexical/utils.ts | 145 ++++++- .../internal-components/PostAd/UIPostAd.tsx | 21 +- .../internal-components/PostMenu/PostMenu.tsx | 1 + .../StoryCommentComposeBar.tsx | 9 +- .../TabsBar/TabsBar.module.css | 8 +- .../TextWithMention.module.css | 13 + .../TextWithMention/TextWithMention.tsx | 129 ++++-- .../UserAvatar/UserAvatar.tsx | 19 +- .../AllCategoriesPage.module.css | 55 +++ .../AllCategoriesPage.stories.tsx | 12 + .../AllCategoriesPage/AllCategoriesPage.tsx | 93 +++++ .../social/pages/AllCategoriesPage/index.tsx | 1 + src/v4/social/pages/Application/index.tsx | 13 +- .../CommunitiesByCategoryPage.module.css | 131 ++++++ .../CommunitiesByCategoryPage.tsx | 108 +++++ .../EmptyCommunity.module.css | 10 + .../EmptyCommunity.tsx | 14 + .../pages/CommunitiesByCategoryPage/index.tsx | 1 + .../CommunityProfilePage.module.css | 13 - .../pages/PostDetailPage/PostDetailPage.tsx | 7 +- .../SelectPostTargetPage.tsx | 2 +- .../pages/SocialHomePage/Explore.module.css | 90 ++++ .../social/pages/SocialHomePage/Explore.tsx | 103 +++++ .../SocialHomePage/ExploreError.module.css | 18 + .../pages/SocialHomePage/ExploreError.tsx | 25 ++ .../pages/SocialHomePage/SocialHomePage.tsx | 11 +- src/v4/social/providers/ExploreProvider.tsx | 118 ++++++ 195 files changed, 5725 insertions(+), 1098 deletions(-) create mode 100644 src/v4/core/hooks/collections/useCategoriesCollection.ts create mode 100644 src/v4/core/hooks/collections/useRecommendedCommunitiesCollection.ts create mode 100644 src/v4/core/hooks/collections/useTrendingCommunitiesCollection.ts create mode 100644 src/v4/core/hooks/useCategory.ts create mode 100644 src/v4/core/natives/ClickableArea/ClickableArea.module.css create mode 100644 src/v4/core/natives/ClickableArea/ClickableArea.ts create mode 100644 src/v4/core/natives/ClickableArea/index.ts create mode 100644 src/v4/core/natives/Img/Img.tsx create mode 100644 src/v4/core/natives/Img/index.tsx create mode 100644 src/v4/icons/Brand.tsx create mode 100644 src/v4/icons/ChevronRight.tsx create mode 100644 src/v4/icons/People.tsx create mode 100644 src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.module.css create mode 100644 src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.stories.tsx create mode 100644 src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.tsx create mode 100644 src/v4/social/components/ExploreCommunityCategories/index.tsx create mode 100644 src/v4/social/components/ExploreCommunityEmpty/ExploreCommunityEmpty.module.css create mode 100644 src/v4/social/components/ExploreCommunityEmpty/ExploreCommunityEmpty.tsx create mode 100644 src/v4/social/components/ExploreCommunityEmpty/index.ts create mode 100644 src/v4/social/components/ExploreEmpty/ExploreEmpty.module.css create mode 100644 src/v4/social/components/ExploreEmpty/ExploreEmpty.tsx create mode 100644 src/v4/social/components/ExploreEmpty/index.ts create mode 100644 src/v4/social/components/RecommendedCommunities/RecommendedCommunities.module.css create mode 100644 src/v4/social/components/RecommendedCommunities/RecommendedCommunities.stories.tsx create mode 100644 src/v4/social/components/RecommendedCommunities/RecommendedCommunities.tsx create mode 100644 src/v4/social/components/RecommendedCommunities/RecommendedCommunityCardSkeleton.module.css create mode 100644 src/v4/social/components/RecommendedCommunities/RecommendedCommunityCardSkeleton.tsx create mode 100644 src/v4/social/components/RecommendedCommunities/index.tsx create mode 100644 src/v4/social/components/TrendingCommunities/TrendingCommunities.module.css create mode 100644 src/v4/social/components/TrendingCommunities/TrendingCommunities.stories.tsx create mode 100644 src/v4/social/components/TrendingCommunities/TrendingCommunities.tsx create mode 100644 src/v4/social/components/TrendingCommunities/index.tsx create mode 100644 src/v4/social/elements/AllCategoriesTitle/AllCategoriesTitle.module.css create mode 100644 src/v4/social/elements/AllCategoriesTitle/AllCategoriesTitle.tsx create mode 100644 src/v4/social/elements/AllCategoriesTitle/index.ts create mode 100644 src/v4/social/elements/CategoryChip/CategoryChip.module.css create mode 100644 src/v4/social/elements/CategoryChip/CategoryChip.tsx create mode 100644 src/v4/social/elements/CategoryChip/CategoryChipSkeleton.module.css create mode 100644 src/v4/social/elements/CategoryChip/CategoryChipSkeleton.tsx create mode 100644 src/v4/social/elements/CategoryChip/index.ts create mode 100644 src/v4/social/elements/CategoryTitle/CategoryTitle.module.css create mode 100644 src/v4/social/elements/CategoryTitle/CategoryTitle.tsx create mode 100644 src/v4/social/elements/CategoryTitle/index.ts create mode 100644 src/v4/social/elements/CommunityCardImage/CommunityCardImage.module.css create mode 100644 src/v4/social/elements/CommunityCardImage/CommunityCardImage.tsx create mode 100644 src/v4/social/elements/CommunityCardImage/index.ts create mode 100644 src/v4/social/elements/CommunityEmptyImage/CommunityEmptyImage.tsx create mode 100644 src/v4/social/elements/CommunityEmptyImage/index.ts create mode 100644 src/v4/social/elements/CommunityEmptyTitle/CommunityEmptyTitle.module.css create mode 100644 src/v4/social/elements/CommunityEmptyTitle/CommunityEmptyTitle.tsx create mode 100644 src/v4/social/elements/CommunityEmptyTitle/index.ts create mode 100644 src/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton.module.css create mode 100644 src/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton.tsx create mode 100644 src/v4/social/elements/CommunityJoinedButton/index.ts create mode 100644 src/v4/social/elements/CommunityRowImage/CommunityRowImage.module.css create mode 100644 src/v4/social/elements/CommunityRowImage/CommunityRowImage.tsx create mode 100644 src/v4/social/elements/CommunityRowImage/index.ts create mode 100644 src/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity.module.css create mode 100644 src/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity.tsx create mode 100644 src/v4/social/elements/ExploreCreateCommunity/index.tsx create mode 100644 src/v4/social/elements/ExploreEmptyImage/ExploreEmptyImage.tsx create mode 100644 src/v4/social/elements/ExploreEmptyImage/index.tsx create mode 100644 src/v4/social/elements/ExploreRecommendedTitle/ExploreRecommendedTitle.module.css create mode 100644 src/v4/social/elements/ExploreRecommendedTitle/ExploreRecommendedTitle.tsx create mode 100644 src/v4/social/elements/ExploreRecommendedTitle/index.tsx create mode 100644 src/v4/social/elements/ExploreTrendingTitle/ExploreTrendingTitle.module.css create mode 100644 src/v4/social/elements/ExploreTrendingTitle/ExploreTrendingTitle.tsx create mode 100644 src/v4/social/elements/ExploreTrendingTitle/index.tsx create mode 100644 src/v4/social/hooks/useCommunityActions.ts create mode 100644 src/v4/social/internal-components/BrandBadge/BrandBadge.module.css create mode 100644 src/v4/social/internal-components/BrandBadge/BrandBadge.stories.tsx create mode 100644 src/v4/social/internal-components/BrandBadge/BrandBadge.tsx create mode 100644 src/v4/social/internal-components/BrandBadge/index.tsx create mode 100644 src/v4/social/internal-components/CategoryImage/CategoryImage.module.css create mode 100644 src/v4/social/internal-components/CategoryImage/CategoryImage.tsx create mode 100644 src/v4/social/internal-components/CategoryImage/index.tsx create mode 100644 src/v4/social/internal-components/CommunityCategories/CommunityCategories.module.css create mode 100644 src/v4/social/internal-components/CommunityCategories/CommunityCategories.tsx create mode 100644 src/v4/social/internal-components/CommunityCategories/index.ts create mode 100644 src/v4/social/internal-components/CommunityRowItem/CommunityRowItem.module.css create mode 100644 src/v4/social/internal-components/CommunityRowItem/CommunityRowItem.tsx create mode 100644 src/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider.module.css create mode 100644 src/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider.tsx create mode 100644 src/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton.module.css create mode 100644 src/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton.tsx create mode 100644 src/v4/social/internal-components/CommunityRowItem/index.tsx create mode 100644 src/v4/social/internal-components/Lexical/__snapshots__/utils.test.ts.snap create mode 100644 src/v4/social/internal-components/Lexical/utils.test.ts create mode 100644 src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.module.css create mode 100644 src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.stories.tsx create mode 100644 src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.tsx create mode 100644 src/v4/social/pages/AllCategoriesPage/index.tsx create mode 100644 src/v4/social/pages/CommunitiesByCategoryPage/CommunitiesByCategoryPage.module.css create mode 100644 src/v4/social/pages/CommunitiesByCategoryPage/CommunitiesByCategoryPage.tsx create mode 100644 src/v4/social/pages/CommunitiesByCategoryPage/EmptyCommunity.module.css create mode 100644 src/v4/social/pages/CommunitiesByCategoryPage/EmptyCommunity.tsx create mode 100644 src/v4/social/pages/CommunitiesByCategoryPage/index.tsx create mode 100644 src/v4/social/pages/SocialHomePage/Explore.module.css create mode 100644 src/v4/social/pages/SocialHomePage/Explore.tsx create mode 100644 src/v4/social/pages/SocialHomePage/ExploreError.module.css create mode 100644 src/v4/social/pages/SocialHomePage/ExploreError.tsx create mode 100644 src/v4/social/providers/ExploreProvider.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a962ced..3e9e42837 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## 4.0.0-beta.15 (2024-10-04) + ## 4.0.0-beta.13 (2024-09-19) ## 4.0.0-beta.12 (2024-09-02) diff --git a/package.json b/package.json index 752ddd327..5c25d280d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@amityco/ui-kit-open-source", - "version": "4.0.0-beta.13", + "version": "4.0.0-beta.15", "engines": { "node": ">=20", "pnpm": "9" @@ -44,7 +44,7 @@ "react-dom": ">=17.0.2" }, "devDependencies": { - "@amityco/ts-sdk": "^6.30.0", + "@amityco/ts-sdk": "^6.30.4", "@eslint/js": "^9.4.0", "@storybook/addon-a11y": "^7.6.7", "@storybook/addon-actions": "^7.6.7", @@ -110,8 +110,8 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.3.4", - "@lexical/link": "^0.16.1", - "@lexical/react": "^0.16.1", + "@lexical/link": "^0.18.0", + "@lexical/react": "^0.18.0", "@radix-ui/react-tabs": "^1.0.4", "@tanstack/react-query": "^5.28.14", "clsx": "^2.1.0", @@ -120,7 +120,7 @@ "filesize": "^9.0.11", "framer-motion": "^11.1.7", "hls.js": "^1.4.14", - "lexical": "^0.16.1", + "lexical": "^0.18.0", "linkify-react": "^4.1.3", "linkifyjs": "^4.1.3", "lodash": "^4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12a18800c..11155766d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,11 +24,11 @@ importers: specifier: ^3.3.4 version: 3.6.0(react-hook-form@7.52.0(react@18.3.1)) '@lexical/link': - specifier: ^0.16.1 - version: 0.16.1 + specifier: ^0.18.0 + version: 0.18.0 '@lexical/react': - specifier: ^0.16.1 - version: 0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.18) + specifier: ^0.18.0 + version: 0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.18) '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@17.0.80)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -54,8 +54,8 @@ importers: specifier: ^1.4.14 version: 1.5.11 lexical: - specifier: ^0.16.1 - version: 0.16.1 + specifier: ^0.18.0 + version: 0.18.0 linkify-react: specifier: ^4.1.3 version: 4.1.3(linkifyjs@4.1.3)(react@18.3.1) @@ -136,8 +136,8 @@ importers: version: 3.23.8 devDependencies: '@amityco/ts-sdk': - specifier: ^6.30.0 - version: 6.30.1 + specifier: ^6.30.4 + version: 6.30.4 '@eslint/js': specifier: ^9.4.0 version: 9.7.0 @@ -315,8 +315,8 @@ packages: '@adobe/css-tools@4.3.3': resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} - '@amityco/ts-sdk@6.30.1': - resolution: {integrity: sha512-aD3e2wf0QdrT0qv62slB7TdBhKQkZv7IrUgX5U7yJt2BrjxM1wE0pMBE5Tx8bNalXwgnQFVtbeJkSmrZ8XCZUQ==} + '@amityco/ts-sdk@6.30.4': + resolution: {integrity: sha512-2cFjm27ZPJnOpuvWiC11W+bA8G9thmzFwISl1UHCyYMY9vxIlmZyf1BpS8ydOulOX44T855F6W1vElZXjmKnaA==} engines: {node: '>=12', npm: '>=6'} '@ampproject/remapping@2.3.0': @@ -1543,74 +1543,74 @@ packages: '@juggle/resize-observer@3.4.0': resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} - '@lexical/clipboard@0.16.1': - resolution: {integrity: sha512-0dWs/SwKS5KPpuf6fUVVt9vSCl6HAqcDGhSITw/okv0rrIlXTUT6WhVsMJtXfFxTyVvwMeOecJHvQH3i/jRQtA==} + '@lexical/clipboard@0.18.0': + resolution: {integrity: sha512-ybc+hx14wj0n2ZjdOkLcZ02MRB3UprXjpLDXlByFIuVcZpUxVcp3NzA0UBPOKXYKvdt0bmgjnAsFWM5OSbwS0w==} - '@lexical/code@0.16.1': - resolution: {integrity: sha512-pOC28rRZ2XkmI2nIJm50DbKaCJtk5D0o7r6nORYp4i0z+lxt5Sf2m82DL9ksUHJRqKy87pwJDpoWvJ2SAI0ohw==} + '@lexical/code@0.18.0': + resolution: {integrity: sha512-VB8fRHIrB8QTqyZUvGBMVWP2tpKe3ArOjPdWAqgrS8MVFldqUhuTHcW+XJFkVxcEBYCXynNT29YRYtQhfQ+vDQ==} - '@lexical/devtools-core@0.16.1': - resolution: {integrity: sha512-8CvGERGL7ySDVGLU+YPeq+JupIXsOFlXa3EuJ88koLKqXxYenwMleZgGqayFp6lCP78xqPKnATVeoOZUt/NabQ==} + '@lexical/devtools-core@0.18.0': + resolution: {integrity: sha512-gVgtEkLwGjz1frOmDpFJzDPFxPgAcC9n5ZaaZWHo5GLcptnQmkuLm1t+UInQWujXhFmcyJzfiqDaMJ8EIcb2Ww==} peerDependencies: react: '>=17.x' react-dom: '>=17.x' - '@lexical/dragon@0.16.1': - resolution: {integrity: sha512-Rvd60GIYN5kpjjBumS34EnNbBaNsoseI0AlzOdtIV302jiHPCLH0noe9kxzu9nZy+MZmjZy8Dx2zTbQT2mueRw==} + '@lexical/dragon@0.18.0': + resolution: {integrity: sha512-toD/y2/TgtG+eFVKXf65kDk/Mv02FwgmcGH18nyAabZnO1TLBaMYPkGFdTTZ8hVmQxqIu9nZuLWUbdIBMs8UWw==} - '@lexical/hashtag@0.16.1': - resolution: {integrity: sha512-G+YOxStAKs3q1utqm9KR4D4lCkwIH52Rctm4RgaVTI+4lvTaybeDRGFV75P/pI/qlF7/FvAYHTYEzCjtC3GNMQ==} + '@lexical/hashtag@0.18.0': + resolution: {integrity: sha512-bm+Sv7keguVYbUY0ngd+iAv2Owd3dePzdVkzkmw9Al8GPXkE5ll8fjq6Xjw2u3OVhf+9pTnesIo/AS7H+h0exw==} - '@lexical/history@0.16.1': - resolution: {integrity: sha512-WQhScx0TJeKSQAnEkRpIaWdUXqirrNrom2MxbBUc/32zEUMm9FzV7nRGknvUabEFUo7vZq6xTZpOExQJqHInQA==} + '@lexical/history@0.18.0': + resolution: {integrity: sha512-c87J4ke1Sae03coElJay2Ikac/4OcA2OmhtNbt2gAi/XBtcsP4mPuz1yZfZf9XIe+weekObgjinvZekQ2AFw0g==} - '@lexical/html@0.16.1': - resolution: {integrity: sha512-vbtAdCvQ3PaAqa5mFmtmrvbiAvjCu1iXBAJ0bsHqFXCF2Sba5LwHVe8dUAOTpfEZEMbiHfjul6b5fj4vNPGF2A==} + '@lexical/html@0.18.0': + resolution: {integrity: sha512-8lhba1DFnnobXgYm4Rk5Gr2tZedD4Gl6A/NKCt7whO/CET63vT3UnK2ggcVVgtIJG530Cv0bdZoJbJu5DauI5w==} - '@lexical/link@0.16.1': - resolution: {integrity: sha512-zG36gEnEqbIe6tK/MhXi7wn/XMY/zdivnPcOY5WyC3derkEezeLSSIFsC1u5UNeK5pbpNMSy4LDpLhi1Ww4Y5w==} + '@lexical/link@0.18.0': + resolution: {integrity: sha512-GCYcbNTSTwJk0lr+GMc8nn6Meq44BZs3QL2d1B0skpZAspd8yI53sRS6HDy5P+jW5P0dzyZr/XJAU4U+7zsEEg==} - '@lexical/list@0.16.1': - resolution: {integrity: sha512-i9YhLAh5N6YO9dP+R1SIL9WEdCKeTiQQYVUzj84vDvX5DIBxMPUjTmMn3LXu9T+QO3h1s2L/vJusZASrl45eAw==} + '@lexical/list@0.18.0': + resolution: {integrity: sha512-DEWs9Scbg3+STZeE2O0OoG8SWnKnxQccObBzyeHRjn4GAN6JA7lgcAzfrdgp0fNWTbMM/ku876MmXKGnqhvg9Q==} - '@lexical/mark@0.16.1': - resolution: {integrity: sha512-CZRGMLcxn5D+jzf1XnH+Z+uUugmpg1mBwTbGybCPm8UWpBrKDHkrscfMgWz62iRWz0cdVjM5+0zWpNElxFTRjQ==} + '@lexical/mark@0.18.0': + resolution: {integrity: sha512-QA4YWfTP5WWnCnoH/RmfcsSZyhhd7oeFWDpfP7S8Bbmhz6kiPwGcsVr+uRQBBT56AqEX167xX2rX8JR6FiYZqA==} - '@lexical/markdown@0.16.1': - resolution: {integrity: sha512-0sBLttMvfQO/hVaIqpHdvDowpgV2CoRuWo2CNwvRLZPPWvPVjL4Nkb73wmi8zAZsAOTbX2aw+g4m/+k5oJqNig==} + '@lexical/markdown@0.18.0': + resolution: {integrity: sha512-uSWwcK8eJw5C+waEhU5WoX8W+JxNZbKuFnZwsn5nsp+iQgqMj4qY6g0yJub4sq8vvh6jjl4vVXhXTq2up9aykw==} - '@lexical/offset@0.16.1': - resolution: {integrity: sha512-/i2J04lQmFeydUZIF8tKXLQTXiJDTQ6GRnkfv1OpxU4amc0rwGa7+qAz/PuF1n58rP6InpLmSHxgY5JztXa2jw==} + '@lexical/offset@0.18.0': + resolution: {integrity: sha512-KGlboyLSxQAH5PMOlJmyvHlbYXZneVnKiHpfyBV5IUX5kuyB/eZbQEYcJP9saekfQ5Xb1FWXWmsZEo+sWtrrZA==} - '@lexical/overflow@0.16.1': - resolution: {integrity: sha512-xh5YpoxwA7K4wgMQF/Sjl8sdjaxqesLCtH5ZrcMsaPlmucDIEEs+i8xxk+kDUTEY7y+3FvRxs4lGNgX8RVWkvQ==} + '@lexical/overflow@0.18.0': + resolution: {integrity: sha512-3ATTwttVgZtVLq60ZUWbpbXBbpuMa3PZD5CxSP3nulviL+2I4phvacV4WUN+8wMeq+PGmuarl+cYfrFL02ii3g==} - '@lexical/plain-text@0.16.1': - resolution: {integrity: sha512-GjY4ylrBZIaAVIF8IFnmW0XGyHAuRmWA6gKB8iTTlsjgFrCHFIYC74EeJSp309O0Hflg9rRBnKoX1TYruFHVwA==} + '@lexical/plain-text@0.18.0': + resolution: {integrity: sha512-L6yQpiwW0ZacY1oNwvRBxSuW2TZaUcveZLheJc8JzGcZoVxzII/CAbLZG8691VbNuKsbOURiNXZIsgwujKmo4Q==} - '@lexical/react@0.16.1': - resolution: {integrity: sha512-SsGgLt9iKfrrMRy9lFb6ROVPUYOgv6b+mCn9Al+TLqs/gBReDBi3msA7m526nrtBUKYUnjHdQ1QXIJzuKgOxcg==} + '@lexical/react@0.18.0': + resolution: {integrity: sha512-DLvIbTsjvFIFqm+9zvAjEwuZHAbSxzZf1AGqf1lLctlL/Ran0f+8EZOv5jttELTe7xISZ2+xSXTLRfyxhNwGXQ==} peerDependencies: react: '>=17.x' react-dom: '>=17.x' - '@lexical/rich-text@0.16.1': - resolution: {integrity: sha512-4uEVXJur7tdSbqbmsToCW4YVm0AMh4y9LK077Yq2O9hSuA5dqpI8UbTDnxZN2D7RfahNvwlqp8eZKFB1yeiJGQ==} + '@lexical/rich-text@0.18.0': + resolution: {integrity: sha512-xMANCB7WueMsmWK8qxik5FZN4ApyaHWHQILS9r4FTbdv/DlNepsR7Pt8kg2317xZ56NAueQLIdyyKYXG1nBrHw==} - '@lexical/selection@0.16.1': - resolution: {integrity: sha512-+nK3RvXtyQvQDq7AZ46JpphmM33pwuulwiRfeXR5T9iFQTtgWOEjsAi/KKX7vGm70BxACfiSxy5QCOgBWFwVJg==} + '@lexical/selection@0.18.0': + resolution: {integrity: sha512-mJoMhmxeZLfM9K2JMYETs9u179IkHQUlgtYG5GZJHjKx2iUn+9KvJ9RVssq+Lusi7C/N42wWPGNHDPdUvFtxXg==} - '@lexical/table@0.16.1': - resolution: {integrity: sha512-GWb0/MM1sVXpi1p2HWWOBldZXASMQ4c6WRNYnRmq7J/aB5N66HqQgJGKp3m66Kz4k1JjhmZfPs7F018qIBhnFQ==} + '@lexical/table@0.18.0': + resolution: {integrity: sha512-TeTAnuFAAgVjm1QE8adRB3GFWN+DUUiS4vzGq+ynPRCtNdpmW27NmTkRMyxKsetUtt7nIFfj4DvLvor4RwqIpA==} - '@lexical/text@0.16.1': - resolution: {integrity: sha512-Os/nKQegORTrKKN6vL3/FMVszyzyqaotlisPynvTaHTUC+yY4uyjM2hlF93i5a2ixxyiPLF9bDroxUP96TMPXg==} + '@lexical/text@0.18.0': + resolution: {integrity: sha512-MTHSBeq3K0+lqSsP5oysBMnY4tPVhB8kAa2xBnEc3dYgXFxEEvJwZahbHNX93EPObtJkxXfUuI63Al4G3lYK8A==} - '@lexical/utils@0.16.1': - resolution: {integrity: sha512-BVyJxDQi/rIxFTDjf2zE7rMDKSuEaeJ4dybHRa/hRERt85gavGByQawSLeQlTjLaYLVsy+x7wCcqh2fNhlLf0g==} + '@lexical/utils@0.18.0': + resolution: {integrity: sha512-4s9dVpBZjqIaA/1q2GtfWFjKsv2Wqhjer0Zw2mcl1TIVN0zreXxcTKN316QppAWmSQJxVGvkWHjjaZJwl6/TSw==} - '@lexical/yjs@0.16.1': - resolution: {integrity: sha512-QHw1bmzB/IypIV1tRWMH4hhwE1xX7wV+HxbzBS8oJAkoU5AYXM/kyp/sQicgqiwVfpai1Px7zatOoUDFgbyzHQ==} + '@lexical/yjs@0.18.0': + resolution: {integrity: sha512-rl7Rl9XIb3ygQEEHOFtACdXs3BE+UUUmdyNqB6kK9A6IRGz+w4Azp+qzt8It/t+c0oaSYHpAtcLNXg1amJz+kA==} peerDependencies: yjs: '>=13.5.22' @@ -5505,8 +5505,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lexical@0.16.1: - resolution: {integrity: sha512-+R05d3+N945OY8pTUjTqQrWoApjC+ctzvjnmNETtx9WmVAaiW0tQVG+AYLt5pDGY8dQXtd4RPorvnxBTECt9SA==} + lexical@0.18.0: + resolution: {integrity: sha512-3K/B0RpzjoW+Wj2E455wWXxkqxqK8UgdIiuqkOqdOsoSSo5mCkHOU6eVw7Nlmlr1MFvAMzGmz4RPn8NZaLQ2Mw==} lib0@0.2.94: resolution: {integrity: sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==} @@ -6329,7 +6329,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qs@6.11.0: @@ -7768,7 +7767,7 @@ snapshots: '@adobe/css-tools@4.3.3': {} - '@amityco/ts-sdk@6.30.1': + '@amityco/ts-sdk@6.30.4': dependencies: agentkeepalive: 4.5.0 axios: 1.7.2(debug@4.3.5) @@ -9180,149 +9179,151 @@ snapshots: '@juggle/resize-observer@3.4.0': {} - '@lexical/clipboard@0.16.1': + '@lexical/clipboard@0.18.0': dependencies: - '@lexical/html': 0.16.1 - '@lexical/list': 0.16.1 - '@lexical/selection': 0.16.1 - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/html': 0.18.0 + '@lexical/list': 0.18.0 + '@lexical/selection': 0.18.0 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/code@0.16.1': + '@lexical/code@0.18.0': dependencies: - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 prismjs: 1.29.0 - '@lexical/devtools-core@0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@lexical/devtools-core@0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@lexical/html': 0.16.1 - '@lexical/link': 0.16.1 - '@lexical/mark': 0.16.1 - '@lexical/table': 0.16.1 - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/html': 0.18.0 + '@lexical/link': 0.18.0 + '@lexical/mark': 0.18.0 + '@lexical/table': 0.18.0 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@lexical/dragon@0.16.1': + '@lexical/dragon@0.18.0': dependencies: - lexical: 0.16.1 + lexical: 0.18.0 - '@lexical/hashtag@0.16.1': + '@lexical/hashtag@0.18.0': dependencies: - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/history@0.16.1': + '@lexical/history@0.18.0': dependencies: - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/html@0.16.1': + '@lexical/html@0.18.0': dependencies: - '@lexical/selection': 0.16.1 - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/selection': 0.18.0 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/link@0.16.1': + '@lexical/link@0.18.0': dependencies: - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/list@0.16.1': + '@lexical/list@0.18.0': dependencies: - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/mark@0.16.1': + '@lexical/mark@0.18.0': dependencies: - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/markdown@0.16.1': + '@lexical/markdown@0.18.0': dependencies: - '@lexical/code': 0.16.1 - '@lexical/link': 0.16.1 - '@lexical/list': 0.16.1 - '@lexical/rich-text': 0.16.1 - '@lexical/text': 0.16.1 - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/code': 0.18.0 + '@lexical/link': 0.18.0 + '@lexical/list': 0.18.0 + '@lexical/rich-text': 0.18.0 + '@lexical/text': 0.18.0 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/offset@0.16.1': + '@lexical/offset@0.18.0': dependencies: - lexical: 0.16.1 + lexical: 0.18.0 - '@lexical/overflow@0.16.1': + '@lexical/overflow@0.18.0': dependencies: - lexical: 0.16.1 + lexical: 0.18.0 - '@lexical/plain-text@0.16.1': + '@lexical/plain-text@0.18.0': dependencies: - '@lexical/clipboard': 0.16.1 - '@lexical/selection': 0.16.1 - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/clipboard': 0.18.0 + '@lexical/selection': 0.18.0 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/react@0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.18)': + '@lexical/react@0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.18)': dependencies: - '@lexical/clipboard': 0.16.1 - '@lexical/code': 0.16.1 - '@lexical/devtools-core': 0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@lexical/dragon': 0.16.1 - '@lexical/hashtag': 0.16.1 - '@lexical/history': 0.16.1 - '@lexical/link': 0.16.1 - '@lexical/list': 0.16.1 - '@lexical/mark': 0.16.1 - '@lexical/markdown': 0.16.1 - '@lexical/overflow': 0.16.1 - '@lexical/plain-text': 0.16.1 - '@lexical/rich-text': 0.16.1 - '@lexical/selection': 0.16.1 - '@lexical/table': 0.16.1 - '@lexical/text': 0.16.1 - '@lexical/utils': 0.16.1 - '@lexical/yjs': 0.16.1(yjs@13.6.18) - lexical: 0.16.1 + '@lexical/clipboard': 0.18.0 + '@lexical/code': 0.18.0 + '@lexical/devtools-core': 0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@lexical/dragon': 0.18.0 + '@lexical/hashtag': 0.18.0 + '@lexical/history': 0.18.0 + '@lexical/link': 0.18.0 + '@lexical/list': 0.18.0 + '@lexical/mark': 0.18.0 + '@lexical/markdown': 0.18.0 + '@lexical/overflow': 0.18.0 + '@lexical/plain-text': 0.18.0 + '@lexical/rich-text': 0.18.0 + '@lexical/selection': 0.18.0 + '@lexical/table': 0.18.0 + '@lexical/text': 0.18.0 + '@lexical/utils': 0.18.0 + '@lexical/yjs': 0.18.0(yjs@13.6.18) + lexical: 0.18.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-error-boundary: 3.1.4(react@18.3.1) transitivePeerDependencies: - yjs - '@lexical/rich-text@0.16.1': + '@lexical/rich-text@0.18.0': dependencies: - '@lexical/clipboard': 0.16.1 - '@lexical/selection': 0.16.1 - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/clipboard': 0.18.0 + '@lexical/selection': 0.18.0 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/selection@0.16.1': + '@lexical/selection@0.18.0': dependencies: - lexical: 0.16.1 + lexical: 0.18.0 - '@lexical/table@0.16.1': + '@lexical/table@0.18.0': dependencies: - '@lexical/utils': 0.16.1 - lexical: 0.16.1 + '@lexical/clipboard': 0.18.0 + '@lexical/utils': 0.18.0 + lexical: 0.18.0 - '@lexical/text@0.16.1': + '@lexical/text@0.18.0': dependencies: - lexical: 0.16.1 + lexical: 0.18.0 - '@lexical/utils@0.16.1': + '@lexical/utils@0.18.0': dependencies: - '@lexical/list': 0.16.1 - '@lexical/selection': 0.16.1 - '@lexical/table': 0.16.1 - lexical: 0.16.1 + '@lexical/list': 0.18.0 + '@lexical/selection': 0.18.0 + '@lexical/table': 0.18.0 + lexical: 0.18.0 - '@lexical/yjs@0.16.1(yjs@13.6.18)': + '@lexical/yjs@0.18.0(yjs@13.6.18)': dependencies: - '@lexical/offset': 0.16.1 - lexical: 0.16.1 + '@lexical/offset': 0.18.0 + '@lexical/selection': 0.18.0 + lexical: 0.18.0 yjs: 13.6.18 '@lokesh.dhakar/quantize@1.3.0': {} @@ -14414,7 +14415,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lexical@0.16.1: {} + lexical@0.18.0: {} lib0@0.2.94: dependencies: diff --git a/readme.md b/readme.md index d737c5511..ef335f14c 100644 --- a/readme.md +++ b/readme.md @@ -2,20 +2,135 @@ ## Prerequisites -Before starting to work, please read the following instructions. -https://ekoapp.atlassian.net/wiki/spaces/UP/pages/2443706407/ASC+Web+UIKit+V4+Governance - -If you have any questions, please ask / discuss with the team. - -### Installation +Before getting started, ensure that you have the following prerequisites installed on your system: - [Node.js](https://nodejs.org/) LTS version (currently version 20) - [pnpm](https://pnpm.io/) version 8 ## How to install PNPM (Optional) -Please refer to our online documentation at https://docs.amity.co or contact a Ui-Kit representative at \* \*developers@amity.co** for support. +``` +corepack enable pnpm +``` + +Ref: https://pnpm.io/installation#using-corepack + +## Running Storybook (Optional) + +To run Storybook and view the UI components in isolation, follow these steps: + +1. Clone the Amity UI-Kit repository: + + ``` + git clone https://github.com/AmityCo/Amity-Social-Cloud-UIKit-Web-OpenSource.git + ``` + +2. Navigate to the cloned repository's directory: + + ``` + cd Amity-Social-Cloud-UIKit-Web-OpenSource + ``` + +3. Install the dependencies using pnpm: + + ``` + pnpm install + ``` + +4. Create a `.env` file at the root of the project with the following content: + + ``` + STORYBOOK_API_REGION= + STORYBOOK_API_KEY= + ``` + + Replace `` and `` with your actual credentials. + +5. Run Storybook: + + ``` + pnpm run storybook + ``` + +6. Open your browser and navigate to `http://localhost:6006` to view the Storybook interface. + +## Installation + +To install the Amity UI-Kit together with another project, follow these steps: + +1. Clone the repository using the following command: + + ``` + git clone https://github.com/AmityCo/Amity-Social-Cloud-UIKit-Web-OpenSource.git + ``` + +2. Navigate to the cloned repository's directory: + + ``` + cd ./Amity-Social-Cloud-UIKit-Web-OpenSource + ``` + +3. Install the dependencies using pnpm: + + ``` + pnpm install + ``` + +4. Build the project: + + ``` + pnpm run build + ``` + +5. Pack the project + + ``` + pnpm pack + ``` + +6. Navigate to your application's directory: + + ``` + cd + ``` + +7. Install the Amity UI-Kit to your application using one of the following package managers: + - NPM: + ``` + npm i file:/ --save + ``` + - Yarn (Classic): + ``` + yarn add file:/ + ``` + - PNPM: + ``` + pnpm i file:/ + ``` + +## Documentation + +For detailed information and guidance on using the Amity UI-Kit, please refer to our comprehensive online documentation available at [https://docs.amity.co](https://docs.amity.co). + +If you require further assistance or have any questions, please don't hesitate to contact our dedicated UI-Kit support team at **developers@amity.co**. We are here to help you make the most of the Amity UI-Kit. ## Contributing -See [our contributing guide](https://github.com/EkoCommunications/AmityUiKitWeb/blob/develop/CONTRIBUTING.md) +We welcome contributions from the community to help improve and enhance the Amity UI-Kit. If you are interested in contributing to this project, please review our [contributing guide](https://github.com/AmityCo/Amity-Social-Cloud-UIKit-Web-OpenSource/blob/develop/contributing.md) for guidelines and best practices. + +Thank you for choosing the Amity UI-Kit for your web development needs! + +### FAQ + +Q: I tried to run `pnpm build` and it throws a types error. +A: Try to structure your project to be like this: + +``` +- your_app + - src +- Amity-Social-Cloud-UIKit-Web-OpenSource + - src +``` + +Q: The modifications I made to the code do not appear to be applied. +A: Please attempt to execute `npm cache clean` or `npm cache clean --force` to resolve this issue. diff --git a/src/social/components/CommunityForm/EditCommunityForm.tsx b/src/social/components/CommunityForm/EditCommunityForm.tsx index 177d333bc..5030fc628 100644 --- a/src/social/components/CommunityForm/EditCommunityForm.tsx +++ b/src/social/components/CommunityForm/EditCommunityForm.tsx @@ -96,6 +96,14 @@ const EditCommunityForm = ({ await onSubmit?.({ ...data, avatarFileId: data.avatarFileId || undefined }); notification.success({ content: }); + } catch (error) { + console.log('error', error); + if (error instanceof Error) { + if (error.message.indexOf(':') > -1) { + const [, errorMessage] = error.message.split(':'); + notification.error({ content: errorMessage }); + } + } } finally { setSubmitting(false); } diff --git a/src/social/components/SideSectionMyCommunity/index.tsx b/src/social/components/SideSectionMyCommunity/index.tsx index a8ebdeb54..2d975c4c7 100644 --- a/src/social/components/SideSectionMyCommunity/index.tsx +++ b/src/social/components/SideSectionMyCommunity/index.tsx @@ -23,7 +23,6 @@ const SideSectionMyCommunity = ({ className, activeCommunity }: SideSectionMyCom const open = () => setIsOpen(true); const close = (communityId?: string) => { - console.log('communityId', communityId); setIsOpen(false); communityId && onCommunityCreated(communityId); }; diff --git a/src/social/components/UserHeader/UIUserHeader.tsx b/src/social/components/UserHeader/UIUserHeader.tsx index 34cefd118..adb43bf92 100644 --- a/src/social/components/UserHeader/UIUserHeader.tsx +++ b/src/social/components/UserHeader/UIUserHeader.tsx @@ -9,6 +9,7 @@ import { UserHeaderTitle, } from './styles'; import { useCustomComponent } from '~/core/providers/CustomComponentsProvider'; +import { BrandBadge } from '~/v4/social/internal-components/BrandBadge/BrandBadge'; interface UIUserHeaderProps { userId?: string | null; @@ -16,6 +17,7 @@ interface UIUserHeaderProps { avatarFileUrl?: string | null; children?: ReactNode; isBanned?: boolean; + isBrand?: boolean; onClick?: (userId: string) => void; } @@ -26,6 +28,7 @@ const UIUserHeader = ({ children, onClick, isBanned, + isBrand, }: UIUserHeaderProps) => { const onClickUser = () => userId && onClick?.(userId); return ( @@ -36,7 +39,7 @@ const UIUserHeader = ({ onClick={onClickUser} /> -
{displayName}
{isBanned && } +
{displayName}
{isBanned && } {isBrand && }
{children && {children}} diff --git a/src/social/components/UserHeader/index.tsx b/src/social/components/UserHeader/index.tsx index d25230fa5..5392d58f3 100644 --- a/src/social/components/UserHeader/index.tsx +++ b/src/social/components/UserHeader/index.tsx @@ -22,6 +22,7 @@ const UserHeader = ({ userId, children, onClick, isBanned = false }: UserHeaderP displayName={user?.displayName} avatarFileUrl={avatarFileUrl} isBanned={isBanned} + isBrand={user?.isBrand} onClick={onClick} > {children} diff --git a/src/social/components/UserHeader/styles.tsx b/src/social/components/UserHeader/styles.tsx index 7ea4a01f7..067e0cb14 100644 --- a/src/social/components/UserHeader/styles.tsx +++ b/src/social/components/UserHeader/styles.tsx @@ -27,6 +27,7 @@ export const UserHeaderTitle = styled.div` display: flex; min-width: 0; align-items: center; + gap: 8px; > div { text-overflow: ellipsis; diff --git a/src/social/pages/CommunityEdit/index.tsx b/src/social/pages/CommunityEdit/index.tsx index 98733bd07..a2e3e21db 100644 --- a/src/social/pages/CommunityEdit/index.tsx +++ b/src/social/pages/CommunityEdit/index.tsx @@ -33,11 +33,11 @@ const CommunityEditPage = ({ useEffect(() => setActiveTab(tab), [tab]); - const { onClickCommunity } = useNavigation(); + const { onBack } = useNavigation(); const community = useCommunity(communityId); const avatarFileUrl = useImage({ fileId: community?.avatarFileId, imageSize: 'medium' }); - const handleReturnToCommunity = () => communityId && onClickCommunity(communityId); + const handleReturnToCommunity = () => communityId && onBack(); const handleEditCommunity = async ( data: Parameters[1], diff --git a/src/v4/chat/components/MessageComposer/MessageComposer.tsx b/src/v4/chat/components/MessageComposer/MessageComposer.tsx index 9d92de65b..2bad850a4 100644 --- a/src/v4/chat/components/MessageComposer/MessageComposer.tsx +++ b/src/v4/chat/components/MessageComposer/MessageComposer.tsx @@ -23,7 +23,7 @@ import { MentionPlugin } from '~/v4/social/internal-components/Lexical/plugins/M import { useMutation } from '@tanstack/react-query'; import { - editorStateToText, + editorToText, getEditorConfig, MentionData, } from '~/v4/social/internal-components/Lexical/utils'; @@ -148,7 +148,7 @@ export const MessageComposer = ({ if (!channel) return; if (!editorRef.current) return; - const { mentioned, mentionees, text } = editorStateToText(editorRef.current); + const { mentioned, mentionees, text } = editorToText(editorRef.current); if (text?.trim().length === 0) return; diff --git a/src/v4/core/components/Avatar/Avatar.tsx b/src/v4/core/components/Avatar/Avatar.tsx index 6c49890aa..03bd71083 100644 --- a/src/v4/core/components/Avatar/Avatar.tsx +++ b/src/v4/core/components/Avatar/Avatar.tsx @@ -1,15 +1,32 @@ import React from 'react'; import styles from './Avatar.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; export interface AvatarProps { + pageId?: string; + componentId?: string; avatarUrl?: string | null; defaultImage: React.ReactNode; onClick?: () => void; } -export const Avatar = ({ avatarUrl, defaultImage, onClick }: AvatarProps) => { +export const Avatar = ({ + pageId = '*', + componentId = '*', + avatarUrl, + defaultImage, + onClick, +}: AvatarProps) => { + const elementId = 'avatar'; + + const { accessibilityId } = useAmityElement({ pageId, componentId, elementId }); return ( -
+
{avatarUrl ? ( // TODO: add handler if cannot fetch the url Avatar diff --git a/src/v4/core/components/ConfirmModal/index.tsx b/src/v4/core/components/ConfirmModal/index.tsx index 0d39ae041..d3e0fff97 100644 --- a/src/v4/core/components/ConfirmModal/index.tsx +++ b/src/v4/core/components/ConfirmModal/index.tsx @@ -18,7 +18,6 @@ interface ConfirmProps extends ConfirmType { const Confirm = ({ pageId = '*', componentId = '*', - elementId = '*', className, title, content, @@ -28,6 +27,7 @@ const Confirm = ({ onCancel, type = 'confirm', }: ConfirmProps) => { + const elementId = 'confirm_modal'; const { accessibilityId, themeStyles } = useAmityElement({ pageId, componentId, elementId }); return ( diff --git a/src/v4/core/components/Modal/index.tsx b/src/v4/core/components/Modal/index.tsx index 925f356e9..c7515a985 100644 --- a/src/v4/core/components/Modal/index.tsx +++ b/src/v4/core/components/Modal/index.tsx @@ -45,10 +45,25 @@ const Modal = ({ ref={modalRef} tabIndex={0} > - {onCancel && } - {title &&
{title}
} + {onCancel && ( + + )} + {title && ( +
+ {title} +
+ )} -
{children}
+
+ {children} +
{footer &&
{footer}
}
diff --git a/src/v4/core/components/Typography/Typography.tsx b/src/v4/core/components/Typography/Typography.tsx index 60e8e18f0..80240d72c 100644 --- a/src/v4/core/components/Typography/Typography.tsx +++ b/src/v4/core/components/Typography/Typography.tsx @@ -2,110 +2,160 @@ import React from 'react'; import clsx from 'clsx'; import typography from '~/v4/styles/typography.module.css'; -interface TypographyProps { - children: React.ReactNode; - className?: string; - style?: React.CSSProperties; -} - -const Typography: React.FC & { - Heading: React.FC; - Title: React.FC; - Subtitle: React.FC; - Body: React.FC; - BodyBold: React.FC; - Caption: React.FC; - CaptionBold: React.FC; -} = ({ children, className = '', style, ...rest }) => { - return ( -
- {children} -
- ); -}; +type TypographyRenderer = ({ typoClassName }: { typoClassName: string }) => JSX.Element; -Typography.Heading = ({ children, className = '', style, ...rest }) => { - return ( -

- {children} -

- ); -}; +type TypographyProps = + | { + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; + } + | { renderer: TypographyRenderer }; -Typography.Title = ({ children, className = '', style, ...rest }) => { - return ( -

- {children} -

- ); +const isRendererProp = (props: TypographyProps): props is { renderer: TypographyRenderer } => { + return (props as { renderer: TypographyRenderer }).renderer !== undefined; }; -Typography.Subtitle = ({ children, className = '', style, ...rest }) => { - return ( -

- {children} -

- ); -}; +export const Typography = { + Heading: (props: TypographyProps) => { + if (isRendererProp(props)) { + return props.renderer({ + typoClassName: clsx(typography['typography'], typography['typography-headings']), + }); + } -Typography.Body = ({ children, className = '', style, ...rest }) => { - return ( - - {children} - - ); -}; + const { children, className, style, ...rest } = props; -Typography.BodyBold = ({ children, className = '', style, ...rest }) => { - return ( - - {children} - - ); -}; + return ( +

+ {children} +

+ ); + }, -Typography.Caption = ({ children, className = '', style, ...rest }) => { - return ( - - {children} - - ); -}; + Title: (props: TypographyProps) => { + if (isRendererProp(props)) { + return props.renderer({ + typoClassName: clsx(typography['typography'], typography['typography-titles']), + }); + } + + const { children, className, style, ...rest } = props; + + return ( +

+ {children} +

+ ); + }, + + Subtitle: (props: TypographyProps) => { + if (isRendererProp(props)) { + return props.renderer({ + typoClassName: clsx(typography['typography'], typography['typography-sub-title']), + }); + } + + const { children, className, style, ...rest } = props; + + return ( +

+ {children} +

+ ); + }, + + Body: (props: TypographyProps) => { + if (isRendererProp(props)) { + return props.renderer({ + typoClassName: clsx(typography['typography'], typography['typography-body']), + }); + } + + const { children, className, style, ...rest } = props; + + return ( + + {children} + + ); + }, + + BodyBold: (props: TypographyProps) => { + if (isRendererProp(props)) { + return props.renderer({ + typoClassName: clsx(typography['typography'], typography['typography-body-bold']), + }); + } + + const { children, className, style, ...rest } = props; + + return ( + + {children} + + ); + }, + + Caption: (props: TypographyProps) => { + if (isRendererProp(props)) { + return props.renderer({ + typoClassName: clsx(typography['typography'], typography['typography-caption']), + }); + } + + const { children, className, style, ...rest } = props; + + return ( + + {children} + + ); + }, + + CaptionBold: (props: TypographyProps) => { + if (isRendererProp(props)) { + return props.renderer({ + typoClassName: clsx(typography['typography'], typography['typography-caption-bold']), + }); + } + + const { children, className, style, ...rest } = props; -Typography.CaptionBold = ({ children, className = '', style, ...rest }) => { - return ( - - {children} - - ); + return ( + + {children} + + ); + }, }; export default Typography; diff --git a/src/v4/core/hooks/collections/useCategoriesCollection.ts b/src/v4/core/hooks/collections/useCategoriesCollection.ts new file mode 100644 index 000000000..c5b2de313 --- /dev/null +++ b/src/v4/core/hooks/collections/useCategoriesCollection.ts @@ -0,0 +1,22 @@ +import { CategoryRepository } from '@amityco/ts-sdk'; + +import useLiveCollection from '~/v4/core/hooks/useLiveCollection'; + +export default function useCategoriesCollection({ + query, + enabled, +}: { + query: Amity.CategoryLiveCollection; + enabled?: boolean; +}) { + const { items, ...rest } = useLiveCollection({ + fetcher: CategoryRepository.getCategories, + params: query, + shouldCall: enabled, + }); + + return { + categories: items, + ...rest, + }; +} diff --git a/src/v4/core/hooks/collections/useRecommendedCommunitiesCollection.ts b/src/v4/core/hooks/collections/useRecommendedCommunitiesCollection.ts new file mode 100644 index 000000000..eb74e336e --- /dev/null +++ b/src/v4/core/hooks/collections/useRecommendedCommunitiesCollection.ts @@ -0,0 +1,21 @@ +import { CommunityRepository } from '@amityco/ts-sdk'; +import useLiveCollection from '~/v4/core/hooks/useLiveCollection'; + +export function useRecommendedCommunitiesCollection({ + params, + enabled = true, +}: { + params: Parameters[0]; + enabled?: boolean; +}) { + const { items, ...rest } = useLiveCollection({ + fetcher: CommunityRepository.getRecommendedCommunities, + params, + shouldCall: enabled, + }); + + return { + recommendedCommunities: items, + ...rest, + }; +} diff --git a/src/v4/core/hooks/collections/useTrendingCommunitiesCollection.ts b/src/v4/core/hooks/collections/useTrendingCommunitiesCollection.ts new file mode 100644 index 000000000..114d6ecdf --- /dev/null +++ b/src/v4/core/hooks/collections/useTrendingCommunitiesCollection.ts @@ -0,0 +1,24 @@ +import { CommunityRepository } from '@amityco/ts-sdk'; +import useLiveCollection from '~/v4/core/hooks/useLiveCollection'; + +export function useTrendingCommunitiesCollection({ + params, + enabled, +}: { + params?: Parameters[0]; + enabled?: boolean; +}) { + const { items, ...rest } = useLiveCollection({ + fetcher: CommunityRepository.getTrendingCommunities, + params: { + ...params, + limit: params?.limit || 10, + }, + shouldCall: enabled, + }); + + return { + trendingCommunities: items, + ...rest, + }; +} diff --git a/src/v4/core/hooks/objects/useUser.ts b/src/v4/core/hooks/objects/useUser.ts index 204cb3b78..9ba519902 100644 --- a/src/v4/core/hooks/objects/useUser.ts +++ b/src/v4/core/hooks/objects/useUser.ts @@ -2,11 +2,17 @@ import { UserRepository } from '@amityco/ts-sdk'; import useLiveObject from '~/v4/core/hooks/useLiveObject'; -export const useUser = (userId?: string | null) => { +export const useUser = ({ + userId, + shouldCall = true, +}: { + userId?: string | null; + shouldCall?: boolean; +}) => { const { item, ...rest } = useLiveObject({ fetcher: UserRepository.getUser, params: userId, - shouldCall: !!userId, + shouldCall: !!userId && shouldCall, }); return { diff --git a/src/v4/core/hooks/useCategory.ts b/src/v4/core/hooks/useCategory.ts new file mode 100644 index 000000000..e63f41ffa --- /dev/null +++ b/src/v4/core/hooks/useCategory.ts @@ -0,0 +1,22 @@ +import { CategoryRepository } from '@amityco/ts-sdk'; + +import { useEffect, useState } from 'react'; + +export const useCategory = ({ + categoryId, +}: { + categoryId?: Amity.Category['categoryId'] | null; +}) => { + const [category, setCategory] = useState(null); + + useEffect(() => { + async function run() { + if (categoryId == null) return; + const category = await CategoryRepository.getCategory(categoryId); + setCategory(category.data); + } + run(); + }, [categoryId]); + + return category; +}; diff --git a/src/v4/core/natives/ClickableArea/ClickableArea.module.css b/src/v4/core/natives/ClickableArea/ClickableArea.module.css new file mode 100644 index 000000000..d4ebb84b3 --- /dev/null +++ b/src/v4/core/natives/ClickableArea/ClickableArea.module.css @@ -0,0 +1,5 @@ +.clickableArea { + box-sizing: border-box; + cursor: pointer; + outline: none; +} diff --git a/src/v4/core/natives/ClickableArea/ClickableArea.ts b/src/v4/core/natives/ClickableArea/ClickableArea.ts new file mode 100644 index 000000000..fd098a8f1 --- /dev/null +++ b/src/v4/core/natives/ClickableArea/ClickableArea.ts @@ -0,0 +1,41 @@ +import clsx from 'clsx'; +import React, { ReactNode, useRef, createElement, DOMAttributes } from 'react'; +import { useButton, AriaButtonOptions } from 'react-aria'; +import styles from './ClickableArea.module.css'; + +export type ClickableAreaProps = Omit< + AriaButtonOptions, + 'elementType' +> & { + elementType: 'span' | 'div'; + key?: string; + children: ReactNode; + className?: string; + style?: React.CSSProperties; +}; + +export function ClickableArea({ + key, + className, + style, + ...props +}: ClickableAreaProps): React.DetailedReactHTMLElement< + { + className: string | undefined; + ref: React.MutableRefObject; + key: string | undefined; + } & DOMAttributes, + HTMLButtonElement +> { + const { children } = props; + const ref = useRef(null); + const { buttonProps } = useButton(props, ref); + + const element = createElement( + props.elementType, + { ...buttonProps, className: clsx(styles.clickableArea, className), ref, key, style }, + children, + ); + + return element; +} diff --git a/src/v4/core/natives/ClickableArea/index.ts b/src/v4/core/natives/ClickableArea/index.ts new file mode 100644 index 000000000..24148527b --- /dev/null +++ b/src/v4/core/natives/ClickableArea/index.ts @@ -0,0 +1,5 @@ +export { ClickableArea } from './ClickableArea'; + +import type { ClickableAreaProps } from './ClickableArea'; + +export type { ClickableAreaProps }; diff --git a/src/v4/core/natives/Img/Img.tsx b/src/v4/core/natives/Img/Img.tsx new file mode 100644 index 000000000..34da055f3 --- /dev/null +++ b/src/v4/core/natives/Img/Img.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react'; + +interface ImgProps extends React.ImgHTMLAttributes { + fallBackRenderer?: () => JSX.Element | null; +} + +export const Img = ({ fallBackRenderer, src, ...props }: ImgProps) => { + const [isError, setIsError] = useState(false); + + const handleError = (event: React.SyntheticEvent) => { + setIsError(true); + }; + + if (isError || src == undefined) { + if (fallBackRenderer) { + return fallBackRenderer(); + } + return null; + } + + return ; +}; diff --git a/src/v4/core/natives/Img/index.tsx b/src/v4/core/natives/Img/index.tsx new file mode 100644 index 000000000..ce8755777 --- /dev/null +++ b/src/v4/core/natives/Img/index.tsx @@ -0,0 +1 @@ +export { Img } from './Img'; diff --git a/src/v4/core/providers/AmityUIKitProvider.tsx b/src/v4/core/providers/AmityUIKitProvider.tsx index 4fb254718..1f71cb3c6 100644 --- a/src/v4/core/providers/AmityUIKitProvider.tsx +++ b/src/v4/core/providers/AmityUIKitProvider.tsx @@ -1,4 +1,3 @@ -import '~/core/providers/UiKitProvider/inter.css'; import './index.css'; import '~/v4/styles/global.css'; @@ -30,7 +29,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AmityUIKitManager } from '~/v4/core/AmityUIKitManager'; import { ConfirmProvider } from '~/v4/core/providers/ConfirmProvider'; import { ConfirmProvider as LegacyConfirmProvider } from '~/core/providers/ConfirmProvider'; -import { NotificationProvider } from '~/v4/core/providers/NotificationProvider'; +import { NotificationProvider, useNotifications } from '~/v4/core/providers/NotificationProvider'; import { DrawerProvider } from '~/v4/core/providers/DrawerProvider'; import { NotificationProvider as LegacyNotificationProvider } from '~/core/providers/NotificationProvider'; import { CustomReactionProvider } from './CustomReactionProvider'; @@ -38,40 +37,7 @@ import { AdEngineProvider } from './AdEngineProvider'; import { AdEngine } from '~/v4/core/AdEngine'; import { GlobalFeedProvider } from '~/v4/social/providers/GlobalFeedProvider'; -export type AmityUIKitConfig = Config; - -interface AmityUIKitProviderProps { - apiKey: string; - apiRegion: string; - apiEndpoint?: { - http?: string; - mqtt?: string; - }; - userId: string; - displayName: string; - postRendererConfig?: any; - theme?: Record; - children?: React.ReactNode; - socialCommunityCreationButtonVisible?: boolean; - actionHandlers?: { - onChangePage?: (data: { type: string; [x: string]: string | boolean }) => void; - onClickCategory?: (categoryId: string) => void; - onClickCommunity?: (communityId: string) => void; - onClickUser?: (userId: string) => void; - onCommunityCreated?: (communityId: string) => void; - onEditCommunity?: (communityId: string, options?: { tab?: string }) => void; - onEditUser?: (userId: string) => void; - onMessageUser?: (userId: string) => void; - }; - pageBehavior?: PageBehavior; - onConnectionStatusChange?: (state: Amity.SessionStates) => void; - onConnected?: () => void; - onDisconnected?: () => void; - getAuthToken?: () => Promise; - configs?: AmityUIKitConfig; -} - -const AmityUIKitProvider: React.FC = ({ +const InternalComponent = ({ apiKey, apiRegion, apiEndpoint, @@ -86,11 +52,13 @@ const AmityUIKitProvider: React.FC = ({ onDisconnected, getAuthToken, configs, -}) => { +}: AmityUIKitProviderProps) => { const queryClient = new QueryClient(); const [client, setClient] = useState(null); const currentUser = useUser(userId); + const { error } = useNotifications(); + const sdkContextValue = useMemo( () => ({ client, @@ -137,8 +105,13 @@ const AmityUIKitProvider: React.FC = ({ const newClient = AmityUIKitManager.getClient(); setClient(newClient); - } catch (error) { - console.error('Error setting up AmityUIKitManager:', error); + } catch (_error) { + console.error('Error setting up AmityUIKitManager:', _error); + if (_error instanceof Error) { + error({ + content: _error.message, + }); + } } }; @@ -150,59 +123,97 @@ const AmityUIKitProvider: React.FC = ({ return (
- - - - - - - - - - - - - - - - - - - - {children} - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + {children} + + + + + + + + + + + + +
); }; +export type AmityUIKitConfig = Config; + +interface AmityUIKitProviderProps { + apiKey: string; + apiRegion: string; + apiEndpoint?: { + http?: string; + mqtt?: string; + }; + userId: string; + displayName?: string; + postRendererConfig?: any; + theme?: Record; + children?: React.ReactNode; + socialCommunityCreationButtonVisible?: boolean; + actionHandlers?: { + onChangePage?: (data: { type: string; [x: string]: string | boolean }) => void; + onClickCategory?: (categoryId: string) => void; + onClickCommunity?: (communityId: string) => void; + onClickUser?: (userId: string) => void; + onCommunityCreated?: (communityId: string) => void; + onEditCommunity?: (communityId: string, options?: { tab?: string }) => void; + onEditUser?: (userId: string) => void; + onMessageUser?: (userId: string) => void; + }; + pageBehavior?: PageBehavior; + onConnectionStatusChange?: (state: Amity.SessionStates) => void; + onConnected?: () => void; + onDisconnected?: () => void; + getAuthToken?: () => Promise; + configs?: AmityUIKitConfig; +} + +const AmityUIKitProvider: React.FC = (props) => { + return ( + + + + + + + + + + + + + + + + + + + + ); +}; + export default AmityUIKitProvider; diff --git a/src/v4/core/providers/CustomizationProvider.tsx b/src/v4/core/providers/CustomizationProvider.tsx index 2c59cf297..376b51bae 100644 --- a/src/v4/core/providers/CustomizationProvider.tsx +++ b/src/v4/core/providers/CustomizationProvider.tsx @@ -534,6 +534,48 @@ export const defaultConfig: DefaultConfig = { image: 'value', }, 'community_profile_page/post_content/*': {}, + 'social_home_page/explore_community_categories/*': {}, + 'social_home_page/recommended_communities/*': {}, + 'social_home_page/*/explore_empty_image': { + image: 'value', + }, + 'social_home_page/explore_empty/title': { + text: 'Your explore is empty', + }, + 'social_home_page/explore_empty/description': { + text: 'Find community or create your own', + }, + 'social_home_page/explore_empty/explore_create_community': { + text: 'Create community', + }, + 'social_home_page/explore_community_empty/title': { + text: 'No community yet', + }, + 'social_home_page/explore_community_empty/description': { + text: `Let's create your own communities`, + }, + 'social_home_page/explore_community_empty/explore_create_community': { + text: 'Create community', + }, + 'social_home_page/*/explore_trending_title': { + text: 'Trending now', + }, + 'social_home_page/*/explore_recommended_title': { + text: 'Recommended for you', + }, + 'social_home_page/trending_communities/*': {}, + 'all_categories_page/*/*': {}, + 'communities_by_category_page/*/*': {}, + '*/*/community_join_button': { + text: 'Join', + }, + '*/*/community_joined_button': { + text: 'Joined', + }, + 'communities_by_category_page/*/community_empty_image': {}, + 'communities_by_category_page/*/community_empty_title': { + text: 'No community yet', + }, }, }; diff --git a/src/v4/core/providers/NavigationProvider.tsx b/src/v4/core/providers/NavigationProvider.tsx index 3608cdcfd..f2e9333ef 100644 --- a/src/v4/core/providers/NavigationProvider.tsx +++ b/src/v4/core/providers/NavigationProvider.tsx @@ -23,6 +23,9 @@ export enum PageTypes { PostComposerPage = 'PostComposerPage', MyCommunitiesSearchPage = 'MyCommunitiesSearchPage', StoryTargetSelectionPage = 'StoryTargetSelectionPage', + AllCategoriesPage = 'AllCategoriesPage', + CommunitiesByCategoryPage = 'CommunitiesByCategoryPage', + CommunityCreatePage = 'CommunityCreatePage', } type Page = @@ -103,6 +106,18 @@ type Page = } | { type: PageTypes.StoryTargetSelectionPage; + } + | { + type: PageTypes.AllCategoriesPage; + } + | { + type: PageTypes.CommunitiesByCategoryPage; + context: { + categoryId: string; + }; + } + | { + type: PageTypes.CommunityCreatePage; }; type ContextValue = { @@ -123,6 +138,7 @@ type ContextValue = { goToMyCommunitiesSearchPage: () => void; goToSelectPostTargetPage: () => void; goToStoryTargetSelectionPage: () => void; + goToCommunityCreatePage: () => void; goToDraftStoryPage: ( targetId: string, targetType: string, @@ -161,6 +177,8 @@ type ContextValue = { storyType: 'communityFeed' | 'globalFeed'; }) => void; goToSocialHomePage: () => void; + goToAllCategoriesPage: () => void; + goToCommunitiesByCategoryPage: (context: { categoryId: string }) => void; //V3 functions onClickStory: ( storyId: string, @@ -196,10 +214,13 @@ let defaultValue: ContextValue = { goToSocialGlobalSearchPage: (tab?: string) => {}, goToSelectPostTargetPage: () => {}, goToStoryTargetSelectionPage: () => {}, + goToCommunityCreatePage: () => {}, goToPostComposerPage: () => {}, goToStoryCreationPage: () => {}, goToSocialHomePage: () => {}, goToMyCommunitiesSearchPage: () => {}, + goToAllCategoriesPage: () => {}, + goToCommunitiesByCategoryPage: (context: { categoryId: string }) => {}, setNavigationBlocker: () => {}, onBack: () => {}, //V3 functions @@ -237,6 +258,7 @@ if (process.env.NODE_ENV !== 'production') { goToSocialGlobalSearchPage: (tab) => console.log(`NavigationContext goToSocialGlobalSearchPage(${tab})`), goToSelectPostTargetPage: () => console.log('NavigationContext goToTargetPage()'), + goToCommunityCreatePage: () => console.log('NavigationContext goToCommunityCreatePage()'), goToStoryTargetSelectionPage: () => console.log('NavigationContext goToStoryTargetSelectionPage()'), goToDraftStoryPage: (data) => console.log(`NavigationContext goToDraftStoryPage()`), @@ -245,6 +267,9 @@ if (process.env.NODE_ENV !== 'production') { goToSocialHomePage: () => console.log('NavigationContext goToSocialHomePage()'), goToMyCommunitiesSearchPage: () => console.log('NavigationContext goToMyCommunitiesSearchPage()'), + goToAllCategoriesPage: () => console.log('NavigationContext goToAllCategoriesPage()'), + goToCommunitiesByCategoryPage: (context) => + console.log(`NavigationContext goToCommunitiesByCategoryPage(${context})`), //V3 functions onClickStory: (storyId, storyType, targetIds) => @@ -289,7 +314,10 @@ interface NavigationProviderProps { mediaType: AmityStoryMediaType, storyType: 'communityFeed' | 'globalFeed', ) => void; + goToAllCategoriesPage?: () => void; + goToCommunitiesByCategoryPage?: (context: { categoryId: string }) => void; onCommunityCreated?: (communityId: string) => void; + goToCommunityCreatePage?: () => void; onEditCommunity?: (communityId: string, options?: { tab?: string }) => void; onEditUser?: (userId: string) => void; onMessageUser?: (userId: string) => void; @@ -541,6 +569,15 @@ export default function NavigationProvider({ [onChangePage, pushPage], ); + const goToCommunityCreatePage = useCallback(() => { + const next = { + type: PageTypes.CommunityCreatePage, + context: {}, + }; + + pushPage(next); + }, [onChangePage, pushPage]); + const goToSocialGlobalSearchPage = useCallback( (tab?: string) => { const next = { @@ -640,6 +677,27 @@ export default function NavigationProvider({ pushPage(next); }, [onChangePage, pushPage]); + const goToAllCategoriesPage = useCallback(() => { + const next = { + type: PageTypes.AllCategoriesPage, + context: {}, + }; + + pushPage(next); + }, [onChangePage, pushPage]); + + const goToCommunitiesByCategoryPage = useCallback( + (context) => { + const next = { + type: PageTypes.CommunitiesByCategoryPage, + context, + }; + + pushPage(next); + }, + [onChangePage, pushPage], + ); + const handleClickStory = useCallback( (targetId, storyType, targetIds) => { const next = { @@ -683,6 +741,9 @@ export default function NavigationProvider({ goToPostComposerPage, goToSocialHomePage, goToMyCommunitiesSearchPage, + goToAllCategoriesPage, + goToCommunitiesByCategoryPage, + goToCommunityCreatePage, setNavigationBlocker, onClickStory: handleClickStory, }} diff --git a/src/v4/helpers/utils.ts b/src/v4/helpers/utils.ts index a0eea90c0..0a0b19aab 100644 --- a/src/v4/helpers/utils.ts +++ b/src/v4/helpers/utils.ts @@ -1,7 +1,13 @@ import { CommunityPostSettings } from '@amityco/ts-sdk'; import isEmpty from 'lodash/isEmpty'; -export type Mentioned = { userId?: string; length: number; index: number; type: string }; +export type Mentioned = { + userId?: string; + length: number; + index: number; + type: string; + displayName?: string; +}; export type Mentionees = (Amity.UserMention | Amity.ChannelMention)[]; export type Metadata = { mentioned?: Mentioned[]; diff --git a/src/v4/icons/Brand.tsx b/src/v4/icons/Brand.tsx new file mode 100644 index 000000000..8688bb408 --- /dev/null +++ b/src/v4/icons/Brand.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +const Brand = (props: React.SVGProps) => ( + + + + + + + + + + +); + +export default Brand; diff --git a/src/v4/icons/ChevronRight.tsx b/src/v4/icons/ChevronRight.tsx new file mode 100644 index 000000000..92103504c --- /dev/null +++ b/src/v4/icons/ChevronRight.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const ChevronRight = (props: React.SVGProps) => ( + + + +); + +export default ChevronRight; diff --git a/src/v4/icons/People.tsx b/src/v4/icons/People.tsx new file mode 100644 index 000000000..b631b3db7 --- /dev/null +++ b/src/v4/icons/People.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +export const People = (props: React.SVGProps) => { + return ( + + + + + + + + + + ); +}; diff --git a/src/v4/social/components/Comment/Comment.module.css b/src/v4/social/components/Comment/Comment.module.css index ab93aa4e2..a18170589 100644 --- a/src/v4/social/components/Comment/Comment.module.css +++ b/src/v4/social/components/Comment/Comment.module.css @@ -137,7 +137,18 @@ width: 100%; } +.postComment__edit__mentionContainer { + width: 100%; + position: absolute; + top: 0; + left: 0; + transform: translateY(-100%); + max-height: 8rem; + overflow: scroll; +} + .postComment__edit__input { + position: relative; display: flex; height: 7.5rem; padding: 0.75rem; diff --git a/src/v4/social/components/Comment/Comment.tsx b/src/v4/social/components/Comment/Comment.tsx index 489e4eaed..303a515be 100644 --- a/src/v4/social/components/Comment/Comment.tsx +++ b/src/v4/social/components/Comment/Comment.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Typography, BottomSheet } from '~/v4/core/components'; import { ModeratorBadge } from '~/v4/social/elements/ModeratorBadge'; import { Timestamp } from '~/v4/social/elements/Timestamp'; @@ -94,6 +94,7 @@ export const Comment = ({ const [hasClickLoadMore, setHasClickLoadMore] = useState(false); const [isEditing, setIsEditing] = useState(false); const [commentData, setCommentData] = useState(); + const mentionRef = useRef(null); const [isShowMore, setIsShowMore] = useState(false); @@ -166,11 +167,12 @@ export const Comment = ({ ) : isEditing ? (
- +
+
{ - setCommentData(value); + onChange={(value) => { + setCommentData({ + data: { + text: value.text, + }, + mentionees: value.mentionees as Amity.UserMention[], + metadata: { + mentioned: value.mentioned, + }, + }); }} maxLines={5} - mentionOffsetBottom={215} + mentionContainer={mentionRef?.current} />
@@ -209,16 +219,21 @@ export const Comment = ({
) : (
- +
- + {comment.creator?.displayName} {isModeratorUser && } 0 && !hasClickLoadMore && (
setHasClickLoadMore(true)} > @@ -281,7 +297,9 @@ export const Comment = ({ {hasClickLoadMore && ( { const userId = useSDK().currentUserId; - const { user } = useUser(userId); + const { user } = useUser({ userId }); const avatarUrl = useImage({ fileId: user?.avatar?.fileId, imageSize: 'small' }); const editorRef = useRef(null); - const composerRef = useRef(null); const composerInputRef = useRef(null); const componentId = 'comment_composer_bar'; + const mentionContainerRef = useRef(null); const [composerHeight, setComposerHeight] = useState(0); - const [mentionOffsetBottom, setMentionOffsetBottom] = useState(0); const [textValue, setTextValue] = useState({ data: { @@ -75,10 +74,6 @@ export const CommentComposer = ({ metadata: {}, }); - const onChange = (val: any) => { - setTextValue(val); - }; - const { mutateAsync } = useMutation({ mutationFn: async ({ params }: { params: CreateCommentParams }) => { const parentId = replyTo ? replyTo.commentId : undefined; @@ -103,21 +98,6 @@ export const CommentComposer = ({ }, }); - useEffect(() => { - if (composerInputRef.current) { - // NOTE: Cannot use ref to get padding of the container and inside input - const containerPaddingBottom = 8; - const inputPaddingBottom = 10; - setMentionOffsetBottom( - composerInputRef.current.offsetHeight - inputPaddingBottom + containerPaddingBottom, - ); - } - - if (composerRef.current) { - setComposerHeight(composerRef.current.offsetHeight); - } - }, []); - if (!shouldAllowCreation) { return (
@@ -128,56 +108,78 @@ export const CommentComposer = ({ } return ( -
-
- } /> -
- -
- +
+
+
+ {replyTo && ( +
+
+ Replying to + + {replyTo?.userId} + +
+ +
+ )}
- - {replyTo && ( +
+
+ } + /> +
-
- Replying to - - {replyTo?.userId} - -
- { + setTextValue({ + data: { + text: text, + }, + mentionees: mentionees, + metadata: { + mentioned: mentioned, + }, + }); + }} + targetType={referenceType} + targetId={referenceId} + value={textValue} + placehoder="Say something nice..." + communityId={community?.communityId} />
- )} + +
); }; diff --git a/src/v4/social/components/CommentComposer/CommentInput.module.css b/src/v4/social/components/CommentComposer/CommentInput.module.css index daae1b98a..de3572627 100644 --- a/src/v4/social/components/CommentComposer/CommentInput.module.css +++ b/src/v4/social/components/CommentComposer/CommentInput.module.css @@ -11,31 +11,22 @@ height: 100%; width: 100%; color: var(--asc-color-base-default); -} - -.editorParagraph span { - color: var(--asc-color-base-default); + line-height: var(--asc-line-height-md); + font-size: var(--asc-text-font-size-md); + margin: 0; } .editorContainer { width: 100%; height: 100%; color: var(--asc-color-base-default); - max-height: calc(var(--asc-line-height-md) * var(--var-max-lines) + (0.62rem * 2)); - - /* padding: 0.62rem 1rem; */ + max-height: calc(var(--asc-line-height-md) * var(--asc-max-lines) + (0.62rem * 2)); position: relative; - - p { - line-height: var(--asc-line-height-md); - font-size: var(--asc-text-font-size-md); - margin: 0; - } } .editorEditableContent { height: max-content; - max-height: calc(var(--asc-line-height-md) * var(--var-max-lines)); + max-height: calc(var(--asc-line-height-md) * var(--asc-max-lines)); overflow-y: scroll; } @@ -43,3 +34,14 @@ border: none; outline: none; } + +.editorLink { + color: var(--asc-color-primary-shade1); + text-decoration: none; +} + +.commentInput__mentionInterceptor { + width: 100%; + height: 1px; + background-color: var(--asc-color-background-default); +} diff --git a/src/v4/social/components/CommentComposer/CommentInput.tsx b/src/v4/social/components/CommentComposer/CommentInput.tsx index 2fc479b3f..68209e474 100644 --- a/src/v4/social/components/CommentComposer/CommentInput.tsx +++ b/src/v4/social/components/CommentComposer/CommentInput.tsx @@ -1,238 +1,176 @@ -import React, { forwardRef, MutableRefObject, useImperativeHandle } from 'react'; +import React, { forwardRef, MutableRefObject, useImperativeHandle, useMemo, useState } from 'react'; +import ReactDOM from 'react-dom'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { EditorRefPlugin } from '@lexical/react/LexicalEditorRefPlugin'; -import { - $getRoot, - LexicalEditor, - SerializedLexicalNode, - SerializedTextNode, - SerializedRootNode, - SerializedParagraphNode, -} from 'lexical'; +import { $getRoot, LexicalEditor, Klass, LexicalNode, COMMAND_PRIORITY_HIGH } from 'lexical'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin'; -import { - MentionNode, - SerializedMentionNode, -} from '~/v4/social/internal-components/MentionTextInput/MentionNodes'; import styles from './CommentInput.module.css'; -import { CommentMentionInput } from '~/v4/social/components/CommentMentionInput'; -import { useMentionUsers } from '~/v4/social/hooks/useMentionUser'; import { CreateCommentParams } from '~/v4/social/components/CommentComposer/CommentComposer'; +import { + editorToText, + getEditorConfig, + MentionData, + textToEditorState, +} from '~/v4/social/internal-components/Lexical/utils'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { AutoLinkNode, LinkNode } from '@lexical/link'; +import { + $createMentionNode, + MentionNode, +} from '~/v4/social/internal-components/Lexical/nodes/MentionNode'; import { Mentioned, Mentionees } from '~/v4/helpers/utils'; - -const theme = { - ltr: 'ltr', - rtl: 'rtl', - placeholder: styles.editorPlaceholder, - paragraph: styles.editorParagraph, -}; - -const editorConfig = { - namespace: 'CommentInput', - theme: theme, - onError(error: Error) { - throw error; - }, - nodes: [MentionNode], -}; - -interface EditorStateJson extends SerializedLexicalNode { - children: []; -} +import { AutoLinkPlugin } from '~/v4/social/internal-components/Lexical/plugins/AutoLinkPlugin'; +import { LinkPlugin } from '~/v4/social/internal-components/Lexical/plugins/LinkPlugin'; +import { MentionPlugin } from '~/v4/social/internal-components/Lexical/plugins/MentionPlugin'; +import { useUserQueryByDisplayName } from '~/v4/core/hooks/collections/useUsersCollection'; +import { useMemberQueryByDisplayName } from '~/v4/social/hooks/useMemberQueryByDisplayName'; +import useCommunity from '~/v4/core/hooks/collections/useCommunity'; +import { MentionItem } from '~/v4/social/internal-components/Lexical/MentionItem'; +import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; interface CommentInputProps { - community?: Amity.Community | null; + pageId?: string; + componentId?: string; + communityId?: string; value?: CreateCommentParams; - mentionOffsetBottom?: number; maxLines?: number; placehoder?: string; targetType?: string; targetId?: string; ref: MutableRefObject; - onChange: (data: CreateCommentParams) => void; + mentionContainer?: HTMLElement | null; + onChange: (data: { mentioned: Mentioned[]; mentionees: Mentionees; text: string }) => void; } export interface CommentInputRef { clearEditorState: () => void; } -function editorStateToText(editor: LexicalEditor): CreateCommentParams { - const editorStateTextString: string[] = []; - const paragraphs = editor.getEditorState().toJSON().root.children as EditorStateJson[]; - - const mentioned: Mentioned[] = []; - const mentionees: { - type: Amity.Mention['type']; - userIds: string[]; - }[] = []; - let runningIndex = 0; - - paragraphs.forEach((paragraph) => { - const children = paragraph.children; - const paragraphText: string[] = []; - - children.forEach((child: { type: string; text: string; userId: string }) => { - if (child.text) { - paragraphText.push(child.text); - } - if (child.type === 'mention') { - mentioned.push({ - index: runningIndex, - length: child.text.length, - type: 'user', - userId: child.userId, - }); - - mentionees.push({ type: 'user', userIds: [child.userId] }); - } - runningIndex += child.text.length; - }); - runningIndex += 1; - editorStateTextString.push(paragraphText.join('')); +const useSuggestions = (communityId?: string | null) => { + const [queryString, setQueryString] = useState(null); + + const { community, isLoading: isCommunityLoading } = useCommunity({ communityId }); + + const isSearchCommunityMembers = useMemo( + () => !!communityId && !isCommunityLoading && !community?.isPublic, + [communityId, isCommunityLoading, community], + ); + + const { + members, + hasMore: hasMoreMember, + isLoading: isLoadingMember, + loadMore: loadMoreMember, + } = useMemberQueryByDisplayName({ + communityId: communityId || '', + displayName: queryString || '', + limit: 10, + enabled: isSearchCommunityMembers, + }); + const { + users, + hasMore: hasMoreUser, + loadMore: loadMoreUser, + isLoading: isLoadingUser, + } = useUserQueryByDisplayName({ + displayName: queryString || '', + limit: 10, + enabled: !isSearchCommunityMembers, }); - return { - data: { text: editorStateTextString.join('\n') }, - mentionees, - metadata: { - mentioned, - }, + const onQueryChange = (newQuery: string | null) => { + setQueryString(newQuery); }; -} -function createRootNode(): SerializedRootNode { - return { - children: [], - direction: 'ltr', - format: '', - indent: 0, - type: 'root', - version: 1, - }; -} + const suggestions = useMemo(() => { + if (!!communityId && isCommunityLoading) return []; -function createParagraphNode(): SerializedParagraphNode { - return { - children: [], - direction: 'ltr', - format: '', - indent: 0, - type: 'paragraph', - version: 1, - textFormat: 0, - }; -} - -function createSerializeTextNode(text: string): SerializedTextNode { - return { - detail: 0, - format: 0, - mode: 'normal', - style: '', - text, - type: 'text', - version: 1, - }; -} + if (isSearchCommunityMembers) { + return members.map(({ user, userId }) => ({ + userId: user?.userId || userId, + displayName: user?.displayName, + })); + } -function createSerializeMentionNode(mention: Mentioned): SerializedMentionNode { - return { - detail: 0, - format: 0, - mode: 'normal', - style: '', - text: ('@' + mention.userId) as string, - type: 'mention', - version: 1, - mentionName: mention.userId as string, - displayName: mention.userId as string, - userId: mention.userId as string, - userInternalId: mention.userId as string, - userPublicId: mention.userId as string, - }; -} + return users.map(({ displayName, userId }) => ({ + userId: userId, + displayName: displayName, + })); + }, [users, members, isSearchCommunityMembers, isCommunityLoading]); -export function TextToEditorState(value: { - data: { text: string }; - metadata?: { - mentioned?: Mentioned[]; + const hasMore = useMemo(() => { + if (isSearchCommunityMembers) { + return hasMoreMember; + } else { + return hasMoreUser; + } + }, [isSearchCommunityMembers, hasMoreMember, hasMoreUser]); + + const loadMore = () => { + if (isLoading || !hasMore) return; + if (isSearchCommunityMembers) { + loadMoreMember(); + } else { + loadMoreUser(); + } }; - mentionees?: Mentionees; -}) { - const rootNode = createRootNode(); - - const textArray = value.data.text.split('\n'); - - const mentions = value.metadata?.mentioned; - - let start = 0; - let stop = -1; - let mentionRunningIndex = 0; - - for (let i = 0; i < textArray.length; i++) { - start = stop + 1; - stop = start + textArray[i].length; - - const paragraph = createParagraphNode(); - if (Array.isArray(mentions) && mentions?.length > 0) { - let runningIndex = 0; + const isLoading = useMemo(() => { + if (isSearchCommunityMembers) { + return isLoadingMember; + } else { + return isLoadingUser; + } + }, [isLoadingMember, isLoadingUser, isSearchCommunityMembers]); - while (runningIndex < textArray[i].length) { - if (mentionRunningIndex >= mentions.length) { - paragraph.children.push(createSerializeTextNode(textArray[i].slice(runningIndex))); - runningIndex = textArray[i].length; - break; - } + return { suggestions, queryString, onQueryChange, loadMore, hasMore, isLoading }; +}; - if (mentions[mentionRunningIndex].index >= stop) { - paragraph.children.push(createSerializeTextNode(textArray[i])); - runningIndex = textArray[i].length; - } else { - const text = textArray[i].slice( - runningIndex, - runningIndex + mentions[mentionRunningIndex]?.index - start, - ); +const nodes = [AutoLinkNode, LinkNode, MentionNode] as Array>; - if (text) { - paragraph.children.push(createSerializeTextNode(text)); - } - - paragraph.children.push(createSerializeMentionNode(mentions[mentionRunningIndex])); +export const CommentInput = forwardRef( + ( + { + pageId = '*', + componentId = '*', + communityId, + value, + onChange, + maxLines = 10, + placehoder, + mentionContainer, + }, + ref, + ) => { + const [intersectionNode, setIntersectionNode] = useState(null); + const elementId = 'comment_input'; + const { themeStyles, uiReference, config, accessibilityId } = useAmityElement({ + pageId, + componentId, + elementId, + }); - runningIndex += - mentions[mentionRunningIndex].index + mentions[mentionRunningIndex].length - start; + const { onQueryChange, suggestions, isLoading, loadMore, hasMore } = + useSuggestions(communityId); - mentionRunningIndex++; + useIntersectionObserver({ + onIntersect: () => { + if (isLoading === false) { + loadMore(); } - } - } - - if (!mentions || mentions?.length === 0) { - const textNode = createSerializeTextNode(textArray[i]); - paragraph.children.push(textNode); - } - - rootNode.children.push(paragraph); - } - - return { root: rootNode }; -} + }, + node: intersectionNode, + options: { + threshold: 0.7, + }, + }); -export const CommentInput = forwardRef( - ({ community, mentionOffsetBottom = 0, value, onChange, maxLines = 10, placehoder }, ref) => { const editorRef = React.useRef(null); - const [queryMentionUser, setQueryMentionUser] = React.useState(null); - - const { mentionUsers } = useMentionUsers({ - displayName: queryMentionUser || '', - community, - }); const clearEditorState = () => { editorRef.current?.update(() => { @@ -245,18 +183,30 @@ export const CommentInput = forwardRef( clearEditorState, })); + const editorConfig = getEditorConfig({ + namespace: uiReference, + theme: { + // root: styles.editorRoot, + placeholder: styles.editorPlaceholder, + paragraph: styles.editorParagraph, + link: styles.editorLink, + }, + nodes, + }); + return (
@@ -269,16 +219,60 @@ export const CommentInput = forwardRef( /> { - onChange(editorStateToText(editor)); + onChange(editorToText(editor)); }} /> + + - setQueryMentionUser(query)} - offsetBottom={mentionOffsetBottom} + > + suggestions={suggestions} + getSuggestionId={(suggestion) => suggestion.userId} + onQueryChange={onQueryChange} + $createNode={(data) => + $createMentionNode({ + text: `@${data?.displayName || ''}`, + data, + }) + } + menuRenderFn={( + _, + { options, selectedIndex, setHighlightedIndex, selectOptionAndCleanUp }, + ) => { + if (!mentionContainer || options.length === 0) { + return null; + } + return ReactDOM.createPortal( + <> + {options.map((option, i: number) => { + return ( + { + setHighlightedIndex(i); + selectOptionAndCleanUp(option); + }} + onMouseEnter={() => { + setHighlightedIndex(i); + }} + key={option.key} + option={option} + /> + ); + })} + {hasMore && ( +
setIntersectionNode(node)} + className={styles.commentInput__mentionInterceptor} + /> + )} + , + mentionContainer, + ); + }} + commandPriority={COMMAND_PRIORITY_HIGH} />
diff --git a/src/v4/social/components/CommentList/CommentList.tsx b/src/v4/social/components/CommentList/CommentList.tsx index 374d93ae1..0dedda793 100644 --- a/src/v4/social/components/CommentList/CommentList.tsx +++ b/src/v4/social/components/CommentList/CommentList.tsx @@ -3,17 +3,13 @@ import React, { useRef, useState } from 'react'; import { Comment } from '~/v4/social/components/Comment/Comment'; import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; import { useAmityComponent } from '~/v4/core/hooks/uikit'; -import useUserSubscription from '~/v4/core/hooks/subscriptions/useUserSubscription'; import { CommentRepository, SubscriptionLevels } from '@amityco/ts-sdk'; -import useCommunitySubscription from '~/v4/core/hooks/subscriptions/useCommunitySubscription'; import { usePaginator } from '~/v4/core/hooks/usePaginator'; import { CommentAd } from '~/v4/social/internal-components/CommentAd/CommentAd'; import { CommentSkeleton } from '~/v4/social/components/Comment/CommentSkeleton'; import styles from './CommentList.module.css'; import useCommunityStoriesSubscription from '~/v4/social/hooks/useCommunityStoriesSubscription'; import { Typography } from '~/v4/core/components'; -import useStory from '~/v4/social/hooks/useStory'; -import usePost from '~/v4/core/hooks/objects/usePost'; import usePostSubscription from '~/v4/core/hooks/subscriptions/usePostSubscription'; type CommentListProps = { @@ -65,13 +61,6 @@ export const CommentList = ({ shouldCall: true, }); - const { story } = useStory({ - storyId: referenceId, - shouldCall: referenceType === 'story', - }); - - const { post } = usePost(referenceType === 'post' ? referenceId : undefined); - useIntersectionObserver({ node: intersectionNode, options: { @@ -97,7 +86,7 @@ export const CommentList = ({ shouldSubscribe: referenceType === 'story' && !!referenceId, }); - if (items.length === 0) { + if (!isLoading && items.length === 0) { return (
No comments yet @@ -123,8 +112,6 @@ export const CommentList = ({ onClickReply={(comment) => onClickReply?.(comment)} componentId={componentId} community={community} - // targetId={post?.targetId || story?.targetId} - // targetType={post?.targetType || story?.targetType} shouldAllowInteraction={shouldAllowInteraction} /> ); diff --git a/src/v4/social/components/CommentMentionInput/MentionUser.module.css b/src/v4/social/components/CommentMentionInput/MentionUser.module.css index 2167f73ff..e4e50ff66 100644 --- a/src/v4/social/components/CommentMentionInput/MentionUser.module.css +++ b/src/v4/social/components/CommentMentionInput/MentionUser.module.css @@ -16,6 +16,14 @@ height: 2.5rem; } +.communityMember__rightPane { + display: grid; + grid-template-columns: auto 1.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; +} + .communityMember__displayName { margin-left: 0.5rem; font-size: 1rem; @@ -25,3 +33,8 @@ overflow: hidden; color: var(--asc-color-base-default); } + +.communityMember__brandIcon { + width: 1.5rem; + height: 1.5rem; +} diff --git a/src/v4/social/components/CommentMentionInput/MentionUser.tsx b/src/v4/social/components/CommentMentionInput/MentionUser.tsx index 2fb5d5b10..7a25b2d56 100644 --- a/src/v4/social/components/CommentMentionInput/MentionUser.tsx +++ b/src/v4/social/components/CommentMentionInput/MentionUser.tsx @@ -2,6 +2,7 @@ import React from 'react'; import styles from './MentionUser.module.css'; import { UserAvatar } from '~/v4/social/internal-components/UserAvatar/UserAvatar'; import { MentionTypeaheadOption } from './CommentMentionInput'; +import { BrandBadge } from '~/v4/social/internal-components/BrandBadge'; interface MentionUserProps { isSelected: boolean; @@ -34,7 +35,12 @@ export function MentionUser({ isSelected, onClick, onMouseEnter, option }: Menti userId={option.user.avatarFileId} />
-

{option.user.displayName}

+
+

{option.user.displayName}

+ {option.user.isBrand ? ( + + ) : null} +
); diff --git a/src/v4/social/components/CommentOptions/CommentOptions.tsx b/src/v4/social/components/CommentOptions/CommentOptions.tsx index fa1f62cb3..d2af72c7f 100644 --- a/src/v4/social/components/CommentOptions/CommentOptions.tsx +++ b/src/v4/social/components/CommentOptions/CommentOptions.tsx @@ -9,6 +9,8 @@ import { FlagIcon, PenIcon, TrashIcon } from '~/v4/social/icons'; import styles from './CommentOptions.module.css'; interface CommentOptionsProps { + pageId?: string; + componentId?: string; comment: Amity.Comment; handleEditComment: () => void; handleDeleteComment: () => void; @@ -16,6 +18,8 @@ interface CommentOptionsProps { } export const CommentOptions = ({ + pageId = '*', + componentId = '*', comment, handleEditComment, handleDeleteComment, @@ -55,6 +59,7 @@ export const CommentOptions = ({ name: 'Edit comment', action: handleEditComment, icon: , + accessibilityId: 'edit_comment', } : null, canReport @@ -62,6 +67,7 @@ export const CommentOptions = ({ name: isFlaggedByMe ? 'Unreport comment' : 'Report comment', action: handleReportComment, icon: , + accessibilityId: 'report_comment', } : null, canDelete @@ -69,6 +75,7 @@ export const CommentOptions = ({ name: 'Delete comment', action: handleDeleteComment, icon: , + accessibilityId: 'delete_comment', } : null, ].filter(isNonNullable); @@ -77,6 +84,7 @@ export const CommentOptions = ({ <> {options.map((option, index) => (
{ diff --git a/src/v4/social/components/CommunityFeed/CommunityFeed.tsx b/src/v4/social/components/CommunityFeed/CommunityFeed.tsx index f37f63295..f749c12e0 100644 --- a/src/v4/social/components/CommunityFeed/CommunityFeed.tsx +++ b/src/v4/social/components/CommunityFeed/CommunityFeed.tsx @@ -128,15 +128,33 @@ export const CommunityFeed = ({ pageId = '*', communityId }: CommunityFeedProps) const renderPublicCommunityFeed = () => { return ( <> - {isLoading - ? Array.from({ length: 2 }).map((_, index) => ( - - )) - : filteredPosts && - filteredPosts.map((post) => ( - - ))} + /> + + ))} + {isLoading && + Array.from({ length: 2 }).map((_, index) => ( + + ))} {posts?.length === 0 && !isLoading && (
@@ -202,6 +202,7 @@ export const CommunityFeed = ({ pageId = '*', communityId }: CommunityFeedProps) className={styles.communityFeed__announcePost} > = ({ {community.isOfficial && }
- +
+ +
void; } @@ -17,30 +21,50 @@ export const CommunitySearchResult = ({ communityCollection = [], isLoading, onLoadMore, + showJoinButton = false, }: CommunitySearchResultProps) => { const componentId = 'community_search_result'; - const { themeStyles } = useAmityComponent({ + const { themeStyles, accessibilityId } = useAmityComponent({ pageId, componentId, }); + const { goToCommunityProfilePage, goToCommunitiesByCategoryPage } = useNavigation(); + const [intersectionNode, setIntersectionNode] = useState(null); useIntersectionObserver({ onIntersect: () => onLoadMore(), node: intersectionNode }); + const { joinCommunity, leaveCommunity } = useCommunityActions(); + return ( -
- {communityCollection.map((community: Amity.Community) => ( - +
+ {communityCollection.map((community) => ( + + + goToCommunityProfilePage(communityId)} + onCategoryClick={(categoryId) => goToCommunitiesByCategoryPage({ categoryId })} + onJoinButtonClick={(communityId) => joinCommunity(communityId)} + onLeaveButtonClick={(communityId) => leaveCommunity(communityId)} + showJoinButton={showJoinButton} + maxCategoriesLength={5} + /> + ))} {isLoading ? Array.from({ length: 5 }).map((_, index) => ( - + + + + )) : null}
setIntersectionNode(node)} /> diff --git a/src/v4/social/components/EmptyNewsFeed/EmptyNewsFeed.tsx b/src/v4/social/components/EmptyNewsFeed/EmptyNewsFeed.tsx index 35407f4cf..def6c3e33 100644 --- a/src/v4/social/components/EmptyNewsFeed/EmptyNewsFeed.tsx +++ b/src/v4/social/components/EmptyNewsFeed/EmptyNewsFeed.tsx @@ -8,6 +8,7 @@ import { CreateCommunityButton } from '~/v4/social/elements/CreateCommunityButto import styles from './EmptyNewsFeed.module.css'; import { useAmityComponent } from '~/v4/core/hooks/uikit'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; interface EmptyNewsfeedProps { pageId?: string; @@ -15,23 +16,29 @@ interface EmptyNewsfeedProps { export function EmptyNewsfeed({ pageId = '*' }: EmptyNewsfeedProps) { const componentId = 'empty_newsfeed'; - const { accessibilityId, config, defaultConfig, isExcluded, uiReference, themeStyles } = - useAmityComponent({ - pageId, - componentId, - }); + + const { goToCommunityCreatePage } = useNavigation(); + + const { isExcluded, themeStyles, accessibilityId } = useAmityComponent({ + pageId, + componentId, + }); if (isExcluded) return null; return ( -
+
<Description pageId={pageId} componentId={componentId} /> </div> <ExploreCommunitiesButton pageId={pageId} componentId={componentId} /> - <CreateCommunityButton pageId={pageId} componentId={componentId} onClick={() => {}} /> + <CreateCommunityButton + pageId={pageId} + componentId={componentId} + onClick={goToCommunityCreatePage} + /> </div> ); } diff --git a/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.module.css b/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.module.css new file mode 100644 index 000000000..57bdec485 --- /dev/null +++ b/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.module.css @@ -0,0 +1,44 @@ +.exploreCommunityCategories { + display: inline-flex; + align-items: center; + gap: 0.5rem; + width: 100%; + overflow-x: scroll; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.exploreCommunityCategories::-webkit-scrollbar { + display: none; +} + +.exploreCommunityCategories__seeMore { + display: flex; + align-items: center; + gap: 0.5rem; + border-radius: 1.125rem; + padding: 0 0.75rem; + border: 1px solid var(--asc-color-base-shade4); + background-color: var(--asc-color-background-default); + height: 2.25rem; + flex-wrap: nowrap; + color: var(--asc-color-base-default); + text-wrap: nowrap; +} + +.exploreCommunityCategories__seeMoreIcon { + width: 1rem; + height: 1rem; + fill: var(--asc-color-base-default); +} + +.exploreCommunityCategories__seeMore:hover { + background-color: var(--asc-color-background-shade1); +} + +.exploreCommunityCategories__categoryChip { + display: flex; + justify-content: center; + align-items: center; + height: 2.25rem; +} diff --git a/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.stories.tsx b/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.stories.tsx new file mode 100644 index 000000000..b0b822f31 --- /dev/null +++ b/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.stories.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { ExploreCommunityCategories } from './ExploreCommunityCategories'; + +export default { + title: 'v4-social/components/ExploreCommunityCategories', +}; + +export const ExploreCommunityCategoriesStory = { + render: () => { + return <ExploreCommunityCategories />; + }, + + name: 'ExploreCommunityCategories', +}; diff --git a/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.tsx b/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.tsx new file mode 100644 index 000000000..a28a0b577 --- /dev/null +++ b/src/v4/social/components/ExploreCommunityCategories/ExploreCommunityCategories.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useRef } from 'react'; +import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; + +import { useAmityComponent } from '~/v4/core/hooks/uikit'; +import { CategoryChip } from '~/v4/social/elements/CategoryChip/CategoryChip'; +import { Typography } from '~/v4/core/components'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { Button } from '~/v4/core/natives/Button/Button'; +import clsx from 'clsx'; +import { useExplore } from '~/v4/social/providers/ExploreProvider'; +import { CategoryChipSkeleton } from '~/v4/social/elements/CategoryChip/CategoryChipSkeleton'; + +import styles from './ExploreCommunityCategories.module.css'; +import ChevronRight from '~/v4/icons/ChevronRight'; + +interface ExploreCommunityCategoriesProps { + pageId?: string; +} + +export const ExploreCommunityCategories = ({ pageId = '*' }: ExploreCommunityCategoriesProps) => { + const componentId = 'explore_community_categories'; + const { accessibilityId, themeStyles } = useAmityComponent({ + pageId, + componentId, + }); + + const { goToAllCategoriesPage, goToCommunitiesByCategoryPage } = useNavigation(); + + const { categories, isLoading, fetchCommunityCategories } = useExplore(); + + useEffect(() => { + fetchCommunityCategories(); + }, []); + + const intersectionRef = useRef<HTMLDivElement>(null); + + useIntersectionObserver({ + node: intersectionRef.current, + onIntersect: () => {}, + }); + + if (isLoading) { + return ( + <div + className={styles.exploreCommunityCategories} + style={themeStyles} + data-qa-anchor={accessibilityId} + > + {Array.from({ length: 5 }).map((_, index) => ( + <CategoryChipSkeleton key={index} /> + ))} + </div> + ); + } + + return ( + <div + className={styles.exploreCommunityCategories} + style={themeStyles} + data-qa-anchor={accessibilityId} + > + {categories.map((category) => ( + <div className={styles.exploreCommunityCategories__categoryChip} key={category.categoryId}> + <CategoryChip + pageId={pageId} + category={category} + onClick={(categoryId) => goToCommunitiesByCategoryPage({ categoryId })} + /> + </div> + ))} + + {categories.length >= 5 ? ( + <Typography.BodyBold + renderer={({ typoClassName }) => ( + <Button + className={clsx(typoClassName, styles.exploreCommunityCategories__seeMore)} + onPress={() => goToAllCategoriesPage()} + > + <div>See more</div> + <ChevronRight className={styles.exploreCommunityCategories__seeMoreIcon} /> + </Button> + )} + /> + ) : null} + </div> + ); +}; diff --git a/src/v4/social/components/ExploreCommunityCategories/index.tsx b/src/v4/social/components/ExploreCommunityCategories/index.tsx new file mode 100644 index 000000000..97e51fefc --- /dev/null +++ b/src/v4/social/components/ExploreCommunityCategories/index.tsx @@ -0,0 +1 @@ +export { ExploreCommunityCategories } from './ExploreCommunityCategories'; diff --git a/src/v4/social/components/ExploreCommunityEmpty/ExploreCommunityEmpty.module.css b/src/v4/social/components/ExploreCommunityEmpty/ExploreCommunityEmpty.module.css new file mode 100644 index 000000000..5851eb613 --- /dev/null +++ b/src/v4/social/components/ExploreCommunityEmpty/ExploreCommunityEmpty.module.css @@ -0,0 +1,15 @@ +.exploreCommunityEmpty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: var(--asc-color-background-default); + gap: 1rem; +} + +.exploreCommunityEmpty__text { + padding-bottom: 1.0625rem; + color: var(--asc-color-base-shade3); +} diff --git a/src/v4/social/components/ExploreCommunityEmpty/ExploreCommunityEmpty.tsx b/src/v4/social/components/ExploreCommunityEmpty/ExploreCommunityEmpty.tsx new file mode 100644 index 000000000..11e8aecf3 --- /dev/null +++ b/src/v4/social/components/ExploreCommunityEmpty/ExploreCommunityEmpty.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useAmityComponent } from '~/v4/core/hooks/uikit'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { Description } from '~/v4/social/elements/Description/Description'; +import { ExploreCreateCommunity } from '~/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity'; +import { ExploreEmptyImage } from '~/v4/social/elements/ExploreEmptyImage'; +import { Title } from '~/v4/social/elements/Title/Title'; + +import styles from './ExploreCommunityEmpty.module.css'; + +interface ExploreCommunityEmptyProps { + pageId?: string; +} + +export function ExploreCommunityEmpty({ pageId = '*' }: ExploreCommunityEmptyProps) { + const componentId = 'explore_community_empty'; + + const { goToCommunityCreatePage } = useNavigation(); + + const { themeStyles, accessibilityId } = useAmityComponent({ + componentId, + pageId, + }); + + return ( + <div + className={styles.exploreCommunityEmpty} + style={themeStyles} + data-qa-anchor={accessibilityId} + > + <ExploreEmptyImage pageId={pageId} componentId={componentId} /> + <div className={styles.exploreCommunityEmpty__text}> + <Title pageId={pageId} componentId={componentId} /> + <Description pageId={pageId} componentId={componentId} /> + </div> + <ExploreCreateCommunity + pageId={pageId} + componentId={componentId} + onClick={goToCommunityCreatePage} + /> + </div> + ); +} diff --git a/src/v4/social/components/ExploreCommunityEmpty/index.ts b/src/v4/social/components/ExploreCommunityEmpty/index.ts new file mode 100644 index 000000000..ca97d0b66 --- /dev/null +++ b/src/v4/social/components/ExploreCommunityEmpty/index.ts @@ -0,0 +1 @@ +export { ExploreCommunityEmpty } from './ExploreCommunityEmpty'; diff --git a/src/v4/social/components/ExploreEmpty/ExploreEmpty.module.css b/src/v4/social/components/ExploreEmpty/ExploreEmpty.module.css new file mode 100644 index 000000000..ce355cf9c --- /dev/null +++ b/src/v4/social/components/ExploreEmpty/ExploreEmpty.module.css @@ -0,0 +1,15 @@ +.exploreEmpty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: var(--asc-color-background-default); + gap: 1rem; +} + +.exploreEmpty__text { + padding-bottom: 1.0625rem; + color: var(--asc-color-base-shade3); +} diff --git a/src/v4/social/components/ExploreEmpty/ExploreEmpty.tsx b/src/v4/social/components/ExploreEmpty/ExploreEmpty.tsx new file mode 100644 index 000000000..6a61bea6d --- /dev/null +++ b/src/v4/social/components/ExploreEmpty/ExploreEmpty.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useAmityComponent } from '~/v4/core/hooks/uikit'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { Description } from '~/v4/social/elements/Description/Description'; +import { ExploreCreateCommunity } from '~/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity'; +import { ExploreEmptyImage } from '~/v4/social/elements/ExploreEmptyImage/ExploreEmptyImage'; +import { Title } from '~/v4/social/elements/Title/Title'; + +import styles from './ExploreEmpty.module.css'; + +interface ExploreEmptyProps { + pageId?: string; +} + +export function ExploreEmpty({ pageId = '*' }: ExploreEmptyProps) { + const componentId = 'explore_empty'; + + const { goToCommunityCreatePage } = useNavigation(); + + const { themeStyles, accessibilityId } = useAmityComponent({ + componentId, + pageId, + }); + + return ( + <div className={styles.exploreEmpty} style={themeStyles} data-qa-anchor={accessibilityId}> + <ExploreEmptyImage pageId={pageId} componentId={componentId} /> + <div className={styles.exploreEmpty__text}> + <Title pageId={pageId} componentId={componentId} /> + <Description pageId={pageId} componentId={componentId} /> + </div> + <ExploreCreateCommunity + pageId={pageId} + componentId={componentId} + onClick={goToCommunityCreatePage} + /> + </div> + ); +} diff --git a/src/v4/social/components/ExploreEmpty/index.ts b/src/v4/social/components/ExploreEmpty/index.ts new file mode 100644 index 000000000..772ca67a8 --- /dev/null +++ b/src/v4/social/components/ExploreEmpty/index.ts @@ -0,0 +1 @@ +export { ExploreEmpty } from './ExploreEmpty'; diff --git a/src/v4/social/components/GlobalFeed/GlobalFeed.module.css b/src/v4/social/components/GlobalFeed/GlobalFeed.module.css index ddaa89ca6..f7fa0d357 100644 --- a/src/v4/social/components/GlobalFeed/GlobalFeed.module.css +++ b/src/v4/social/components/GlobalFeed/GlobalFeed.module.css @@ -45,6 +45,10 @@ width: 100%; } +.global_feed__divider + .global_feed__divider { + display: none; +} + .global_feed__post__divider { width: 100%; background-color: var(--asc-color-base-shade4); diff --git a/src/v4/social/components/GlobalFeed/GlobalFeed.tsx b/src/v4/social/components/GlobalFeed/GlobalFeed.tsx index c6ccaf817..5df69d96b 100644 --- a/src/v4/social/components/GlobalFeed/GlobalFeed.tsx +++ b/src/v4/social/components/GlobalFeed/GlobalFeed.tsx @@ -1,17 +1,16 @@ -import React, { useRef, useState } from 'react'; +import React, { useState } from 'react'; import { PostContent, PostContentSkeleton } from '~/v4/social/components/PostContent'; import { EmptyNewsfeed } from '~/v4/social/components/EmptyNewsFeed/EmptyNewsFeed'; import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; import { usePageBehavior } from '~/v4/core/providers/PageBehaviorProvider'; - -import styles from './GlobalFeed.module.css'; import { useAmityComponent } from '~/v4/core/hooks/uikit'; import { PostAd } from '~/v4/social/internal-components/PostAd/PostAd'; -import { Button } from '~/v4/core/natives/Button'; import { AmityPostCategory, AmityPostContentComponentStyle, } from '~/v4/social/components/PostContent/PostContent'; +import { ClickableArea } from '~/v4/core/natives/ClickableArea'; +import styles from './GlobalFeed.module.css'; interface GlobalFeedProps { pageId?: string; @@ -68,12 +67,13 @@ export const GlobalFeed = ({ return ( <div className={styles.global_feed} style={themeStyles} data-qa-anchor={accessibilityId}> {items.map((item, index) => ( - <div key={getItemKey(item, items[Math.max(0, index - 1)])}> + <React.Fragment key={getItemKey(item, items[Math.max(0, index - 1)])}> {index !== 0 ? <div className={styles.global_feed__divider} /> : null} {isAmityAd(item) ? ( <PostAd ad={item} /> ) : ( - <Button + <ClickableArea + elementType="div" className={styles.global_feed__postContainer} onPress={() => AmityGlobalFeedComponentBehavior?.goToPostDetailPage?.({ postId: item.postId }) @@ -89,17 +89,18 @@ export const GlobalFeed = ({ }} onPostDeleted={onPostDeleted} /> - </Button> + </ClickableArea> )} - </div> + </React.Fragment> ))} + {items.length > 0 ? <div className={styles.global_feed__divider} /> : null} {isLoading ? Array.from({ length: 2 }).map((_, index) => ( <div key={index}> - <div className={styles.global_feed__divider} /> <div className={styles.global_feed__postSkeletonContainer}> <PostContentSkeleton /> </div> + {index !== 1 ? <div className={styles.global_feed__divider} /> : null} </div> )) : null} diff --git a/src/v4/social/components/Newsfeed/Newsfeed.module.css b/src/v4/social/components/Newsfeed/Newsfeed.module.css index 215bf7297..ac85223eb 100644 --- a/src/v4/social/components/Newsfeed/Newsfeed.module.css +++ b/src/v4/social/components/Newsfeed/Newsfeed.module.css @@ -47,6 +47,10 @@ width: 100%; } +.newsfeed__divider + .newsfeed__divider { + display: none; +} + .newsfeed__post__divider { width: 100%; background-color: var(--asc-color-base-shade4); diff --git a/src/v4/social/components/Newsfeed/Newsfeed.tsx b/src/v4/social/components/Newsfeed/Newsfeed.tsx index 741aa2244..b1a4de7bd 100644 --- a/src/v4/social/components/Newsfeed/Newsfeed.tsx +++ b/src/v4/social/components/Newsfeed/Newsfeed.tsx @@ -6,40 +6,7 @@ import { GlobalFeed } from '~/v4/social/components/GlobalFeed'; import styles from './Newsfeed.module.css'; import { useAmityComponent } from '~/v4/core/hooks/uikit'; import { useGlobalFeedContext } from '~/v4/social/providers/GlobalFeedProvider'; - -const Spinner = (props: React.SVGProps<SVGSVGElement>) => { - return ( - <div className={styles.newsfeed__pullToRefresh__spinner}> - <svg - xmlns="http://www.w3.org/2000/svg" - width="21" - height="20" - viewBox="0 0 21 20" - fill="none" - > - <path - fillRule="evenodd" - clipRule="evenodd" - d="M11.1122 5C11.1122 5.39449 10.7924 5.71429 10.3979 5.71429C10.0035 5.71429 9.68366 5.39449 9.68366 5V0.714286C9.68366 0.319797 10.0035 0 10.3979 0C10.7924 0 11.1122 0.319797 11.1122 0.714286V5ZM8.25509 6.28846C8.59673 6.09122 8.71378 5.65437 8.51654 5.31273L6.37368 1.60119C6.17644 1.25955 5.73959 1.1425 5.39795 1.33975C5.05631 1.53699 4.93926 1.97384 5.1365 2.31548L7.27936 6.02702C7.4766 6.36865 7.91346 6.48571 8.25509 6.28846ZM6.42496 6.88141C6.7666 7.07865 6.88366 7.51551 6.68641 7.85714C6.48917 8.19878 6.05232 8.31583 5.71068 8.11859L1.99914 5.97573C1.6575 5.77849 1.54045 5.34164 1.7377 5C1.93494 4.65836 2.37179 4.54131 2.71343 4.73855L6.42496 6.88141ZM6.11224 10C6.11224 9.60551 5.79244 9.28571 5.39795 9.28571H1.11223C0.717746 9.28571 0.397949 9.60551 0.397949 10C0.397949 10.3945 0.717746 10.7143 1.11224 10.7143H5.39795C5.79244 10.7143 6.11224 10.3945 6.11224 10ZM5.71068 11.8814C6.05232 11.6842 6.48917 11.8012 6.68641 12.1429C6.88366 12.4845 6.7666 12.9213 6.42497 13.1186L2.71343 15.2614C2.37179 15.4587 1.93494 15.3416 1.7377 15C1.54045 14.6584 1.6575 14.2215 1.99914 14.0243L5.71068 11.8814ZM8.25509 13.7115C7.91345 13.5143 7.4766 13.6313 7.27936 13.973L5.1365 17.6845C4.93926 18.0262 5.05631 18.463 5.39795 18.6603C5.73959 18.8575 6.17644 18.7404 6.37368 18.3988L8.51654 14.6873C8.71378 14.3456 8.59673 13.9088 8.25509 13.7115ZM10.3979 14.2857C10.0035 14.2857 9.68366 14.6055 9.68366 15V19.2857C9.68366 19.6802 10.0035 20 10.3979 20C10.7924 20 11.1122 19.6802 11.1122 19.2857V15C11.1122 14.6055 10.7924 14.2857 10.3979 14.2857ZM12.5408 6.28846C12.8824 6.48571 13.3193 6.36865 13.5165 6.02702L15.6594 2.31548C15.8566 1.97384 15.7396 1.53699 15.3979 1.33975C15.0563 1.1425 14.6195 1.25956 14.4222 1.60119L12.2794 5.31273C12.0821 5.65437 12.1992 6.09122 12.5408 6.28846ZM15.0852 8.11859C14.7436 8.31583 14.3067 8.19878 14.1095 7.85714C13.9122 7.51551 14.0293 7.07866 14.3709 6.88141L18.0825 4.73855C18.4241 4.54131 18.861 4.65836 19.0582 5C19.2554 5.34164 19.1384 5.77849 18.7968 5.97573L15.0852 8.11859ZM14.6837 10C14.6837 10.3945 15.0035 10.7143 15.3979 10.7143H19.6837C20.0782 10.7143 20.3979 10.3945 20.3979 10C20.3979 9.60551 20.0782 9.28571 19.6837 9.28571H15.3979C15.0035 9.28571 14.6837 9.60551 14.6837 10ZM14.3709 13.1186C14.0293 12.9213 13.9122 12.4845 14.1095 12.1429C14.3067 11.8012 14.7436 11.6842 15.0852 11.8814L18.7968 14.0243C19.1384 14.2215 19.2554 14.6584 19.0582 15C18.861 15.3416 18.4241 15.4587 18.0825 15.2614L14.3709 13.1186ZM12.5408 13.7115C12.1992 13.9088 12.0821 14.3456 12.2794 14.6873L14.4222 18.3988C14.6195 18.7404 15.0563 18.8575 15.3979 18.6603C15.7396 18.463 15.8566 18.0262 15.6594 17.6845L13.5165 13.973C13.3193 13.6313 12.8824 13.5143 12.5408 13.7115Z" - fill="url(#paint0_angular_1709_10374)" - /> - <defs> - <radialGradient - id="paint0_angular_1709_10374" - cx="0" - cy="0" - r="1" - gradientUnits="userSpaceOnUse" - gradientTransform="translate(10.3979 10) scale(10)" - > - <stop offset="0.669733" stopColor="#595F67" /> - <stop offset="0.716307" stopColor="#262626" stopOpacity="0.01" /> - </radialGradient> - </defs> - </svg> - </div> - ); -}; +import { RefreshSpinner } from '~/v4/icons/RefreshSpinner'; interface NewsfeedProps { pageId?: string; @@ -103,7 +70,7 @@ export const Newsfeed = ({ pageId = '*' }: NewsfeedProps) => { } className={styles.newsfeed__pullToRefresh} > - <Spinner /> + <RefreshSpinner className={styles.newsfeed__pullToRefresh__spinner} /> </div> <div className={styles.newsfeed__divider} /> <StoryTab type="globalFeed" pageId={pageId} /> diff --git a/src/v4/social/components/PostContent/ImageContent/ImageContent.tsx b/src/v4/social/components/PostContent/ImageContent/ImageContent.tsx index 34e67c542..26002e171 100644 --- a/src/v4/social/components/PostContent/ImageContent/ImageContent.tsx +++ b/src/v4/social/components/PostContent/ImageContent/ImageContent.tsx @@ -24,12 +24,16 @@ const ImageThumbnail = ({ fileId }: { fileId: string }) => { }; const Image = ({ + pageId = '*', + componentId = '*', postId, index, imageLeftCount, postAmount, onImageClick, }: { + pageId?: string; + componentId?: string; postId: string; index: number; imageLeftCount: number; @@ -44,6 +48,7 @@ const Image = ({ return ( <Button + data-qa-anchor={`${pageId}/${componentId}/post_image`} key={imagePost.postId} className={styles.imageContent__imgContainer} onPress={() => onImageClick()} @@ -93,6 +98,8 @@ export const ImageContent = ({ > {first4Images.map((postId: string, index: number) => ( <Image + pageId={pageId} + componentId={componentId} key={postId} postId={postId} index={index} diff --git a/src/v4/social/components/PostContent/LinkPreview/LinkPreview.tsx b/src/v4/social/components/PostContent/LinkPreview/LinkPreview.tsx index 46bd6f219..1c78b0d8d 100644 --- a/src/v4/social/components/PostContent/LinkPreview/LinkPreview.tsx +++ b/src/v4/social/components/PostContent/LinkPreview/LinkPreview.tsx @@ -52,10 +52,12 @@ const usePreviewLink = ({ url }: { url: string }) => { }; interface LinkPreviewProps { + pageId?: string; + componentId?: string; url: string; } -export function LinkPreview({ url }: LinkPreviewProps) { +export function LinkPreview({ pageId = '*', componentId = '*', url }: LinkPreviewProps) { const urlObject = new URL(url); const previewData = usePreviewLink({ url }); @@ -82,7 +84,11 @@ export function LinkPreview({ url }: LinkPreviewProps) { } return ( - <Button onPress={handleClick} className={styles.linkPreview}> + <Button + data-qa-anchor={`${pageId}/${componentId}/post_preview_link`} + onPress={handleClick} + className={styles.linkPreview} + > <div className={styles.linkPreview__top}> {previewData.data?.image ? ( <object data={previewData.data.image} className={styles.linkPreview__object}> diff --git a/src/v4/social/components/PostContent/PostContent.module.css b/src/v4/social/components/PostContent/PostContent.module.css index 730cad73f..8e7d7c1a7 100644 --- a/src/v4/social/components/PostContent/PostContent.module.css +++ b/src/v4/social/components/PostContent/PostContent.module.css @@ -5,7 +5,7 @@ .postContent__bar { display: grid; - grid-template-columns: min-content 1fr min-content; + grid-template-columns: min-content minmax(0, 1fr) min-content; align-items: center; gap: 0.5rem; } @@ -18,6 +18,10 @@ padding-bottom: 0.5rem; } +.postContent__bar__detail { + overflow: hidden; +} + .postContent__bar__information__subtitle { display: flex; align-items: center; @@ -31,6 +35,13 @@ gap: 0.25rem; } +.postContent__bar__information__subtitle__brand { + display: flex; + justify-content: center; + align-items: center; + gap: 0.25rem; +} + .postContent__bar__information__subtitle__separator { color: var(--asc-color-base-shade2); } @@ -92,13 +103,22 @@ } .postTitle { - display: flex; + display: grid; align-items: center; + grid-template-columns: minmax(0, max-content); + grid-auto-flow: column; gap: 0.25rem; } +.postTitle[data-show-target-community='true'] { + grid-template-columns: minmax(0, max-content) minmax(50%, 1fr); +} + .postTitle__text { color: var(--asc-color-base-default); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .postTitle__icon { @@ -107,6 +127,68 @@ width: 1rem; } +.postTitle__user__container { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-auto-flow: column; + gap: 0.25rem; + align-items: center; + max-width: 100%; +} + +.postTitle__user__container[data-show-brand-badge='true'][data-show-target='true'] { + grid-template-columns: minmax(0, min-content) min-content min-content; +} + +.postTitle__user__container[data-show-brand-badge='false'][data-show-target='true'], +.postTitle__user__container[data-show-brand-badge='true'][data-show-target='false'] { + grid-template-columns: minmax(0, min-content) min-content; +} + +.postTitle__brandIcon { + width: 1.25rem; + height: 1.25rem; +} + +.postTitle__community { + display: grid; + grid-template-columns: minmax(0, min-content); + grid-auto-flow: column; + gap: 0.25rem; + align-items: center; + max-width: 100%; +} + +.postTitle__community[data-show-private-badge='true'][data-show-official-badge='true'] { + grid-template-columns: min-content minmax(0, min-content) min-content; +} + +.postTitle__community[data-show-private-badge='true'][data-show-official-badge='false'] { + grid-template-columns: min-content minmax(0, min-content); +} + +.postTitle__community[data-show-private-badge='false'][data-show-official-badge='true'] { + grid-template-columns: minmax(0, min-content) min-content; +} + +.postTitle__communityText { + color: var(--asc-color-base-default); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.postTitle__community__privateIcon { + width: 0.75rem; + height: 0.75rem; +} + +.postTitle__community__verifiedIcon { + width: 1.25rem; + height: 1.25rem; +} + .postContent__comment { display: grid; } diff --git a/src/v4/social/components/PostContent/PostContent.tsx b/src/v4/social/components/PostContent/PostContent.tsx index d359ef780..c2d360b4d 100644 --- a/src/v4/social/components/PostContent/PostContent.tsx +++ b/src/v4/social/components/PostContent/PostContent.tsx @@ -6,7 +6,6 @@ import { ModeratorBadge } from '~/v4/social/elements/ModeratorBadge'; import { MenuButton } from '~/v4/social/elements/MenuButton'; import { ShareButton } from '~/v4/social/elements/ShareButton'; import useCommunity from '~/v4/core/hooks/collections/useCommunity'; -import { useUser } from '~/v4/core/hooks/objects/useUser'; import { Typography } from '~/v4/core/components'; import AngleRight from '~/v4/icons/AngleRight'; import { UserAvatar } from '~/v4/social/internal-components/UserAvatar'; @@ -40,6 +39,11 @@ import dayjs from 'dayjs'; import { useVisibilitySensor } from '~/v4/social/hooks/useVisibilitySensor'; import { AnnouncementBadge } from '~/v4/social/elements/AnnouncementBadge'; import { PinBadge } from '~/v4/social/elements/PinBadge'; +import { BrandBadge } from '~/v4/social/internal-components/BrandBadge'; +import clsx from 'clsx'; +import { Lock } from '~/icons'; +import Verified from '~/v4/icons/Verified'; +import { useUser } from '~/v4/core/hooks/objects/useUser'; export enum AmityPostContentComponentStyle { FEED = 'feed', @@ -56,107 +60,141 @@ export enum AmityPostCategory { interface PostTitleProps { post: Amity.Post; pageId?: string; + componentId?: string; hideTarget?: boolean; } -const PostTitle = ({ pageId, post, hideTarget }: PostTitleProps) => { - const shouldCall = useMemo(() => post?.targetType === 'community', [post?.targetType]); +const PostTitle = ({ pageId, componentId, post, hideTarget }: PostTitleProps) => { + const shouldCallCommunity = useMemo(() => post?.targetType === 'community', [post?.targetType]); + const shouldCallUser = useMemo( + () => post?.targetType === 'user' && post?.postedUserId !== post?.targetId, + [post?.targetType, post?.postedUserId, post?.targetId], + ); const { community: targetCommunity } = useCommunity({ communityId: post?.targetId, - shouldCall, + shouldCall: shouldCallCommunity, }); - const { goToCommunityProfilePage } = useNavigation(); + const { user: targetUser } = useUser({ + userId: post?.targetId, + shouldCall: shouldCallUser, + }); - const { user: postedUser } = useUser(post.postedUserId); - const { onClickCommunity, onClickUser } = useNavigation(); + const { goToCommunityProfilePage, onClickUser } = useNavigation(); - if (targetCommunity) { - return ( - <div className={styles.postTitle}> - {postedUser && ( - <Button onPress={() => onClickUser(postedUser.userId)}> - <Typography.BodyBold className={styles.postTitle__text}> - {postedUser.displayName} - </Typography.BodyBold> - </Button> - )} - {targetCommunity && !hideTarget && ( - <Button onPress={() => goToCommunityProfilePage(targetCommunity.communityId)}> - <AngleRight className={styles.postTitle__icon} /> - <Typography.BodyBold className={styles.postTitle__text}> - {targetCommunity.displayName} - </Typography.BodyBold>{' '} - </Button> - )} - </div> - ); - } + const showTargetCommunity = targetCommunity && !hideTarget; + const showTargetUser = targetUser && !hideTarget; + const showBrandBadge = post.creator.isBrand; + const showPrivateBadge = targetCommunity?.isPublic === false; + const showOfficialBadge = targetCommunity?.isOfficial === true; + + const showTarget = showTargetCommunity || showTargetUser; return ( - <Button onPress={() => postedUser && onClickUser(postedUser.userId)}> - <Typography.BodyBold className={styles.postTitle__text}> - {postedUser?.displayName} - </Typography.BodyBold> - </Button> + <div className={styles.postTitle} data-show-target-community={showTargetCommunity === true}> + {post.creator && ( + <div + className={styles.postTitle__user__container} + data-show-brand-badge={showBrandBadge === true} + data-show-target={showTarget === true} + > + <Typography.BodyBold + renderer={({ typoClassName }) => ( + <Button + className={clsx(typoClassName, styles.postTitle__text)} + onPress={() => onClickUser(post.creator.userId)} + data-qa-anchor={`${pageId}/${componentId}/username`} + > + {post.creator.displayName} + </Button> + )} + /> + {showBrandBadge ? <BrandBadge className={styles.postTitle__brandIcon} /> : null} + + {showTarget ? ( + <AngleRight + data-qa-anchor={`${pageId}/${componentId}/arrow_right`} + className={styles.postTitle__icon} + /> + ) : null} + </div> + )} + {showTargetCommunity && ( + <div + className={styles.postTitle__community} + data-show-private-badge={showPrivateBadge === true} + data-show-official-badge={showOfficialBadge === true} + > + {showPrivateBadge && <Lock className={styles.postTitle__community__privateIcon} />} + <Typography.BodyBold + renderer={({ typoClassName }) => ( + <Button + data-qa-anchor={`${pageId}/${componentId}/community_name`} + className={clsx(typoClassName, styles.postTitle__communityText)} + onPress={() => { + goToCommunityProfilePage(targetCommunity.communityId); + }} + > + {targetCommunity.displayName} + </Button> + )} + /> + {showOfficialBadge && <Verified className={styles.postTitle__community__verifiedIcon} />} + </div> + )} + {showTargetUser && ( + <div + className={styles.postTitle__user__container} + data-show-brand-badge={targetUser?.isBrand === true} + data-show-target={false} + > + <Typography.BodyBold + renderer={({ typoClassName }) => ( + <Button + className={clsx(typoClassName, styles.postTitle__text)} + onPress={() => onClickUser(targetUser.userId)} + > + {targetUser.displayName} + </Button> + )} + /> + {targetUser?.isBrand === true ? ( + <BrandBadge className={styles.postTitle__brandIcon} /> + ) : null} + </div> + )} + </div> ); }; -const useMutateAddReaction = ({ - postId, - reactionByMe, -}: { - postId: string; - reactionByMe: string | null; -}) => - useMutation({ - mutationFn: async (reactionKey: string) => { - if (reactionByMe) { - try { - await ReactionRepository.removeReaction('post', postId, reactionByMe); - } catch { - console.log("Can't remove reaction."); - } - } - return ReactionRepository.addReaction('post', postId, reactionKey); - }, - }); - -const useMutateRemoveReaction = ({ - postId, - reactionsByMe, -}: { - postId: string; - reactionsByMe: string[]; -}) => - useMutation({ - mutationFn: async () => { - return Promise.all( - reactionsByMe.map((reaction) => { - try { - return ReactionRepository.removeReaction('post', postId, reaction); - } catch (e) { - console.log("Can't remove reaction."); - } - }), - ); - }, - }); - const ChildrenPostContent = ({ + pageId, + componentId, post, onImageClick, onVideoClick, }: { + pageId?: string; + componentId?: string; post: Amity.Post[]; onImageClick: (imageIndex: number) => void; onVideoClick: (videoIndex: number) => void; }) => { return ( <> - <ImageContent post={post} onImageClick={onImageClick} /> - <VideoContent post={post} onVideoClick={onVideoClick} /> + <ImageContent + pageId={pageId} + componentId={componentId} + post={post} + onImageClick={onImageClick} + /> + <VideoContent + pageId={pageId} + componentId={componentId} + post={post} + onVideoClick={onVideoClick} + /> </> ); }; @@ -342,11 +380,16 @@ export const PostContent = ({ )} <div className={styles.postContent__bar} data-type={style}> <div className={styles.postContent__bar__userAvatar}> - <UserAvatar userId={post?.postedUserId} /> + <UserAvatar pageId={pageId} componentId={componentId} userId={post?.postedUserId} /> </div> - <div> + <div className={styles.postContent__bar__detail}> <div> - <PostTitle post={post} hideTarget={hideTarget} /> + <PostTitle + post={post} + hideTarget={hideTarget} + pageId={pageId} + componentId={componentId} + /> </div> <div className={styles.postContent__bar__information__subtitle}> {isCommunityModerator ? ( @@ -397,9 +440,17 @@ export const PostContent = ({ </div> <div className={styles.postContent__content_and_reactions}> <div className={styles.postContent__content}> - <TextContent text={post.data.text} mentionees={post?.metadata?.mentioned} /> + <TextContent + pageId={pageId} + componentId={componentId} + text={post?.data?.text} + mentioned={post?.metadata?.mentioned} + mentionees={post?.mentioness} + /> {post.children.length > 0 ? ( <ChildrenPostContent + pageId={pageId} + componentId={componentId} post={post} onImageClick={openImageViewer} onVideoClick={openVideoViewer} @@ -441,14 +492,20 @@ export const PostContent = ({ )} </div> ) : null} - <Typography.Caption className={styles.postContent__reactionsBar__reactions__count}> + <Typography.Caption + data-qa-anchor={`${pageId}/${componentId}/like_count`} + className={styles.postContent__reactionsBar__reactions__count} + > {`${millify(post?.reactionsCount || 0)} ${ post?.reactionsCount === 1 ? 'like' : 'likes' }`} </Typography.Caption> </div> - <Typography.Caption className={styles.postContent__commentsCount}> + <Typography.Caption + data-qa-anchor={`${pageId}/${componentId}/comment_count`} + className={styles.postContent__commentsCount} + > {`${post?.commentsCount || 0} ${post?.commentsCount === 1 ? 'comment' : 'comments'}`} </Typography.Caption> </div> @@ -459,7 +516,9 @@ export const PostContent = ({ Join community to interact with all posts </Typography.Body> </> - ) : !targetCommunity?.isJoined && page.type === PageTypes.PostDetailPage ? null : ( + ) : targetCommunity && + !targetCommunity?.isJoined && + page.type === PageTypes.PostDetailPage ? null : ( <> <div className={styles.postContent__divider} /> <div className={styles.postContent__reactionBar}> diff --git a/src/v4/social/components/PostContent/TextContent/TextContent.tsx b/src/v4/social/components/PostContent/TextContent/TextContent.tsx index 0303dc108..9c0b595cc 100644 --- a/src/v4/social/components/PostContent/TextContent/TextContent.tsx +++ b/src/v4/social/components/PostContent/TextContent/TextContent.tsx @@ -1,11 +1,12 @@ import React, { useState, useMemo, ReactNode, useEffect } from 'react'; import { Linkify } from '~/v4/social/internal-components/Linkify'; -import { Mentioned, findChunks, processChunks } from '~/v4/helpers/utils'; +import { Mentioned, findChunks, processChunks, Mentionees } from '~/v4/helpers/utils'; import { Typography } from '~/v4/core/components'; import { LinkPreview } from '~/v4/social/components/PostContent/LinkPreview/LinkPreview'; import styles from './TextContent.module.css'; import * as linkify from 'linkifyjs'; +import { TextWithMention } from '~/v4/social/internal-components/TextWithMention/TextWithMention'; interface MentionHighlightTagProps { children: ReactNode; @@ -19,24 +20,20 @@ const MentionHighlightTag = ({ children }: MentionHighlightTagProps) => { const MAX_TEXT_LENGTH = 500; interface TextContentProps { + pageId?: string; + componentId?: string; text?: string; - mentionees?: Mentioned[]; + mentioned?: Mentioned[]; + mentionees?: Mentionees; } -export const TextContent = ({ text = '', mentionees }: TextContentProps) => { - const needReadMore = text.length > MAX_TEXT_LENGTH; - - const [isReadMoreClick, setIsReadMoreClick] = useState(false); - - const isShowReadMore = needReadMore && !isReadMoreClick; - - const chunks = useMemo(() => { - if (isShowReadMore) { - return processChunks(text.substring(0, MAX_TEXT_LENGTH), findChunks(mentionees)); - } - return processChunks(text, findChunks(mentionees)); - }, [mentionees, text, isShowReadMore]); - +export const TextContent = ({ + pageId = '*', + componentId = '*', + text = '', + mentionees = [], + mentioned, +}: TextContentProps) => { if (!text) { return null; } @@ -45,30 +42,18 @@ export const TextContent = ({ text = '', mentionees }: TextContentProps) => { return ( <> - <Typography.Body className={styles.postContent}> - {chunks.map((chunk) => { - const key = `${text}-${chunk.start}-${chunk.end}`; - const sub = text.substring(chunk.start, chunk.end); - if (chunk.highlight) { - const mentionee = mentionees?.find((m) => m.index === chunk.start); - if (mentionee) { - return ( - <MentionHighlightTag key={key} mentionee={mentionee}> - {sub} - </MentionHighlightTag> - ); - } - return <span key={key}>{sub}</span>; - } - return <Linkify key={key}>{sub}</Linkify>; - })} - {isShowReadMore ? ( - <span className={styles.postContent__readmore} onClick={() => setIsReadMoreClick(true)}> - ...Read more - </span> - ) : null} - </Typography.Body> - {linksFounded && linksFounded.length > 0 && <LinkPreview url={linksFounded[0].href} />} + <TextWithMention + pageId={pageId} + componentId={componentId} + data={{ text: text }} + mentionees={mentionees} + metadata={{ + mentioned, + }} + /> + {linksFounded && linksFounded.length > 0 && ( + <LinkPreview pageId={pageId} componentId={componentId} url={linksFounded[0].href} /> + )} </> ); }; diff --git a/src/v4/social/components/PostContent/VideoContent/VideoContent.tsx b/src/v4/social/components/PostContent/VideoContent/VideoContent.tsx index 5cb60fa5b..2b9006c5e 100644 --- a/src/v4/social/components/PostContent/VideoContent/VideoContent.tsx +++ b/src/v4/social/components/PostContent/VideoContent/VideoContent.tsx @@ -35,12 +35,16 @@ const VideoThumbnail = ({ fileId }: { fileId: string }) => { }; const Video = ({ + pageId = '*', + componentId = '*', postId, postAmount, videoLeftCount, index, onVideoClick, }: { + pageId?: string; + componentId?: string; postId: string; postAmount: number; videoLeftCount: number; @@ -58,6 +62,7 @@ const Video = ({ className={styles.videoContent__videoContainer} data-videos-amount={Math.min(postAmount, 4)} onPress={() => onVideoClick()} + data-qa-anchor={`${pageId}/${componentId}/post_video`} > <VideoThumbnail fileId={videoPost.data.thumbnailFileId} /> {videoLeftCount > 0 && index === postAmount - 1 && ( @@ -91,7 +96,7 @@ export const VideoContent = ({ post, onVideoClick, }: VideoContentProps) => { - const { themeStyles } = useAmityElement({ + const { themeStyles, accessibilityId } = useAmityElement({ pageId, componentId, elementId, @@ -116,6 +121,8 @@ export const VideoContent = ({ > {first4Videos.map((postId: string, index: number) => ( <Video + pageId={pageId} + componentId={componentId} key={postId} index={index} postId={postId} diff --git a/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.module.css b/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.module.css new file mode 100644 index 000000000..8ed6cc448 --- /dev/null +++ b/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.module.css @@ -0,0 +1,91 @@ +.recommendedCommunities { + display: inline-flex; + flex-wrap: nowrap; + gap: 0.75rem; + overflow-x: scroll; + width: 100%; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.recommendedCommunityCard { + border-radius: var(--asc-border-radius-sm); + border: 1px solid var(--asc-color-base-shade4); + width: 16.75rem; + height: calc(7.8rem + 6rem); +} + +.recommendedCommunityCard__image { + width: 100%; + height: 7.8rem; + overflow: hidden; +} + +.recommendedCommunityCard__img { + border-radius: var(--asc-border-radius-sm) var(--asc-border-radius-sm) 0 0; +} + +.recommendedCommunityCard__content { + display: flex; + flex-direction: column; + padding: 0.62rem; + width: 100%; +} + +.recommendedCommunityCard__bottom { + display: flex; + justify-content: space-between; + flex-wrap: nowrap; + gap: 0.5rem; + width: 100%; +} + +.recommendedCommunityCard__content__left { + display: flex; + justify-content: center; + align-items: start; + flex-direction: column; + gap: 0.25rem; + flex-shrink: 1; + width: 9rem; +} + +.recommendedCommunities__contentTitle { + display: flex; + align-items: center; + gap: 0.12rem; + width: 100%; +} + +.recommendedCommunities__content__right { + display: flex; + align-items: end; + align-self: flex-end; + flex: 0 0 4.2rem; +} + +.recommendedCommunityCard__name { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.recommendedCommunityCard__communityName { + display: flex; + justify-content: start; + align-items: center; + gap: 0.25rem; + width: 100%; +} + +.recommendedCommunityCard__communityName__private, +.recommendedCommunityCard__communityName__official { + width: 1.25rem; + height: 1.25rem; + display: flex; + justify-content: center; + align-items: center; + padding-top: 0.22rem; + padding-bottom: 0.28rem; +} diff --git a/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.stories.tsx b/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.stories.tsx new file mode 100644 index 000000000..1ce483276 --- /dev/null +++ b/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.stories.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { RecommendedCommunities } from './RecommendedCommunities'; + +export default { + title: 'v4-social/components/RecommendedCommunities', +}; + +export const RecommendedCommunitiesStory = { + render: () => { + return <RecommendedCommunities />; + }, + + name: 'RecommendedCommunities', +}; diff --git a/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.tsx b/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.tsx new file mode 100644 index 000000000..141e00c83 --- /dev/null +++ b/src/v4/social/components/RecommendedCommunities/RecommendedCommunities.tsx @@ -0,0 +1,178 @@ +import React, { useEffect } from 'react'; + +import styles from './RecommendedCommunities.module.css'; +import { useAmityComponent } from '~/v4/core/hooks/uikit'; +import { CommunityJoinButton } from '~/v4/social/elements/CommunityJoinButton/CommunityJoinButton'; +import { CommunityMembersCount } from '~/v4/social/elements/CommunityMembersCount/CommunityMembersCount'; +import { CommunityCategories } from '~/v4/social/internal-components/CommunityCategories/CommunityCategories'; +import { CommunityPrivateBadge } from '~/v4/social/elements/CommunityPrivateBadge/CommunityPrivateBadge'; +import { CommunityDisplayName } from '~/v4/social/elements/CommunityDisplayName/CommunityDisplayName'; +import { CommunityOfficialBadge } from '~/v4/social/elements/CommunityOfficialBadge/CommunityOfficialBadge'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { CommunityCardImage } from '~/v4/social/elements/CommunityCardImage'; +import useImage from '~/core/hooks/useImage'; +import { RecommendedCommunityCardSkeleton } from './RecommendedCommunityCardSkeleton'; +import { useExplore } from '~/v4/social/providers/ExploreProvider'; +import { CommunityJoinedButton } from '~/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton'; +import { useCommunityActions } from '~/v4/social/hooks/useCommunityActions'; +import { ClickableArea } from '~/v4/core/natives/ClickableArea/ClickableArea'; + +interface RecommendedCommunityCardProps { + community: Amity.Community; + pageId: string; + componentId: string; + onClick: (communityId: string) => void; + onCategoryClick?: (categoryId: string) => void; + onJoinButtonClick: (communityId: string) => void; + onLeaveButtonClick: (communityId: string) => void; +} + +const RecommendedCommunityCard = ({ + pageId, + componentId, + community, + onClick, + onCategoryClick, + onJoinButtonClick, + onLeaveButtonClick, +}: RecommendedCommunityCardProps) => { + const avatarUrl = useImage({ + fileId: community.avatarFileId, + imageSize: 'medium', + }); + + return ( + <ClickableArea + elementType="div" + className={styles.recommendedCommunityCard} + onPress={() => onClick(community.communityId)} + > + <div className={styles.recommendedCommunityCard__image}> + <CommunityCardImage + pageId={pageId} + componentId={componentId} + imgSrc={avatarUrl} + className={styles.recommendedCommunityCard__img} + /> + </div> + <div className={styles.recommendedCommunityCard__content}> + <div className={styles.recommendedCommunities__contentTitle}> + {!community.isPublic && ( + <div className={styles.recommendedCommunityCard__communityName__private}> + <CommunityPrivateBadge pageId={pageId} componentId={componentId} /> + </div> + )} + <CommunityDisplayName pageId={pageId} componentId={componentId} community={community} /> + {community.isOfficial && ( + <div className={styles.recommendedCommunityCard__communityName__official}> + <CommunityOfficialBadge pageId={pageId} componentId={componentId} /> + </div> + )} + </div> + <div className={styles.recommendedCommunityCard__bottom}> + <div className={styles.recommendedCommunityCard__content__left}> + <CommunityCategories + pageId={pageId} + componentId={componentId} + community={community} + onClick={onCategoryClick} + truncate + maxCategoryCharacters={5} + maxCategoriesLength={2} + /> + <CommunityMembersCount + pageId={pageId} + componentId={componentId} + memberCount={community.membersCount} + /> + </div> + <div className={styles.recommendedCommunities__content__right}> + {community.isJoined ? ( + <CommunityJoinedButton + pageId={pageId} + componentId={componentId} + onClick={() => onLeaveButtonClick(community.communityId)} + /> + ) : ( + <CommunityJoinButton + pageId={pageId} + componentId={componentId} + onClick={() => onJoinButtonClick(community.communityId)} + /> + )} + </div> + </div> + </div> + </ClickableArea> + ); +}; + +interface RecommendedCommunitiesProps { + pageId?: string; +} + +export const RecommendedCommunities = ({ pageId = '*' }: RecommendedCommunitiesProps) => { + const componentId = 'recommended_communities'; + const { accessibilityId, themeStyles } = useAmityComponent({ + pageId, + componentId, + }); + + const { goToCommunitiesByCategoryPage, goToCommunityProfilePage } = useNavigation(); + + const { + recommendedCommunities, + isLoading, + fetchRecommendedCommunities, + refetchRecommendedCommunities, + } = useExplore(); + + useEffect(() => { + fetchRecommendedCommunities(); + }, []); + + const { joinCommunity, leaveCommunity } = useCommunityActions({ + onJoinSuccess: () => { + refetchRecommendedCommunities(); + }, + }); + + const handleJoinButtonClick = (communityId: string) => joinCommunity(communityId); + + const handleLeaveButtonClick = (communityId: string) => leaveCommunity(communityId); + + if (isLoading) { + return ( + <div className={styles.recommendedCommunities}> + {Array.from({ length: 4 }).map((_, index) => ( + <RecommendedCommunityCardSkeleton key={index} /> + ))} + </div> + ); + } + + if (recommendedCommunities.length === 0) { + return null; + } + + return ( + <div + style={themeStyles} + data-qa-anchor={accessibilityId} + className={styles.recommendedCommunities} + > + {recommendedCommunities.map((community) => ( + <RecommendedCommunityCard + key={community.communityId} + community={community} + pageId={pageId} + componentId={componentId} + onClick={(communityId) => goToCommunityProfilePage(communityId)} + onCategoryClick={(categoryId) => goToCommunitiesByCategoryPage({ categoryId })} + onJoinButtonClick={handleJoinButtonClick} + onLeaveButtonClick={handleLeaveButtonClick} + /> + ))} + </div> + ); +}; diff --git a/src/v4/social/components/RecommendedCommunities/RecommendedCommunityCardSkeleton.module.css b/src/v4/social/components/RecommendedCommunities/RecommendedCommunityCardSkeleton.module.css new file mode 100644 index 000000000..f46e6cee4 --- /dev/null +++ b/src/v4/social/components/RecommendedCommunities/RecommendedCommunityCardSkeleton.module.css @@ -0,0 +1,63 @@ +.recommendedCommunityCardSkeleton { + width: 17.375rem; + height: 13.3125rem; + border-radius: 0.75rem; + border: 1px solid var(--asc-color-base-shade4); + box-shadow: var(--asc-box-shadow-02); +} + +.recommendedCommunityCardSkeleton__image { + width: 17.375rem; + height: 8.1875rem; + border-radius: 0.75rem 0.75rem 0 0; + background-color: var(--asc-color-base-shade4); +} + +.recommendedCommunityCardSkeleton__content { + width: 17.375rem; + height: 5.12rem; + display: flex; + flex-direction: column; + padding: 1.06rem 1rem; +} + +.recommendedCommunityCardSkeleton__contentBar1 { + border-radius: 0.75rem; + background-color: var(--asc-color-base-shade4); + width: 5.1875rem; + height: 0.5rem; +} + +.recommendedCommunityCardSkeleton__contentBar2 { + margin-top: 1rem; + border-radius: 0.75rem; + background-color: var(--asc-color-base-shade4); + width: 8.9055rem; + height: 0.5rem; +} + +.recommendedCommunityCardSkeleton__contentBar3 { + margin-top: 0.5rem; + border-radius: 0.75rem; + background-color: var(--asc-color-base-shade4); + width: 11.25rem; + height: 0.5rem; +} + +.recommendedCommunityCardSkeleton__animation { + animation: skeleton-pulse 1.5s ease-in-out infinite; +} + +@keyframes skeleton-pulse { + 0% { + opacity: 0.6; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0.6; + } +} diff --git a/src/v4/social/components/RecommendedCommunities/RecommendedCommunityCardSkeleton.tsx b/src/v4/social/components/RecommendedCommunities/RecommendedCommunityCardSkeleton.tsx new file mode 100644 index 000000000..54a52e0a1 --- /dev/null +++ b/src/v4/social/components/RecommendedCommunities/RecommendedCommunityCardSkeleton.tsx @@ -0,0 +1,40 @@ +import clsx from 'clsx'; +import React from 'react'; + +import styles from './RecommendedCommunityCardSkeleton.module.css'; + +export const RecommendedCommunityCardSkeleton = () => ( + <div className={styles.recommendedCommunityCardSkeleton}> + <div + className={clsx( + styles.recommendedCommunityCardSkeleton__image, + styles.recommendedCommunityCardSkeleton__animation, + )} + /> + <div + className={clsx( + styles.recommendedCommunityCardSkeleton__content, + styles.recommendedCommunityCardSkeleton__animation, + )} + > + <div + className={clsx( + styles.recommendedCommunityCardSkeleton__contentBar1, + styles.recommendedCommunityCardSkeleton__animation, + )} + /> + <div + className={clsx( + styles.recommendedCommunityCardSkeleton__contentBar2, + styles.recommendedCommunityCardSkeleton__animation, + )} + /> + <div + className={clsx( + styles.recommendedCommunityCardSkeleton__contentBar3, + styles.recommendedCommunityCardSkeleton__animation, + )} + /> + </div> + </div> +); diff --git a/src/v4/social/components/RecommendedCommunities/index.tsx b/src/v4/social/components/RecommendedCommunities/index.tsx new file mode 100644 index 000000000..5cd2e5a96 --- /dev/null +++ b/src/v4/social/components/RecommendedCommunities/index.tsx @@ -0,0 +1 @@ +export { RecommendedCommunities } from './RecommendedCommunities'; diff --git a/src/v4/social/components/ReplyComment/ReplyComment.tsx b/src/v4/social/components/ReplyComment/ReplyComment.tsx index 4bfa69c71..1e3c850c2 100644 --- a/src/v4/social/components/ReplyComment/ReplyComment.tsx +++ b/src/v4/social/components/ReplyComment/ReplyComment.tsx @@ -109,11 +109,13 @@ const PostReplyComment = ({ pageId = '*', community, comment }: ReplyCommentProp </div> ) : isEditing ? ( <div className={styles.postReplyComment__edit}> - <UserAvatar userId={comment.userId} /> + <UserAvatar pageId={pageId} componentId={componentId} userId={comment.userId} /> <div className={styles.postReplyComment__edit__inputWrap}> <div className={styles.postReplyComment__edit__input}> <CommentInput - community={community} + pageId={pageId} + componentId={componentId} + communityId={community?.communityId} value={{ data: { text: (comment.data as Amity.ContentDataText).text, @@ -121,8 +123,16 @@ const PostReplyComment = ({ pageId = '*', community, comment }: ReplyCommentProp mentionees: comment.mentionees as Mentionees, metadata: comment.metadata || {}, }} - onChange={(value: CreateCommentParams) => { - setCommentData(value); + onChange={(value) => { + setCommentData({ + data: { + text: value.text, + }, + mentionees: value.mentionees as Amity.UserMention[], + metadata: { + mentioned: value.mentioned, + }, + }); }} /> </div> @@ -154,7 +164,7 @@ const PostReplyComment = ({ pageId = '*', community, comment }: ReplyCommentProp style={themeStyles} data-qa-anchor={accessibilityId} > - <UserAvatar userId={comment.userId} /> + <UserAvatar pageId={pageId} componentId={componentId} userId={comment.userId} /> <div className={styles.postReplyComment__details}> <div className={styles.postReplyComment__content}> <Typography.BodyBold className={styles.postReplyComment__content__username}> @@ -162,6 +172,8 @@ const PostReplyComment = ({ pageId = '*', community, comment }: ReplyCommentProp </Typography.BodyBold> {isModeratorUser && <ModeratorBadge pageId={pageId} componentId={componentId} />} <TextWithMention + pageId={pageId} + componentId={componentId} data={{ text: (comment.data as Amity.ContentDataText).text }} mentionees={comment.mentionees as Amity.UserMention[]} metadata={comment.metadata} @@ -179,6 +191,7 @@ const PostReplyComment = ({ pageId = '*', community, comment }: ReplyCommentProp </Typography.Caption> <div onClick={handleLike}> <Typography.CaptionBold + data-qa-anchor={`${pageId}/${componentId}/reply_comment_like`} className={styles.postReplyComment__secondRow__like} data-is-liked={isLiked} > @@ -186,6 +199,7 @@ const PostReplyComment = ({ pageId = '*', community, comment }: ReplyCommentProp </Typography.CaptionBold> </div> <EllipsisH + data-qa-anchor={`${pageId}/${componentId}/reply_comment_options`} className={styles.postReplyComment__secondRow__actionButton} onClick={() => setBottomSheetOpen(true)} /> @@ -207,6 +221,8 @@ const PostReplyComment = ({ pageId = '*', community, comment }: ReplyCommentProp detent="content-height" > <CommentOptions + pageId={pageId} + componentId={componentId} comment={comment} onCloseBottomSheet={toggleBottomSheet} handleEditComment={handleEditComment} diff --git a/src/v4/social/components/ReplyCommentList/ReplyCommentList.tsx b/src/v4/social/components/ReplyCommentList/ReplyCommentList.tsx index 1ec452b73..7b4b6c4e5 100644 --- a/src/v4/social/components/ReplyCommentList/ReplyCommentList.tsx +++ b/src/v4/social/components/ReplyCommentList/ReplyCommentList.tsx @@ -7,6 +7,8 @@ import ReplyComment from '~/v4/social/components/ReplyComment/ReplyComment'; import styles from './ReplyCommentList.module.css'; interface ReplyCommentProps { + pageId?: string; + componentId?: string; community?: Amity.Community; referenceId: string; referenceType: string; @@ -14,6 +16,8 @@ interface ReplyCommentProps { } export const ReplyCommentList = ({ + pageId = '*', + componentId = '*', referenceId, referenceType, community, @@ -36,7 +40,9 @@ export const ReplyCommentList = ({ <div> {isLoading && <CommentSkeleton numberOfSkeletons={3} />} {comments.map((comment) => { - return <ReplyComment community={community} comment={comment as Amity.Comment} />; + return ( + <ReplyComment pageId={pageId} community={community} comment={comment as Amity.Comment} /> + ); })} {hasMore && ( <div diff --git a/src/v4/social/components/StoryTab/StoryTabCommunity.tsx b/src/v4/social/components/StoryTab/StoryTabCommunity.tsx index bcde21eeb..2ed839e16 100644 --- a/src/v4/social/components/StoryTab/StoryTabCommunity.tsx +++ b/src/v4/social/components/StoryTab/StoryTabCommunity.tsx @@ -115,7 +115,12 @@ export const StoryTabCommunityFeed: React.FC<StoryTabCommunityFeedProps> = ({ {isErrored && <ErrorIcon className={clsx(styles.errorIcon)} />} </div> <Truncate lines={1}> - <div className={clsx(styles.storyTitle)}>Story</div> + <div + data-qa-anchore={`${pageId}/${componentId}/story_title`} + className={clsx(styles.storyTitle)} + > + Story + </div> </Truncate> </div> ); diff --git a/src/v4/social/components/StoryTab/StoryTabItem.tsx b/src/v4/social/components/StoryTab/StoryTabItem.tsx index cb2ec12c6..6d134d8d2 100644 --- a/src/v4/social/components/StoryTab/StoryTabItem.tsx +++ b/src/v4/social/components/StoryTab/StoryTabItem.tsx @@ -86,7 +86,10 @@ export const StoryTabItem: React.FC<StoryTabProps> = ({ isErrored={isErrored} /> - <div className={styles.avatarBackground}> + <div + data-qa-anchor={`${pageId}/${componentId}/community_avatar`} + className={styles.avatarBackground} + > {communityAvatar && ( <img className={styles.avatar} src={communityAvatar} alt={community?.displayName} /> )} @@ -95,7 +98,10 @@ export const StoryTabItem: React.FC<StoryTabProps> = ({ {community?.isOfficial && !isErrored && <Verified className={styles.verifiedIcon} />} </div> - <Typography.Caption className={clsx(styles.displayName)}> + <Typography.Caption + data-qa-anchor={`${pageId}/${componentId}/community_name`} + className={clsx(styles.displayName)} + > {!community?.isPublic && <LockIcon />} {community?.displayName} </Typography.Caption> diff --git a/src/v4/social/components/TopNavigation/TopNavigation.tsx b/src/v4/social/components/TopNavigation/TopNavigation.tsx index bfbaa5609..6d8e4d6a2 100644 --- a/src/v4/social/components/TopNavigation/TopNavigation.tsx +++ b/src/v4/social/components/TopNavigation/TopNavigation.tsx @@ -28,9 +28,8 @@ export function TopNavigation({ const handleGlobalSearchClick = () => { switch (selectedTab) { case HomePageTab.Newsfeed: - goToSocialGlobalSearchPage(); - break; case HomePageTab.Explore: + goToSocialGlobalSearchPage(); break; case HomePageTab.MyCommunities: goToMyCommunitiesSearchPage(); diff --git a/src/v4/social/components/TopSearchBar/TopSearchBar.module.css b/src/v4/social/components/TopSearchBar/TopSearchBar.module.css index d08fcbe81..ee0919c89 100644 --- a/src/v4/social/components/TopSearchBar/TopSearchBar.module.css +++ b/src/v4/social/components/TopSearchBar/TopSearchBar.module.css @@ -20,6 +20,7 @@ border: none; background-color: var(--asc-color-base-shade4); color: var(--asc-color-base-shade2); + outline: none; } .topSearchBar__searchIcon { diff --git a/src/v4/social/components/TopSearchBar/TopSearchBar.tsx b/src/v4/social/components/TopSearchBar/TopSearchBar.tsx index 1610b0c5d..e40efe6c9 100644 --- a/src/v4/social/components/TopSearchBar/TopSearchBar.tsx +++ b/src/v4/social/components/TopSearchBar/TopSearchBar.tsx @@ -6,6 +6,7 @@ import { SearchIcon } from '~/v4/social/elements/SearchIcon'; import styles from './TopSearchBar.module.css'; import { useAmityComponent } from '~/v4/core/hooks/uikit'; import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { Input } from 'react-aria-components'; export interface TopSearchBarProps { pageId?: string; @@ -38,7 +39,7 @@ export function TopSearchBar({ pageId = '*', search }: TopSearchBarProps) { defaultClassName={styles.topSearchBar__searchIcon} imgClassName={styles.topSearchBar__searchIcon_img} /> - <input + <Input className={styles.topSearchBar__textInput} type="text" value={searchValue} diff --git a/src/v4/social/components/TrendingCommunities/TrendingCommunities.module.css b/src/v4/social/components/TrendingCommunities/TrendingCommunities.module.css new file mode 100644 index 000000000..39340617b --- /dev/null +++ b/src/v4/social/components/TrendingCommunities/TrendingCommunities.module.css @@ -0,0 +1,6 @@ +.trendingCommunities { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/src/v4/social/components/TrendingCommunities/TrendingCommunities.stories.tsx b/src/v4/social/components/TrendingCommunities/TrendingCommunities.stories.tsx new file mode 100644 index 000000000..a7008b1b0 --- /dev/null +++ b/src/v4/social/components/TrendingCommunities/TrendingCommunities.stories.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { TrendingCommunities } from './TrendingCommunities'; + +export default { + title: 'v4-social/components/TrendingCommunities', +}; + +export const RecommendedCommunitiesStory = { + render: () => { + return <TrendingCommunities />; + }, + + name: 'TrendingCommunities', +}; diff --git a/src/v4/social/components/TrendingCommunities/TrendingCommunities.tsx b/src/v4/social/components/TrendingCommunities/TrendingCommunities.tsx new file mode 100644 index 000000000..15492b86a --- /dev/null +++ b/src/v4/social/components/TrendingCommunities/TrendingCommunities.tsx @@ -0,0 +1,84 @@ +import React, { useEffect } from 'react'; + +import styles from './TrendingCommunities.module.css'; +import { useAmityComponent } from '~/v4/core/hooks/uikit'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { CommunityRowItem } from '~/v4/social/internal-components/CommunityRowItem'; +import { CommunityRowItemSkeleton } from '~/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton'; +import { useExplore } from '~/v4/social/providers/ExploreProvider'; +import { CommunityRowItemDivider } from '~/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider'; +import { useCommunityActions } from '~/v4/social/hooks/useCommunityActions'; + +interface TrendingCommunitiesProps { + pageId?: string; +} + +export const TrendingCommunities = ({ pageId = '*' }: TrendingCommunitiesProps) => { + const componentId = 'trending_communities'; + const { accessibilityId, themeStyles } = useAmityComponent({ + pageId, + componentId, + }); + + const { trendingCommunities, isLoading, fetchTrendingCommunities } = useExplore(); + + useEffect(() => { + fetchTrendingCommunities(); + }, []); + + const { goToCommunitiesByCategoryPage, goToCommunityProfilePage } = useNavigation(); + + const { joinCommunity, leaveCommunity } = useCommunityActions(); + + const handleJoinButtonClick = (communityId: string) => joinCommunity(communityId); + const handleLeaveButtonClick = (communityId: string) => leaveCommunity(communityId); + + if (isLoading) { + return ( + <div + style={themeStyles} + data-qa-anchor={accessibilityId} + className={styles.trendingCommunities} + > + {Array.from({ length: 2 }).map((_, index) => ( + <React.Fragment key={index}> + <CommunityRowItemDivider /> + <CommunityRowItemSkeleton /> + </React.Fragment> + ))} + </div> + ); + } + + if (trendingCommunities.length === 0) { + return null; + } + + return ( + <div + style={themeStyles} + data-qa-anchor={accessibilityId} + className={styles.trendingCommunities} + > + {trendingCommunities.map((community, index) => ( + <React.Fragment key={community.communityId}> + <CommunityRowItemDivider /> + <CommunityRowItem + community={community} + pageId={pageId} + componentId={componentId} + order={index + 1} + onClick={(communityId) => goToCommunityProfilePage(communityId)} + onCategoryClick={(categoryId) => goToCommunitiesByCategoryPage({ categoryId })} + onJoinButtonClick={handleJoinButtonClick} + onLeaveButtonClick={handleLeaveButtonClick} + showJoinButton + minCategoryCharacters={3} + maxCategoryCharacters={36} + maxCategoriesLength={2} + /> + </React.Fragment> + ))} + </div> + ); +}; diff --git a/src/v4/social/components/TrendingCommunities/index.tsx b/src/v4/social/components/TrendingCommunities/index.tsx new file mode 100644 index 000000000..e641ad6dd --- /dev/null +++ b/src/v4/social/components/TrendingCommunities/index.tsx @@ -0,0 +1 @@ +export { TrendingCommunities } from './TrendingCommunities'; diff --git a/src/v4/social/components/UserSearchResult/UserSearchItem.module.css b/src/v4/social/components/UserSearchResult/UserSearchItem.module.css index 7b55aa592..9e25df17c 100644 --- a/src/v4/social/components/UserSearchResult/UserSearchItem.module.css +++ b/src/v4/social/components/UserSearchResult/UserSearchItem.module.css @@ -18,6 +18,7 @@ justify-content: center; align-items: center; width: 100%; + gap: 0.25rem; } .userItem__avatar { @@ -25,10 +26,20 @@ height: 2.5rem; } +.userItem__brandIcon__container { + width: 1.125rem; + height: 1.125rem; +} + +.userItem__brandIcon { + width: 1.125rem; + height: 1.125rem; +} + .userItem__userName { display: flex; justify-content: start; - align-items: last baseline; + align-items: last baseline baseline; gap: 0.25rem; width: 100%; color: var(--asc-color-base-default); diff --git a/src/v4/social/components/UserSearchResult/UserSearchItem.tsx b/src/v4/social/components/UserSearchResult/UserSearchItem.tsx index a67d5c0ea..788d0a94b 100644 --- a/src/v4/social/components/UserSearchResult/UserSearchItem.tsx +++ b/src/v4/social/components/UserSearchResult/UserSearchItem.tsx @@ -4,25 +4,44 @@ import { Typography } from '~/v4/core/components'; import styles from './UserSearchItem.module.css'; import { useNavigation } from '~/v4/core/providers/NavigationProvider'; import { Button } from '~/v4/core/natives/Button'; +import { BrandBadge } from '~/v4/social/internal-components/BrandBadge'; interface UserSearchItemProps { + pageId?: string; + componentId?: string; user: Amity.User; onClick?: () => void; } -export const UserSearchItem = ({ user }: UserSearchItemProps) => { +export const UserSearchItem = ({ pageId = '*', componentId = '*', user }: UserSearchItemProps) => { const { onClickUser } = useNavigation(); return ( <Button key={user.userId} className={styles.userItem} onPress={() => onClickUser(user.userId)}> - <div className={styles.userItem__leftPane}> - <UserAvatar userId={user.userId} className={styles.userItem__avatar} /> + <div + data-qa-anchor={`${pageId}/${componentId}/search_user_avatar`} + className={styles.userItem__leftPane} + > + <UserAvatar + pageId={pageId} + componentId={componentId} + userId={user.userId} + className={styles.userItem__avatar} + /> </div> <div className={styles.userItem__rightPane}> <div className={styles.userItem__userName}> - <Typography.BodyBold className={styles.userItem__userName__text}> + <Typography.BodyBold + data-qa-anchor={`${pageId}/${componentId}/search_username`} + className={styles.userItem__userName__text} + > {user.displayName} </Typography.BodyBold> + {user.isBrand ? ( + <div className={styles.userItem__brandIcon__container}> + <BrandBadge className={styles.userItem__brandIcon} /> + </div> + ) : null} </div> </div> </Button> diff --git a/src/v4/social/components/UserSearchResult/UserSearchResult.tsx b/src/v4/social/components/UserSearchResult/UserSearchResult.tsx index f419ccb6e..d18ded607 100644 --- a/src/v4/social/components/UserSearchResult/UserSearchResult.tsx +++ b/src/v4/social/components/UserSearchResult/UserSearchResult.tsx @@ -20,20 +20,19 @@ export const UserSearchResult = ({ onLoadMore, }: UserSearchResultProps) => { const componentId = 'user_search_result'; - const { accessibilityId, config, defaultConfig, isExcluded, uiReference, themeStyles } = - useAmityComponent({ - pageId, - componentId, - }); + const { accessibilityId, themeStyles } = useAmityComponent({ + pageId, + componentId, + }); const [intersectionNode, setIntersectionNode] = useState<HTMLDivElement | null>(null); useIntersectionObserver({ onIntersect: () => onLoadMore(), node: intersectionNode }); return ( - <div className={styles.userSearchResult} style={themeStyles}> + <div className={styles.userSearchResult} style={themeStyles} data-qa-anchor={accessibilityId}> {userCollection.map((user) => ( - <UserSearchItem key={user.userId} user={user} /> + <UserSearchItem pageId={pageId} componentId={componentId} key={user.userId} user={user} /> ))} {isLoading ? Array.from({ length: 5 }).map((_, index) => ( diff --git a/src/v4/social/elements/AllCategoriesTitle/AllCategoriesTitle.module.css b/src/v4/social/elements/AllCategoriesTitle/AllCategoriesTitle.module.css new file mode 100644 index 000000000..7b5b9e6a2 --- /dev/null +++ b/src/v4/social/elements/AllCategoriesTitle/AllCategoriesTitle.module.css @@ -0,0 +1,8 @@ +.communityName__truncate { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; +} diff --git a/src/v4/social/elements/AllCategoriesTitle/AllCategoriesTitle.tsx b/src/v4/social/elements/AllCategoriesTitle/AllCategoriesTitle.tsx new file mode 100644 index 000000000..fec74b7b7 --- /dev/null +++ b/src/v4/social/elements/AllCategoriesTitle/AllCategoriesTitle.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import styles from './AllCategoriesTitle.module.css'; +import { Typography } from '~/v4/core/components'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; + +interface AllCategoriesTitleProps { + pageId?: string; + componentId?: string; +} + +export const AllCategoriesTitle = ({ + pageId = '*', + componentId = '*', +}: AllCategoriesTitleProps) => { + const elementId = 'all_categories_title'; + const { config, themeStyles, accessibilityId, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + <Typography.Title + data-qa-anchor={accessibilityId} + className={styles.communityName__truncate} + style={themeStyles} + > + All Categories + </Typography.Title> + ); +}; diff --git a/src/v4/social/elements/AllCategoriesTitle/index.ts b/src/v4/social/elements/AllCategoriesTitle/index.ts new file mode 100644 index 000000000..13c618184 --- /dev/null +++ b/src/v4/social/elements/AllCategoriesTitle/index.ts @@ -0,0 +1 @@ +export { AllCategoriesTitle } from './AllCategoriesTitle'; diff --git a/src/v4/social/elements/CategoryChip/CategoryChip.module.css b/src/v4/social/elements/CategoryChip/CategoryChip.module.css new file mode 100644 index 000000000..0ac244b2d --- /dev/null +++ b/src/v4/social/elements/CategoryChip/CategoryChip.module.css @@ -0,0 +1,29 @@ +.categoryChip { + display: flex; + gap: 0.5rem; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + padding: 0.25rem 0.75rem 0.25rem 0.25rem; + border-radius: 1.125rem; + border: 1px solid var(--asc-color-base-shade4); + background-color: var(--asc-color-background-default); +} + +.categoryChip:hover { + background-color: var(--asc-color-background-shade1); +} + +.categoryChip__image { + width: 1.75rem; + height: 1.75rem; + border-radius: 50%; + overflow: hidden; +} + +.categoryChip__text { + color: var(--asc-color-base-default); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/v4/social/elements/CategoryChip/CategoryChip.tsx b/src/v4/social/elements/CategoryChip/CategoryChip.tsx new file mode 100644 index 000000000..b55011291 --- /dev/null +++ b/src/v4/social/elements/CategoryChip/CategoryChip.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import styles from './CategoryChip.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { Button } from '~/v4/core/natives/Button'; +import { Typography } from '~/v4/core/components'; +import { useImage } from '~/v4/core/hooks/useImage'; +import clsx from 'clsx'; +import { CategoryImage } from '~/v4/social/internal-components/CategoryImage/CategoryImage'; + +interface CategoryChipProps { + pageId: string; + componentId?: string; + category: Amity.Category; + onClick?: (categoryId: string) => void; +} + +export const CategoryChip: React.FC<CategoryChipProps> = ({ + pageId = '*', + componentId = '*', + category, + onClick, +}) => { + const elementId = 'category_chip'; + + const { isExcluded, accessibilityId, themeStyles } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + const categoryImage = useImage({ fileId: category.avatar?.fileId }); + + if (isExcluded) return null; + + return ( + <Button + data-qa-anchor={accessibilityId} + style={themeStyles} + className={styles.categoryChip} + onPress={() => onClick?.(category.categoryId)} + > + <CategoryImage + imgSrc={categoryImage} + className={styles.categoryChip__image} + pageId={pageId} + componentId={componentId} + elementId={elementId} + /> + <Typography.BodyBold + renderer={({ typoClassName }) => ( + <span className={clsx(typoClassName, styles.categoryChip__text)}>{category.name}</span> + )} + /> + </Button> + ); +}; diff --git a/src/v4/social/elements/CategoryChip/CategoryChipSkeleton.module.css b/src/v4/social/elements/CategoryChip/CategoryChipSkeleton.module.css new file mode 100644 index 000000000..20d55276d --- /dev/null +++ b/src/v4/social/elements/CategoryChip/CategoryChipSkeleton.module.css @@ -0,0 +1,22 @@ +.exploreCategories__categoryChipSkeleton { + width: 5.625rem; + height: 2.25rem; + flex-shrink: 0; + border-radius: 2.5rem; + background: var(--asc-color-base-shade4); + animation: skeleton-pulse 1.5s ease-in-out infinite; +} + +@keyframes skeleton-pulse { + 0% { + opacity: 0.6; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0.6; + } +} diff --git a/src/v4/social/elements/CategoryChip/CategoryChipSkeleton.tsx b/src/v4/social/elements/CategoryChip/CategoryChipSkeleton.tsx new file mode 100644 index 000000000..4a2a053dc --- /dev/null +++ b/src/v4/social/elements/CategoryChip/CategoryChipSkeleton.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import styles from './CategoryChipSkeleton.module.css'; + +export const CategoryChipSkeleton = () => ( + <div className={styles.exploreCategories__categoryChipSkeleton} /> +); diff --git a/src/v4/social/elements/CategoryChip/index.ts b/src/v4/social/elements/CategoryChip/index.ts new file mode 100644 index 000000000..14717373b --- /dev/null +++ b/src/v4/social/elements/CategoryChip/index.ts @@ -0,0 +1 @@ +export { CategoryChip } from './CategoryChip'; diff --git a/src/v4/social/elements/CategoryTitle/CategoryTitle.module.css b/src/v4/social/elements/CategoryTitle/CategoryTitle.module.css new file mode 100644 index 000000000..1f5c12a0c --- /dev/null +++ b/src/v4/social/elements/CategoryTitle/CategoryTitle.module.css @@ -0,0 +1,5 @@ +.categoryTitle { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/v4/social/elements/CategoryTitle/CategoryTitle.tsx b/src/v4/social/elements/CategoryTitle/CategoryTitle.tsx new file mode 100644 index 000000000..451f7e76e --- /dev/null +++ b/src/v4/social/elements/CategoryTitle/CategoryTitle.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import styles from './CategoryTitle.module.css'; +import { Typography } from '~/v4/core/components'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; + +interface CategoryTitleProps { + pageId?: string; + componentId?: string; + categoryName: string; +} + +export const CategoryTitle = ({ + pageId = '*', + componentId = '*', + categoryName, +}: CategoryTitleProps) => { + const elementId = 'all_categories_title'; + const { config, themeStyles, accessibilityId, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + <Typography.Title + data-qa-anchor={accessibilityId} + className={styles.categoryTitle} + style={themeStyles} + > + {categoryName} + </Typography.Title> + ); +}; diff --git a/src/v4/social/elements/CategoryTitle/index.ts b/src/v4/social/elements/CategoryTitle/index.ts new file mode 100644 index 000000000..6ba158372 --- /dev/null +++ b/src/v4/social/elements/CategoryTitle/index.ts @@ -0,0 +1 @@ +export { CategoryTitle } from './CategoryTitle'; diff --git a/src/v4/social/elements/CommentButton/CommentButton.tsx b/src/v4/social/elements/CommentButton/CommentButton.tsx index 3a7e83c15..33ec24920 100644 --- a/src/v4/social/elements/CommentButton/CommentButton.tsx +++ b/src/v4/social/elements/CommentButton/CommentButton.tsx @@ -53,7 +53,10 @@ export function CommentButton({ defaultIcon={() => ( <div className={clsx(styles.commentButton)}> <CommentSvg className={clsx(styles.commentButton__icon, defaultIconClassName)} /> - <Typography.BodyBold className={styles.commentButton__text}> + <Typography.BodyBold + data-qa-anchor={`${pageId}/${componentId}/comment_count`} + className={styles.commentButton__text} + > {typeof commentsCount === 'number' ? commentsCount : config.text} </Typography.BodyBold> </div> diff --git a/src/v4/social/elements/CommunityCardImage/CommunityCardImage.module.css b/src/v4/social/elements/CommunityCardImage/CommunityCardImage.module.css new file mode 100644 index 000000000..76c243d35 --- /dev/null +++ b/src/v4/social/elements/CommunityCardImage/CommunityCardImage.module.css @@ -0,0 +1,20 @@ +.communityCardImage { + width: 100%; + height: 100%; + overflow: hidden; + object-fit: cover; +} + +.communityCardImage__placeholderImage { + display: flex; + justify-content: center; + align-items: center; + background-color: var(--asc-color-base-shade3); + width: 100%; + height: 100%; +} + +.communityCardImage__placeholderImageIcon { + width: 4rem; + height: 2.364rem; +} diff --git a/src/v4/social/elements/CommunityCardImage/CommunityCardImage.tsx b/src/v4/social/elements/CommunityCardImage/CommunityCardImage.tsx new file mode 100644 index 000000000..a84f7e8ab --- /dev/null +++ b/src/v4/social/elements/CommunityCardImage/CommunityCardImage.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import styles from './CommunityCardImage.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { Img } from '~/v4/core/natives/Img/Img'; +import { People } from '~/v4/icons/People'; +import clsx from 'clsx'; + +const PlaceholderImage = ({ className }: { className?: string }) => { + return ( + <div className={clsx(styles.communityCardImage__placeholderImage, className)}> + <People className={styles.communityCardImage__placeholderImageIcon} /> + </div> + ); +}; + +interface CommunityCardImageProps { + pageId?: string; + componentId?: string; + imgSrc?: string; + className?: string; +} + +export const CommunityCardImage: React.FC<CommunityCardImageProps> = ({ + pageId = '*', + componentId = '*', + imgSrc, + className, +}) => { + const elementId = 'community_card_image'; + + const { themeStyles, accessibilityId } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + return ( + <Img + data-qa-anchor={accessibilityId} + style={themeStyles} + className={clsx(styles.communityCardImage, className)} + src={imgSrc} + fallBackRenderer={() => <PlaceholderImage className={className} />} + /> + ); +}; diff --git a/src/v4/social/elements/CommunityCardImage/index.ts b/src/v4/social/elements/CommunityCardImage/index.ts new file mode 100644 index 000000000..36eea75fd --- /dev/null +++ b/src/v4/social/elements/CommunityCardImage/index.ts @@ -0,0 +1 @@ +export { CommunityCardImage } from './CommunityCardImage'; diff --git a/src/v4/social/elements/CommunityCategory/CommunityCategory.module.css b/src/v4/social/elements/CommunityCategory/CommunityCategory.module.css index fee67d974..362aeff10 100644 --- a/src/v4/social/elements/CommunityCategory/CommunityCategory.module.css +++ b/src/v4/social/elements/CommunityCategory/CommunityCategory.module.css @@ -1,11 +1,16 @@ -.community__category { - display: flex; - align-items: center; - gap: 0.5rem; - overflow-x: scroll; - width: 100%; +.communityCategory { + padding: 0.12rem 0.5rem; + border-radius: 1.25rem; + background-color: var(--asc-color-base-shade4); + color: var(--asc-color-base-default); + line-height: 1.125rem; + font-size: 0.8125rem; + white-space: nowrap; } -.community__category::-webkit-scrollbar { - display: none; +.communityCategory[data-truncated='true'] { + text-overflow: ellipsis; + overflow: hidden; + min-width: var(--asc-community-category-min-characters, unset); + max-width: var(--asc-community-category-max-characters, unset); } diff --git a/src/v4/social/elements/CommunityCategory/CommunityCategory.tsx b/src/v4/social/elements/CommunityCategory/CommunityCategory.tsx index debf081ec..937593169 100644 --- a/src/v4/social/elements/CommunityCategory/CommunityCategory.tsx +++ b/src/v4/social/elements/CommunityCategory/CommunityCategory.tsx @@ -2,17 +2,29 @@ import React from 'react'; import styles from './CommunityCategory.module.css'; import { useAmityElement } from '~/v4/core/hooks/uikit'; import { CommunityCategoryName } from '~/v4/social/elements/CommunityCategoryName'; +import { Button } from '~/v4/core/natives/Button/Button'; +import clsx from 'clsx'; interface CommunityCategoryProps { - categories: Amity.Category[]; pageId?: string; componentId?: string; + categoryName: string; + minCharacters?: number; + maxCharacters?: number; + truncate?: boolean; + className?: string; + onClick?: () => void; } export const CommunityCategory = ({ pageId = '*', componentId = '*', - categories, + categoryName, + minCharacters, + maxCharacters, + truncate = false, + className, + onClick, }: CommunityCategoryProps) => { const elementId = 'community_category'; const { config, themeStyles, accessibilityId, isExcluded } = useAmityElement({ @@ -23,15 +35,28 @@ export const CommunityCategory = ({ if (isExcluded) return null; + const categoryNameLength = categoryName.length; + return ( - <div + <Button + style={ + { + ...themeStyles, + '--asc-community-category-min-characters': + minCharacters && categoryNameLength > minCharacters + ? `${Math.min(minCharacters, categoryName.length)}ch` + : undefined, + '--asc-community-category-max-characters': maxCharacters + ? `${maxCharacters}ch` + : undefined, + } as React.CSSProperties + } data-qa-anchor={accessibilityId} - style={themeStyles} - className={styles.community__category} + data-truncated={categoryNameLength > (minCharacters ?? 0) ? truncate : false} + className={clsx(styles.communityCategory, className)} + onPress={() => onClick?.()} > - {categories.map((category) => ( - <CommunityCategoryName categoryName={category.name} /> - ))} - </div> + {categoryName} + </Button> ); }; diff --git a/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.module.css b/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.module.css index 1986addeb..fdaead2b7 100644 --- a/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.module.css +++ b/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.module.css @@ -1,12 +1,12 @@ .communityCategoryName { - display: flex; + width: 100%; padding: 0.12rem 0.5rem; - justify-content: center; - align-items: center; border-radius: 1.25rem; background-color: var(--asc-color-base-shade4); color: var(--asc-color-base-default); line-height: 1.125rem; font-size: 0.8125rem; white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } diff --git a/src/v4/social/elements/CommunityEmptyImage/CommunityEmptyImage.tsx b/src/v4/social/elements/CommunityEmptyImage/CommunityEmptyImage.tsx new file mode 100644 index 000000000..0b0ad497c --- /dev/null +++ b/src/v4/social/elements/CommunityEmptyImage/CommunityEmptyImage.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { IconComponent } from '~/v4/core/IconComponent'; + +interface CommunityEmptyImageProps { + pageId?: string; + componentId?: string; +} + +export const CommunityEmptyImage = ({ + pageId = '*', + componentId = '*', +}: CommunityEmptyImageProps) => { + const elementId = 'community_empty_image'; + + const { config, accessibilityId, isExcluded, uiReference, defaultConfig } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + <IconComponent + data-qa-anchor={accessibilityId} + defaultIcon={() => ( + <svg + width="55" + height="45" + viewBox="0 0 55 45" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M8.9375 0.3125C9.78125 0.3125 10.625 1.15625 10.625 2V8.75C10.625 9.69922 9.78125 10.4375 8.9375 10.4375H2.1875C1.23828 10.4375 0.5 9.69922 0.5 8.75V2C0.5 1.15625 1.23828 0.3125 2.1875 0.3125H8.9375ZM8.9375 17.1875C9.78125 17.1875 10.625 18.0312 10.625 18.875V25.625C10.625 26.5742 9.78125 27.3125 8.9375 27.3125H2.1875C1.23828 27.3125 0.5 26.5742 0.5 25.625V18.875C0.5 18.0312 1.23828 17.1875 2.1875 17.1875H8.9375ZM8.9375 34.0625C9.78125 34.0625 10.625 34.9062 10.625 35.75V42.5C10.625 43.4492 9.78125 44.1875 8.9375 44.1875H2.1875C1.23828 44.1875 0.5 43.4492 0.5 42.5V35.75C0.5 34.9062 1.23828 34.0625 2.1875 34.0625H8.9375ZM52.8125 19.7188C53.6562 19.7188 54.5 20.5625 54.5 21.4062V23.0938C54.5 24.043 53.6562 24.7812 52.8125 24.7812H19.0625C18.1133 24.7812 17.375 24.043 17.375 23.0938V21.4062C17.375 20.5625 18.1133 19.7188 19.0625 19.7188H52.8125ZM52.8125 36.5938C53.6562 36.5938 54.5 37.4375 54.5 38.2812V39.9688C54.5 40.918 53.6562 41.6562 52.8125 41.6562H19.0625C18.1133 41.6562 17.375 40.918 17.375 39.9688V38.2812C17.375 37.4375 18.1133 36.5938 19.0625 36.5938H52.8125ZM52.8125 2.84375C53.6562 2.84375 54.5 3.6875 54.5 4.53125V6.21875C54.5 7.16797 53.6562 7.90625 52.8125 7.90625H19.0625C18.1133 7.90625 17.375 7.16797 17.375 6.21875V4.53125C17.375 3.6875 18.1133 2.84375 19.0625 2.84375H52.8125Z" + fill="#EBECEF" + /> + </svg> + )} + imgIcon={() => <img src={config.icon} alt={uiReference} />} + configIconName={config.icon} + defaultIconName={defaultConfig.icon} + /> + ); +}; diff --git a/src/v4/social/elements/CommunityEmptyImage/index.ts b/src/v4/social/elements/CommunityEmptyImage/index.ts new file mode 100644 index 000000000..02f0c424a --- /dev/null +++ b/src/v4/social/elements/CommunityEmptyImage/index.ts @@ -0,0 +1 @@ +export { CommunityEmptyImage } from './CommunityEmptyImage'; diff --git a/src/v4/social/elements/CommunityEmptyTitle/CommunityEmptyTitle.module.css b/src/v4/social/elements/CommunityEmptyTitle/CommunityEmptyTitle.module.css new file mode 100644 index 000000000..651aa5758 --- /dev/null +++ b/src/v4/social/elements/CommunityEmptyTitle/CommunityEmptyTitle.module.css @@ -0,0 +1,3 @@ +.communityEmptyTitle { + color: var(--asc-color-base-shade3); +} diff --git a/src/v4/social/elements/CommunityEmptyTitle/CommunityEmptyTitle.tsx b/src/v4/social/elements/CommunityEmptyTitle/CommunityEmptyTitle.tsx new file mode 100644 index 000000000..12635363c --- /dev/null +++ b/src/v4/social/elements/CommunityEmptyTitle/CommunityEmptyTitle.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Typography } from '~/v4/core/components/Typography'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import styles from './CommunityEmptyTitle.module.css'; + +interface CommunityEmptyTitleProps { + pageId?: string; + componentId?: string; +} + +export const CommunityEmptyTitle = ({ + pageId = '*', + componentId = '*', +}: CommunityEmptyTitleProps) => { + const elementId = 'community_empty_title'; + + const { themeStyles, config, accessibilityId, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) { + return null; + } + + return ( + <Typography.Title + style={themeStyles} + className={styles.communityEmptyTitle} + data-qa-anchor={accessibilityId} + > + {config.text} + </Typography.Title> + ); +}; diff --git a/src/v4/social/elements/CommunityEmptyTitle/index.ts b/src/v4/social/elements/CommunityEmptyTitle/index.ts new file mode 100644 index 000000000..e5d23c4b8 --- /dev/null +++ b/src/v4/social/elements/CommunityEmptyTitle/index.ts @@ -0,0 +1 @@ +export { CommunityEmptyTitle } from './CommunityEmptyTitle'; diff --git a/src/v4/social/elements/CommunityInfo/CommunityInfo.tsx b/src/v4/social/elements/CommunityInfo/CommunityInfo.tsx index 456bf11ee..3fafeca53 100644 --- a/src/v4/social/elements/CommunityInfo/CommunityInfo.tsx +++ b/src/v4/social/elements/CommunityInfo/CommunityInfo.tsx @@ -28,7 +28,11 @@ export const CommunityInfo = ({ }); if (isExcluded) return null; return ( - <Button onPress={onClick} className={styles.communityInfo__container}> + <Button + data-qa-anchor={accessibilityId} + onPress={onClick} + className={styles.communityInfo__container} + > <div className={styles.communityInfo__wrapper}> <Typography.BodyBold className={styles.communityInfo__count}> {millify(count)} diff --git a/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.module.css b/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.module.css index da25c1d29..278e86f08 100644 --- a/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.module.css +++ b/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.module.css @@ -10,7 +10,34 @@ margin-top: 1rem; } +.communityJoinButton[data-is-joined='true'] { + background: var(--asc-color-background-transparent-white); + border: 1px solid var(--asc-color-base-shade4); +} + .joinButton { width: 1.25rem; height: 1rem; + fill: var(--asc-color-white); +} + +.checkButton { + width: 1.25rem; + height: 1rem; + fill: var(--asc-color-black); +} + +.communityJoinButton__text { + color: var(--asc-color-white); + + /* IOS / 13 Caption Bold */ + font-size: 0.8125rem; + font-style: normal; + font-weight: 600; + line-height: 1.125rem; /* 138.462% */ + letter-spacing: -0.005rem; +} + +.communityJoinButton__text[data-is-joined='true'] { + color: var(--asc-color-black); } diff --git a/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.tsx b/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.tsx index 6e775613d..a490b7aed 100644 --- a/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.tsx +++ b/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.tsx @@ -5,6 +5,7 @@ import styles from './CommunityJoinButton.module.css'; import { IconComponent } from '~/v4/core/IconComponent'; import { Plus as PlusIcon } from '~/v4/icons/Plus'; import clsx from 'clsx'; +import { Typography } from '~/v4/core/components/Typography'; interface CommunityJoinButtonProps { pageId?: string; @@ -35,7 +36,7 @@ export const CommunityJoinButton = ({ <Button data-qa-anchor={accessibilityId} style={themeStyles} - onPress={onClick} + onPress={() => onClick?.()} className={clsx(styles.communityJoinButton, className)} > <IconComponent @@ -44,7 +45,9 @@ export const CommunityJoinButton = ({ defaultIconName={defaultConfig.icon} configIconName={config.icon} /> - Join + <Typography.CaptionBold className={styles.communityJoinButton__text}> + {config.text} + </Typography.CaptionBold> </Button> ); }; diff --git a/src/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton.module.css b/src/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton.module.css new file mode 100644 index 000000000..8c8cfbbab --- /dev/null +++ b/src/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton.module.css @@ -0,0 +1,35 @@ +.communityJoinedButton { + display: flex; + width: 100%; + background: var(--asc-color-background-transparent-white); + border: 1px solid var(--asc-color-base-shade4); + padding: 0.625rem 1rem 0.625rem 0.75rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + border-radius: 0.5rem; + margin-top: 1rem; +} + +.joinedButton { + width: 1.25rem; + height: 1rem; + fill: var(--asc-color-white); +} + +.checkButton { + width: 1.25rem; + height: 1rem; + fill: var(--asc-color-black); +} + +.communityJoinedButton__text { + color: var(--asc-color-black); + + /* IOS / 13 Caption Bold */ + font-size: 0.8125rem; + font-style: normal; + font-weight: 600; + line-height: 1.125rem; /* 138.462% */ + letter-spacing: -0.005rem; +} diff --git a/src/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton.tsx b/src/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton.tsx new file mode 100644 index 000000000..fdb87e07f --- /dev/null +++ b/src/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Button } from '~/v4/core/natives/Button'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import styles from './CommunityJoinedButton.module.css'; +import { IconComponent } from '~/v4/core/IconComponent'; +import clsx from 'clsx'; +import { Typography } from '~/v4/core/components/Typography'; +import Check from '~/v4/icons/Check'; + +interface CommunityJoinedButtonProps { + pageId?: string; + componentId?: string; + onClick?: () => void; + className?: string; + defaultClassName?: string; +} + +export const CommunityJoinedButton = ({ + pageId = '*', + componentId = '*', + onClick, + className, + defaultClassName, +}: CommunityJoinedButtonProps) => { + const elementId = 'community_joined_button'; + const { config, themeStyles, accessibilityId, isExcluded, uiReference, defaultConfig } = + useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + <Button + data-qa-anchor={accessibilityId} + style={themeStyles} + onPress={() => onClick?.()} + className={clsx(styles.communityJoinedButton, className)} + > + <IconComponent + defaultIcon={() => <Check className={clsx(styles.checkButton, defaultClassName)} />} + imgIcon={() => <img src={config.icon} alt={uiReference} />} + defaultIconName={defaultConfig.icon} + configIconName={config.icon} + /> + + <Typography.CaptionBold className={styles.communityJoinedButton__text}> + {config.text} + </Typography.CaptionBold> + </Button> + ); +}; diff --git a/src/v4/social/elements/CommunityJoinedButton/index.ts b/src/v4/social/elements/CommunityJoinedButton/index.ts new file mode 100644 index 000000000..b5c4cd298 --- /dev/null +++ b/src/v4/social/elements/CommunityJoinedButton/index.ts @@ -0,0 +1 @@ +export { CommunityJoinedButton } from './CommunityJoinedButton'; diff --git a/src/v4/social/elements/CommunityOfficialBadge/CommunityOfficialBadge.tsx b/src/v4/social/elements/CommunityOfficialBadge/CommunityOfficialBadge.tsx index caa4b526a..c919b5363 100644 --- a/src/v4/social/elements/CommunityOfficialBadge/CommunityOfficialBadge.tsx +++ b/src/v4/social/elements/CommunityOfficialBadge/CommunityOfficialBadge.tsx @@ -7,13 +7,20 @@ import styles from './CommunityOfficialBadge.module.css'; const OfficialBadgeIconSvg = (props: React.SVGProps<SVGSVGElement>) => ( <svg xmlns="http://www.w3.org/2000/svg" - width="21" - height="20" - viewBox="0 0 21 20" + width="17" + height="16" + viewBox="0 0 17 16" fill="none" {...props} > - <path d="M9.58112 15.4777C10.2749 16.1767 10.9373 16.1715 11.6363 15.4777L12.4188 14.6953C12.4918 14.6223 12.5544 14.6014 12.6483 14.6014H13.7541C14.74 14.6014 15.2095 14.1319 15.2095 13.1461V12.0402C15.2095 11.9463 15.2355 11.8785 15.3034 11.8107L16.0858 11.023C16.7848 10.3293 16.7796 9.66681 16.0858 8.97305L15.3034 8.19061C15.2355 8.11758 15.2095 8.05499 15.2095 7.9611V6.85525C15.2095 5.86938 14.74 5.39991 13.7541 5.39991H12.6483C12.5544 5.39991 12.4866 5.37383 12.4188 5.30602L11.6363 4.52358C10.9373 3.82461 10.2749 3.82461 9.58112 4.5288L8.79868 5.30602C8.73087 5.37383 8.66306 5.39991 8.56917 5.39991H7.46332C6.47745 5.39991 6.00799 5.85894 6.00799 6.85525V7.9611C6.00799 8.05499 5.9819 8.1228 5.91409 8.19061L5.13165 8.97305C4.43268 9.66681 4.43789 10.3293 5.13165 11.023L5.91409 11.8107C5.9819 11.8785 6.00799 11.9463 6.00799 12.0402V13.1461C6.00799 14.1319 6.47745 14.6014 7.46332 14.6014H8.56917C8.66306 14.6014 8.72565 14.6223 8.79868 14.6953L9.58112 15.4777ZM9.74282 12.6714C9.56026 12.6714 9.4142 12.6036 9.29944 12.4836L7.63024 10.6162C7.54156 10.5223 7.49462 10.3919 7.49462 10.2615C7.49462 9.94849 7.71892 9.72419 8.04232 9.72419C8.21968 9.72419 8.35008 9.78157 8.46484 9.90676L9.72196 11.3099L12.2362 7.72115C12.3614 7.5438 12.497 7.47077 12.7057 7.47077C13.0291 7.47077 13.2586 7.69507 13.2586 7.99761C13.2586 8.10193 13.2169 8.23234 13.1386 8.34188L10.2123 12.4471C10.0923 12.6036 9.93583 12.6714 9.74282 12.6714Z" /> + <path + d="M9.11463 13.479C8.41557 14.1728 7.75304 14.178 7.0592 13.479L6.27668 12.6964C6.20364 12.6234 6.14104 12.6025 6.04714 12.6025H4.94117C3.95519 12.6025 3.48568 12.133 3.48568 11.147V10.0411C3.48568 9.94718 3.45959 9.87936 3.39177 9.81154L2.60925 9.0238C1.91541 8.32996 1.9102 7.66743 2.60925 6.97359L3.39177 6.19107C3.45959 6.12325 3.48568 6.05543 3.48568 5.96153V4.85556C3.48568 3.85915 3.95519 3.40007 4.94117 3.40007H6.04714C6.14104 3.40007 6.20886 3.37398 6.27668 3.30616L7.0592 2.52886C7.75304 1.82459 8.41557 1.82459 9.11463 2.52364L9.89715 3.30616C9.96497 3.37398 10.0328 3.40007 10.1267 3.40007H11.2327C12.2186 3.40007 12.6882 3.86958 12.6882 4.85556V5.96153C12.6882 6.05543 12.7142 6.11803 12.7821 6.19107L13.5646 6.97359C14.2584 7.66743 14.2636 8.32996 13.5646 9.0238L12.7821 9.81154C12.7142 9.87936 12.6882 9.94718 12.6882 10.0411V11.147C12.6882 12.133 12.2186 12.6025 11.2327 12.6025H10.1267C10.0328 12.6025 9.97019 12.6234 9.89715 12.6964L9.11463 13.479Z" + fill="#1054DE" + /> + <path + d="M6.77768 10.4846C6.89245 10.6045 7.03852 10.6724 7.22111 10.6724C7.41413 10.6724 7.57063 10.6045 7.69062 10.448L10.6173 6.3424C10.6955 6.23285 10.7372 6.10243 10.7372 5.99809C10.7372 5.69551 10.5077 5.47119 10.1843 5.47119C9.97559 5.47119 9.83995 5.54423 9.71475 5.7216L7.20024 9.31077L5.94299 7.90745C5.82822 7.78224 5.69779 7.72486 5.52042 7.72486C5.19698 7.72486 4.97266 7.94918 4.97266 8.26219C4.97266 8.39261 5.01961 8.52303 5.10829 8.61694L6.77768 10.4846Z" + fill="white" + /> </svg> ); diff --git a/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.tsx b/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.tsx index b673a1cb8..36999f7d9 100644 --- a/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.tsx +++ b/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.tsx @@ -36,6 +36,7 @@ export const CommunityProfileTab: React.FC<CommunityTabsProps> = ({ className={styles.communityTabs__container} > <Button + data-qa-anchor={`${accessibilityId}_feed`} data-is-active={activeTab === 'community_feed'} onPress={() => onTabChange('community_feed')} className={styles.communityTabs__tab} @@ -43,12 +44,29 @@ export const CommunityProfileTab: React.FC<CommunityTabsProps> = ({ <FeedIcon /> </Button> <Button + data-qa-anchor={`${accessibilityId}_pin`} data-is-active={activeTab === 'community_pin'} onPress={() => onTabChange('community_pin')} className={styles.communityTabs__tab} > <PinIcon /> </Button> + {/* <Button + data-qa-anchor={`${accessibilityId}_photo`} + data-is-active={activeTab === 'community_pin'} + onPress={() => onTabChange('community_pin')} + className={styles.communityTabs__tab} + > + <PinIcon /> + </Button> + <Button + data-qa-anchor={`${accessibilityId}_video`} + data-is-active={activeTab === 'community_pin'} + onPress={() => onTabChange('community_pin')} + className={styles.communityTabs__tab} + > + <PinIcon /> + </Button> */} </div> ); }; diff --git a/src/v4/social/elements/CommunityRowImage/CommunityRowImage.module.css b/src/v4/social/elements/CommunityRowImage/CommunityRowImage.module.css new file mode 100644 index 000000000..3fd76b476 --- /dev/null +++ b/src/v4/social/elements/CommunityRowImage/CommunityRowImage.module.css @@ -0,0 +1,7 @@ +.communityRowImage__img { + width: 100%; + height: 100%; + overflow: hidden; + object-fit: cover; + border-radius: var(--asc-border-radius-sm); +} diff --git a/src/v4/social/elements/CommunityRowImage/CommunityRowImage.tsx b/src/v4/social/elements/CommunityRowImage/CommunityRowImage.tsx new file mode 100644 index 000000000..4df2dd422 --- /dev/null +++ b/src/v4/social/elements/CommunityRowImage/CommunityRowImage.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import styles from './CommunityRowImage.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { Img } from '~/v4/core/natives/Img/Img'; + +const PlaceholderImage = () => { + return ( + <svg xmlns="http://www.w3.org/2000/svg" width="81" height="80" viewBox="0 0 81 80" fill="none"> + <g clipPath="url(#clip0_7036_40403)"> + <rect x="0.449219" width="80" height="80" rx="4" fill="#A5A9B5" /> + <path + d="M40.449 28.8C41.9151 28.8 43.3213 29.3697 44.358 30.3837C45.3947 31.3977 45.9772 32.7729 45.9772 34.2069C45.9772 35.6409 45.3947 37.0162 44.358 38.0302C43.3213 39.0442 41.9151 39.6138 40.449 39.6138C38.9828 39.6138 37.5767 39.0442 36.5399 38.0302C35.5032 37.0162 34.9208 35.6409 34.9208 34.2069C34.9208 32.7729 35.5032 31.3977 36.5399 30.3837C37.5767 29.3697 38.9828 28.8 40.449 28.8ZM29.3926 32.6621C30.2771 32.6621 31.0984 32.8938 31.8092 33.3109C31.5722 35.52 32.2356 37.7137 33.594 39.4285C32.8042 40.9115 31.2248 41.9311 29.3926 41.9311C28.1358 41.9311 26.9306 41.4428 26.042 40.5737C25.1533 39.7045 24.6541 38.5257 24.6541 37.2966C24.6541 36.0675 25.1533 34.8887 26.042 34.0195C26.9306 33.1504 28.1358 32.6621 29.3926 32.6621ZM51.5054 32.6621C52.7621 32.6621 53.9673 33.1504 54.856 34.0195C55.7446 34.8887 56.2438 36.0675 56.2438 37.2966C56.2438 38.5257 55.7446 39.7045 54.856 40.5737C53.9673 41.4428 52.7621 41.9311 51.5054 41.9311C49.6732 41.9311 48.0937 40.9115 47.3039 39.4285C48.6623 37.7137 49.3257 35.52 49.0888 33.3109C49.7995 32.8938 50.6209 32.6621 51.5054 32.6621ZM30.1823 48.4966C30.1823 45.2988 34.7786 42.7035 40.449 42.7035C46.1193 42.7035 50.7156 45.2988 50.7156 48.4966V51.2H30.1823V48.4966ZM21.4951 51.2V48.8828C21.4951 46.7355 24.4803 44.928 28.5238 44.4028C27.5919 45.4533 27.0233 46.9054 27.0233 48.4966V51.2H21.4951ZM59.4028 51.2H53.8746V48.4966C53.8746 46.9054 53.306 45.4533 52.3741 44.4028C56.4176 44.928 59.4028 46.7355 59.4028 48.8828V51.2Z" + fill="white" + /> + <rect x="0.449219" width="80" height="80" fill="url(#paint0_linear_7036_40403)" /> + </g> + <defs> + <linearGradient + id="paint0_linear_7036_40403" + x1="40.4492" + y1="40" + x2="40.4492" + y2="80" + gradientUnits="userSpaceOnUse" + > + <stop stopOpacity="0" /> + <stop offset="1" stopOpacity="0.4" /> + </linearGradient> + <clipPath id="clip0_7036_40403"> + <rect x="0.449219" width="80" height="80" rx="4" fill="white" /> + </clipPath> + </defs> + </svg> + ); +}; + +interface CommunityRowImageProps { + pageId?: string; + componentId?: string; + imgSrc?: string; +} + +export const CommunityRowImage: React.FC<CommunityRowImageProps> = ({ + pageId = '*', + componentId = '*', + imgSrc, +}) => { + const elementId = 'community_row_image'; + + const { themeStyles } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + return ( + <Img + style={themeStyles} + className={styles.communityRowImage__img} + src={imgSrc} + fallBackRenderer={() => <PlaceholderImage />} + /> + ); +}; diff --git a/src/v4/social/elements/CommunityRowImage/index.ts b/src/v4/social/elements/CommunityRowImage/index.ts new file mode 100644 index 000000000..5054022bf --- /dev/null +++ b/src/v4/social/elements/CommunityRowImage/index.ts @@ -0,0 +1 @@ +export { CommunityRowImage } from './CommunityRowImage'; diff --git a/src/v4/social/elements/CreateCommunityButton/CreateCommunityButton.tsx b/src/v4/social/elements/CreateCommunityButton/CreateCommunityButton.tsx index c6936410b..2b6791bd0 100644 --- a/src/v4/social/elements/CreateCommunityButton/CreateCommunityButton.tsx +++ b/src/v4/social/elements/CreateCommunityButton/CreateCommunityButton.tsx @@ -38,7 +38,7 @@ export function CreateCommunityButton({ configIconName={config.icon} defaultIconName={defaultConfig.icon} /> - <Typography.Body>{config.text}</Typography.Body> + <Typography.Body>Create Community</Typography.Body> </div> ); } diff --git a/src/v4/social/elements/CreateNewPostButton/CreateNewPostButton.tsx b/src/v4/social/elements/CreateNewPostButton/CreateNewPostButton.tsx index 200bb20dd..ed28d4278 100644 --- a/src/v4/social/elements/CreateNewPostButton/CreateNewPostButton.tsx +++ b/src/v4/social/elements/CreateNewPostButton/CreateNewPostButton.tsx @@ -1,12 +1,13 @@ import React from 'react'; import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { Button } from '~/v4/core/natives/Button'; import styles from './CreateNewPostButton.module.css'; interface CreateNewPostButtonProps { pageId: string; componentId?: string; isValid: boolean; - onSubmit: (e: React.FormEvent) => void; + onSubmit: () => void; } export function CreateNewPostButton({ @@ -24,15 +25,15 @@ export function CreateNewPostButton({ if (isExcluded) return null; return ( - <button - onSubmit={onSubmit} + <Button + onPress={onSubmit} style={themeStyles} - disabled={!isValid} + isDisabled={!isValid} className={styles.createNewPostButton} type="submit" data-qa-anchor={accessibilityId} > {config.text} - </button> + </Button> ); } diff --git a/src/v4/social/elements/EditPostTitle/EditPostTitle.tsx b/src/v4/social/elements/EditPostTitle/EditPostTitle.tsx index 732857dca..cf96204f0 100644 --- a/src/v4/social/elements/EditPostTitle/EditPostTitle.tsx +++ b/src/v4/social/elements/EditPostTitle/EditPostTitle.tsx @@ -9,7 +9,7 @@ interface EditPostTitleProps { export function EditPostTitle({ pageId = '*', componentId = '*' }: EditPostTitleProps) { const elementId = 'edit_post_title'; - const { config, isExcluded, themeStyles } = useAmityElement({ + const { config, isExcluded, themeStyles, accessibilityId } = useAmityElement({ pageId, componentId, elementId, @@ -17,7 +17,7 @@ export function EditPostTitle({ pageId = '*', componentId = '*' }: EditPostTitle if (isExcluded) return null; return ( - <div style={themeStyles} className={styles.editPostTitle}> + <div data-qa-anchor={accessibilityId} style={themeStyles} className={styles.editPostTitle}> {config.text} </div> ); diff --git a/src/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity.module.css b/src/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity.module.css new file mode 100644 index 000000000..e16c22e9f --- /dev/null +++ b/src/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity.module.css @@ -0,0 +1,20 @@ +.exploreCreateCommunityButton { + display: flex; + padding: 0.625rem 1rem 0.625rem 0.75rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + border-radius: 0.25rem; + background: var(--asc-color-primary-default); + cursor: pointer; +} + +.exploreCreateCommunityButton__icon { + fill: var(--asc-color-white); + height: 1.25rem; + width: 1.25rem; +} + +.exploreCreateCommunityButton__text { + color: var(--asc-color-white); +} diff --git a/src/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity.tsx b/src/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity.tsx new file mode 100644 index 000000000..51ba78cc0 --- /dev/null +++ b/src/v4/social/elements/ExploreCreateCommunity/ExploreCreateCommunity.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Typography } from '~/v4/core/components'; +import styles from './ExploreCreateCommunity.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { Plus } from '~/v4/icons/Plus'; +import { Button } from '~/v4/core/natives/Button'; + +interface DescriptionProps { + pageId?: string; + componentId?: string; + onClick?: () => void; +} + +export function ExploreCreateCommunity({ + pageId = '*', + componentId = '*', + onClick, +}: DescriptionProps) { + const elementId = 'explore_create_community'; + const { accessibilityId, config, defaultConfig, isExcluded, uiReference, themeStyles } = + useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + <Button + className={styles.exploreCreateCommunityButton} + data-qa-anchor={accessibilityId} + style={themeStyles} + onPress={onClick} + > + <Plus className={styles.exploreCreateCommunityButton__icon} /> + <Typography.BodyBold className={styles.exploreCreateCommunityButton__text}> + {config.text} + </Typography.BodyBold> + </Button> + ); +} diff --git a/src/v4/social/elements/ExploreCreateCommunity/index.tsx b/src/v4/social/elements/ExploreCreateCommunity/index.tsx new file mode 100644 index 000000000..9a1dc9788 --- /dev/null +++ b/src/v4/social/elements/ExploreCreateCommunity/index.tsx @@ -0,0 +1 @@ +export { ExploreCreateCommunity } from './ExploreCreateCommunity'; diff --git a/src/v4/social/elements/ExploreEmptyImage/ExploreEmptyImage.tsx b/src/v4/social/elements/ExploreEmptyImage/ExploreEmptyImage.tsx new file mode 100644 index 000000000..fd5f08e9b --- /dev/null +++ b/src/v4/social/elements/ExploreEmptyImage/ExploreEmptyImage.tsx @@ -0,0 +1,191 @@ +import React from 'react'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { IconComponent } from '~/v4/core/IconComponent'; + +const DarkExploreEmptyImageSvg = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="161" + height="161" + viewBox="0 0 161 161" + fill="none" + > + <path + d="M130.1 24.5H34.0996C29.6813 24.5 26.0996 28.0817 26.0996 32.5V128.5C26.0996 132.918 29.6813 136.5 34.0996 136.5H130.1C134.518 136.5 138.1 132.918 138.1 128.5V32.5C138.1 28.0817 134.518 24.5 130.1 24.5Z" + fill="#292B32" + /> + <path + d="M43.7002 68.2H44.2002H151.7C152.628 68.2 153.519 68.5688 154.175 69.2251C154.831 69.8815 155.2 70.7718 155.2 71.7V91.7C155.2 92.6283 154.831 93.5185 154.175 94.1749C153.519 94.8313 152.628 95.2 151.7 95.2H43.7002C42.7719 95.2 41.8817 94.8313 41.2253 94.1749C40.5689 93.5185 40.2002 92.6283 40.2002 91.7V71.7C40.2002 70.7718 40.5689 69.8815 41.2253 69.2251C41.8817 68.5688 42.7719 68.2 43.7002 68.2Z" + fill="#292B32" + stroke="#40434E" + /> + <path + opacity="0.3" + d="M90.9002 74.1H70.1002C68.7747 74.1 67.7002 75.1745 67.7002 76.5C67.7002 77.8255 68.7747 78.9 70.1002 78.9H90.9002C92.2257 78.9 93.3002 77.8255 93.3002 76.5C93.3002 75.1745 92.2257 74.1 90.9002 74.1Z" + fill="#40434E" + /> + <path + opacity="0.15" + d="M105.3 84.5H70.1002C68.7747 84.5 67.7002 85.5745 67.7002 86.9C67.7002 88.2255 68.7747 89.3 70.1002 89.3H105.3C106.626 89.3 107.7 88.2255 107.7 86.9C107.7 85.5745 106.626 84.5 105.3 84.5Z" + fill="#40434E" + /> + <path + d="M53.6996 89.3C57.897 89.3 61.2996 85.8974 61.2996 81.7C61.2996 77.5027 57.897 74.1 53.6996 74.1C49.5022 74.1 46.0996 77.5027 46.0996 81.7C46.0996 85.8974 49.5022 89.3 53.6996 89.3Z" + fill="#40434E" + /> + <path + d="M9.2998 102.6H9.7998H117.3C118.228 102.6 119.118 102.969 119.775 103.625C120.431 104.282 120.8 105.172 120.8 106.1V126.1C120.8 127.028 120.431 127.919 119.775 128.575C119.118 129.231 118.228 129.6 117.3 129.6H9.2998C8.37155 129.6 7.48131 129.231 6.82493 128.575C6.16855 127.919 5.7998 127.028 5.7998 126.1V106.1C5.7998 105.172 6.16855 104.282 6.82493 103.625C7.48131 102.969 8.37155 102.6 9.2998 102.6Z" + fill="#292B32" + stroke="#40434E" + /> + <path + opacity="0.3" + d="M56.4998 108.5H35.6998C34.3743 108.5 33.2998 109.575 33.2998 110.9C33.2998 112.225 34.3743 113.3 35.6998 113.3H56.4998C57.8253 113.3 58.8998 112.225 58.8998 110.9C58.8998 109.575 57.8253 108.5 56.4998 108.5Z" + fill="#40434E" + /> + <path + opacity="0.15" + d="M70.8998 118.9H35.6998C34.3743 118.9 33.2998 119.975 33.2998 121.3C33.2998 122.626 34.3743 123.7 35.6998 123.7H70.8998C72.2253 123.7 73.2998 122.626 73.2998 121.3C73.2998 119.975 72.2253 118.9 70.8998 118.9Z" + fill="#40434E" + /> + <path + d="M19.3002 123.7C23.4976 123.7 26.9002 120.297 26.9002 116.1C26.9002 111.903 23.4976 108.5 19.3002 108.5C15.1028 108.5 11.7002 111.903 11.7002 116.1C11.7002 120.297 15.1028 123.7 19.3002 123.7Z" + fill="#40434E" + /> + <path + d="M9.2998 33.8H117.3C119.233 33.8 120.8 35.367 120.8 37.3V57.3C120.8 59.233 119.233 60.8 117.3 60.8H9.2998C7.36681 60.8 5.7998 59.233 5.7998 57.3V37.3C5.7998 35.367 7.36681 33.8 9.2998 33.8Z" + fill="#292B32" + stroke="#40434E" + /> + <path + opacity="0.3" + d="M54.8992 39.7H34.0992C32.7737 39.7 31.6992 40.7745 31.6992 42.1C31.6992 43.4255 32.7737 44.5 34.0992 44.5H54.8992C56.2247 44.5 57.2992 43.4255 57.2992 42.1C57.2992 40.7745 56.2247 39.7 54.8992 39.7Z" + fill="#40434E" + /> + <path + opacity="0.15" + d="M69.2992 50.1H34.0992C32.7737 50.1 31.6992 51.1745 31.6992 52.5C31.6992 53.8255 32.7737 54.9 34.0992 54.9H69.2992C70.6247 54.9 71.6992 53.8255 71.6992 52.5C71.6992 51.1745 70.6247 50.1 69.2992 50.1Z" + fill="#40434E" + /> + <path + d="M19.3002 54.9C23.4976 54.9 26.9002 51.4974 26.9002 47.3C26.9002 43.1026 23.4976 39.7 19.3002 39.7C15.1028 39.7 11.7002 43.1026 11.7002 47.3C11.7002 51.4974 15.1028 54.9 19.3002 54.9Z" + fill="#40434E" + /> + </svg> +); + +const ExploreEmptyImageSvg = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="161" + height="160" + viewBox="0 0 161 160" + fill="none" + > + <path + d="M130.452 24H34.4521C30.0339 24 26.4521 27.5817 26.4521 32V128C26.4521 132.418 30.0339 136 34.4521 136H130.452C134.87 136 138.452 132.418 138.452 128V32C138.452 27.5817 134.87 24 130.452 24Z" + fill="#EBECEF" + /> + <path + d="M44.0522 67.7H44.5522H152.052C152.981 67.7 153.871 68.0688 154.527 68.7251C155.184 69.3815 155.552 70.2718 155.552 71.2V91.2C155.552 92.1283 155.184 93.0185 154.527 93.6749C153.871 94.3313 152.981 94.7 152.052 94.7H44.0522C43.124 94.7 42.2337 94.3313 41.5774 93.6749C40.921 93.0185 40.5522 92.1283 40.5522 91.2V71.2C40.5522 70.2718 40.921 69.3815 41.5774 68.7251C42.2337 68.0688 43.124 67.7 44.0522 67.7Z" + fill="white" + stroke="#EBECEF" + /> + <path + opacity="0.3" + d="M91.2523 73.6H70.4522C69.1268 73.6 68.0522 74.6745 68.0522 76C68.0522 77.3255 69.1268 78.4 70.4522 78.4H91.2523C92.5777 78.4 93.6523 77.3255 93.6523 76C93.6523 74.6745 92.5777 73.6 91.2523 73.6Z" + fill="#A5A9B5" + /> + <path + opacity="0.15" + d="M105.652 84H70.4522C69.1268 84 68.0522 85.0745 68.0522 86.4C68.0522 87.7255 69.1268 88.8 70.4522 88.8H105.652C106.978 88.8 108.052 87.7255 108.052 86.4C108.052 85.0745 106.978 84 105.652 84Z" + fill="#A5A9B5" + /> + <path + d="M54.0521 88.8C58.2495 88.8 61.6521 85.3974 61.6521 81.2C61.6521 77.0027 58.2495 73.6 54.0521 73.6C49.8548 73.6 46.4521 77.0027 46.4521 81.2C46.4521 85.3974 49.8548 88.8 54.0521 88.8Z" + fill="#A5A9B5" + /> + <path + d="M9.65234 102.1H10.1523H117.652C118.581 102.1 119.471 102.469 120.127 103.125C120.784 103.782 121.152 104.672 121.152 105.6V125.6C121.152 126.528 120.784 127.419 120.127 128.075C119.471 128.731 118.581 129.1 117.652 129.1H9.65234C8.72409 129.1 7.83385 128.731 7.17747 128.075C6.52109 127.419 6.15234 126.528 6.15234 125.6V105.6C6.15234 104.672 6.52109 103.782 7.17747 103.125C7.83385 102.469 8.72409 102.1 9.65234 102.1Z" + fill="white" + stroke="#EBECEF" + /> + <path + opacity="0.3" + d="M56.8523 108H36.0523C34.7269 108 33.6523 109.075 33.6523 110.4C33.6523 111.725 34.7269 112.8 36.0523 112.8H56.8523C58.1778 112.8 59.2523 111.725 59.2523 110.4C59.2523 109.075 58.1778 108 56.8523 108Z" + fill="#A5A9B5" + /> + <path + opacity="0.15" + d="M71.2523 118.4H36.0523C34.7269 118.4 33.6523 119.475 33.6523 120.8C33.6523 122.126 34.7269 123.2 36.0523 123.2H71.2523C72.5778 123.2 73.6523 122.126 73.6523 120.8C73.6523 119.475 72.5778 118.4 71.2523 118.4Z" + fill="#A5A9B5" + /> + <path + d="M19.6522 123.2C23.8496 123.2 27.2522 119.797 27.2522 115.6C27.2522 111.403 23.8496 108 19.6522 108C15.4549 108 12.0522 111.403 12.0522 115.6C12.0522 119.797 15.4549 123.2 19.6522 123.2Z" + fill="#A5A9B5" + /> + <path + d="M9.65234 33.3H117.652C119.585 33.3 121.152 34.867 121.152 36.8V56.8C121.152 58.733 119.585 60.3 117.652 60.3H9.65234C7.71935 60.3 6.15234 58.733 6.15234 56.8V36.8C6.15234 34.867 7.71935 33.3 9.65234 33.3Z" + fill="white" + stroke="#EBECEF" + /> + <path + opacity="0.3" + d="M55.2522 39.2H34.4522C33.1268 39.2 32.0522 40.2745 32.0522 41.6C32.0522 42.9255 33.1268 44 34.4522 44H55.2522C56.5777 44 57.6522 42.9255 57.6522 41.6C57.6522 40.2745 56.5777 39.2 55.2522 39.2Z" + fill="#A5A9B5" + /> + <path + opacity="0.15" + d="M69.6522 49.6H34.4522C33.1268 49.6 32.0522 50.6745 32.0522 52C32.0522 53.3255 33.1268 54.4 34.4522 54.4H69.6522C70.9777 54.4 72.0522 53.3255 72.0522 52C72.0522 50.6745 70.9777 49.6 69.6522 49.6Z" + fill="#A5A9B5" + /> + <path + d="M19.6522 54.4C23.8496 54.4 27.2522 50.9974 27.2522 46.8C27.2522 42.6026 23.8496 39.2 19.6522 39.2C15.4549 39.2 12.0522 42.6026 12.0522 46.8C12.0522 50.9974 15.4549 54.4 19.6522 54.4Z" + fill="#A5A9B5" + /> + </svg> +); + +interface ExploreEmptyImageProps { + pageId?: string; + componentId?: string; +} + +export const ExploreEmptyImage = ({ pageId = '*', componentId = '*' }: ExploreEmptyImageProps) => { + const elementId = 'explore_empty_image'; + const { + currentTheme, + accessibilityId, + config, + defaultConfig, + isExcluded, + uiReference, + themeStyles, + } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + <IconComponent + defaultIconName={defaultConfig.icon} + configIconName={config.icon} + imgIcon={() => ( + <img + style={themeStyles} + src={config.icon} + alt={uiReference} + data-qa-anchor={accessibilityId} + /> + )} + defaultIcon={() => ( + <div data-qa-anchor={accessibilityId} style={themeStyles}> + {currentTheme === 'light' ? <ExploreEmptyImageSvg /> : <DarkExploreEmptyImageSvg />} + </div> + )} + /> + ); +}; diff --git a/src/v4/social/elements/ExploreEmptyImage/index.tsx b/src/v4/social/elements/ExploreEmptyImage/index.tsx new file mode 100644 index 000000000..63f270016 --- /dev/null +++ b/src/v4/social/elements/ExploreEmptyImage/index.tsx @@ -0,0 +1 @@ +export { ExploreEmptyImage } from './ExploreEmptyImage'; diff --git a/src/v4/social/elements/ExploreRecommendedTitle/ExploreRecommendedTitle.module.css b/src/v4/social/elements/ExploreRecommendedTitle/ExploreRecommendedTitle.module.css new file mode 100644 index 000000000..03aa3f7c3 --- /dev/null +++ b/src/v4/social/elements/ExploreRecommendedTitle/ExploreRecommendedTitle.module.css @@ -0,0 +1,3 @@ +.exploreRecommendedTitle { + color: var(--asc-color-base-default); +} diff --git a/src/v4/social/elements/ExploreRecommendedTitle/ExploreRecommendedTitle.tsx b/src/v4/social/elements/ExploreRecommendedTitle/ExploreRecommendedTitle.tsx new file mode 100644 index 000000000..705957d71 --- /dev/null +++ b/src/v4/social/elements/ExploreRecommendedTitle/ExploreRecommendedTitle.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Typography } from '~/v4/core/components'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import clsx from 'clsx'; +import styles from './ExploreRecommendedTitle.module.css'; + +interface TitleProps { + pageId?: string; + componentId?: string; + titleClassName?: string; +} + +export function ExploreRecommendedTitle({ + pageId = '*', + componentId = '*', + titleClassName, +}: TitleProps) { + const elementId = 'explore_recommended_title'; + const { accessibilityId, config, isExcluded, themeStyles } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + <Typography.Title + className={clsx(styles.exploreRecommendedTitle, titleClassName)} + style={themeStyles} + data-qa-anchor={accessibilityId} + > + {config.text} + </Typography.Title> + ); +} diff --git a/src/v4/social/elements/ExploreRecommendedTitle/index.tsx b/src/v4/social/elements/ExploreRecommendedTitle/index.tsx new file mode 100644 index 000000000..a80ae8654 --- /dev/null +++ b/src/v4/social/elements/ExploreRecommendedTitle/index.tsx @@ -0,0 +1 @@ +export { ExploreRecommendedTitle } from './ExploreRecommendedTitle'; diff --git a/src/v4/social/elements/ExploreTrendingTitle/ExploreTrendingTitle.module.css b/src/v4/social/elements/ExploreTrendingTitle/ExploreTrendingTitle.module.css new file mode 100644 index 000000000..ff76513bb --- /dev/null +++ b/src/v4/social/elements/ExploreTrendingTitle/ExploreTrendingTitle.module.css @@ -0,0 +1,3 @@ +.exploreTrendingTitle { + color: var(--asc-color-base-default); +} diff --git a/src/v4/social/elements/ExploreTrendingTitle/ExploreTrendingTitle.tsx b/src/v4/social/elements/ExploreTrendingTitle/ExploreTrendingTitle.tsx new file mode 100644 index 000000000..e1a939a73 --- /dev/null +++ b/src/v4/social/elements/ExploreTrendingTitle/ExploreTrendingTitle.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Typography } from '~/v4/core/components'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import clsx from 'clsx'; +import styles from './ExploreTrendingTitle.module.css'; + +interface TitleProps { + pageId?: string; + componentId?: string; + titleClassName?: string; +} + +export function ExploreTrendingTitle({ + pageId = '*', + componentId = '*', + titleClassName, +}: TitleProps) { + const elementId = 'explore_trending_title'; + const { accessibilityId, config, isExcluded, themeStyles } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + <Typography.Title + className={clsx(styles.exploreTrendingTitle, titleClassName)} + style={themeStyles} + data-qa-anchor={accessibilityId} + > + {config.text} + </Typography.Title> + ); +} diff --git a/src/v4/social/elements/ExploreTrendingTitle/index.tsx b/src/v4/social/elements/ExploreTrendingTitle/index.tsx new file mode 100644 index 000000000..c1399f1b1 --- /dev/null +++ b/src/v4/social/elements/ExploreTrendingTitle/index.tsx @@ -0,0 +1 @@ +export { ExploreTrendingTitle } from './ExploreTrendingTitle'; diff --git a/src/v4/social/elements/MyTimelineAvatar/MyTimelineAvatar.tsx b/src/v4/social/elements/MyTimelineAvatar/MyTimelineAvatar.tsx index 25a9bdd01..a671c88fc 100644 --- a/src/v4/social/elements/MyTimelineAvatar/MyTimelineAvatar.tsx +++ b/src/v4/social/elements/MyTimelineAvatar/MyTimelineAvatar.tsx @@ -24,7 +24,12 @@ export function MyTimelineAvatar({ if (isExcluded) return null; return ( <div className={styles.myTimelineAvatar} data-qa-anchor={accessibilityId}> - <UserAvatar className={styles.myTimelineAvatar__userAvatar} userId={userId} /> + <UserAvatar + pageId={pageId} + componentId={componentId} + className={styles.myTimelineAvatar__userAvatar} + userId={userId} + /> </div> ); } diff --git a/src/v4/social/elements/PostTextField/PostTextField.tsx b/src/v4/social/elements/PostTextField/PostTextField.tsx index 9ba25416e..25ee78ed0 100644 --- a/src/v4/social/elements/PostTextField/PostTextField.tsx +++ b/src/v4/social/elements/PostTextField/PostTextField.tsx @@ -17,7 +17,7 @@ import { Mentioned, Mentionees } from '~/v4/helpers/utils'; import { LinkPlugin } from '~/v4/social/internal-components/Lexical/plugins/LinkPlugin'; import { AutoLinkPlugin } from '~/v4/social/internal-components/Lexical/plugins/AutoLinkPlugin'; import { - editorStateToText, + editorToText, getEditorConfig, MentionData, textToEditorState, @@ -29,6 +29,7 @@ import { AutoLinkNode, LinkNode } from '@lexical/link'; import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; import useCommunity from '~/v4/core/hooks/collections/useCommunity'; import { MentionItem } from '~/v4/social/internal-components/Lexical/MentionItem'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; interface PostTextFieldProps { pageId?: string; @@ -136,8 +137,10 @@ export const PostTextField = ({ pageId = '*', componentId = '*', }: PostTextFieldProps) => { - const [intersectionNode, setIntersectionNode] = useState<HTMLDivElement | null>(null); const elementId = 'post_text_field'; + const [intersectionNode, setIntersectionNode] = useState<HTMLElement | null>(null); + + const { accessibilityId } = useAmityElement({ pageId, componentId, elementId }); const editorConfig = getEditorConfig({ namespace: 'PostTextField', @@ -153,9 +156,7 @@ export const PostTextField = ({ useIntersectionObserver({ onIntersect: () => { - if (isLoading === false) { - loadMore(); - } + loadMore(); }, node: intersectionNode, options: { @@ -173,10 +174,7 @@ export const PostTextField = ({ : {}), }} > - <div - className={styles.editorContainer} - data-qa-anchor={`${pageId}/${componentId}/${elementId}`} - > + <div className={styles.editorContainer} data-qa-anchor={accessibilityId}> <RichTextPlugin contentEditable={<ContentEditable />} placeholder={<div className={styles.editorPlaceholder}>What’s going on...</div>} @@ -184,7 +182,7 @@ export const PostTextField = ({ /> <OnChangePlugin onChange={(_, editor) => { - onChange(editorStateToText(editor)); + onChange(editorToText(editor)); }} /> <HistoryPlugin /> @@ -222,14 +220,18 @@ export const PostTextField = ({ setHighlightedIndex(i); }} key={option.key} - option={option} + option={{ + ...option, + setRefElement: (element) => { + if (i === options.length - 1) { + setIntersectionNode(element); + } + option.setRefElement(element); + }, + }} /> ); })} - <div - ref={(node) => setIntersectionNode(node)} - className={styles.postTextField__mentionInterceptor} - /> </>, mentionContainer, ); diff --git a/src/v4/social/hooks/useCategoriesByIds.ts b/src/v4/social/hooks/useCategoriesByIds.ts index 981e36ec7..19024d41b 100644 --- a/src/v4/social/hooks/useCategoriesByIds.ts +++ b/src/v4/social/hooks/useCategoriesByIds.ts @@ -8,9 +8,11 @@ const useCategoriesByIds = (categoryIds?: string[]) => { async function run() { if (categoryIds == null || categoryIds.length === 0) return; const categories = await Promise.all( - categoryIds.map( - async (categoryId) => (await CategoryRepository.getCategory(categoryId)).data, - ), + categoryIds.map(async (categoryId) => { + const cache = CategoryRepository.getCategory.locally(categoryId); + if (cache?.data) return cache.data; + return (await CategoryRepository.getCategory(categoryId)).data; + }), ); setCategories(categories); } @@ -19,5 +21,4 @@ const useCategoriesByIds = (categoryIds?: string[]) => { return categories; }; - export default useCategoriesByIds; diff --git a/src/v4/social/hooks/useCommunityActions.ts b/src/v4/social/hooks/useCommunityActions.ts new file mode 100644 index 000000000..e2ea00b38 --- /dev/null +++ b/src/v4/social/hooks/useCommunityActions.ts @@ -0,0 +1,57 @@ +import { CommunityRepository } from '@amityco/ts-sdk'; +import { useMutation } from '@tanstack/react-query'; +import { useNotifications } from '~/v4/core/providers/NotificationProvider'; + +export const useCommunityActions = ({ + onJoinSuccess, + onJoinError, + onLeaveSuccess, + onLeaveError, +}: { + onJoinSuccess?: () => void; + onJoinError?: (error: Error) => void; + onLeaveSuccess?: () => void; + onLeaveError?: (error: Error) => void; +} = {}): { + joinCommunity: (communityId: string) => void; + leaveCommunity: (communityId: string) => void; +} => { + const { error: errorFn, success } = useNotifications(); + + const { mutate: joinCommunity } = useMutation({ + mutationFn: (communityId: string) => CommunityRepository.joinCommunity(communityId), + onSuccess: () => { + success({ + content: 'Successfully joined the community.', + }); + onJoinSuccess?.(); + }, + onError: (error) => { + errorFn({ + content: 'Failed to join the community', + }); + onJoinError?.(error); + }, + }); + + const { mutate: leaveCommunity } = useMutation({ + mutationFn: (communityId: string) => CommunityRepository.leaveCommunity(communityId), + onSuccess: () => { + success({ + content: 'Successfully left the community', + }); + onLeaveSuccess?.(); + }, + onError: (error) => { + errorFn({ + content: 'Failed to leave the community', + }); + onLeaveError?.(error); + }, + }); + + return { + joinCommunity, + leaveCommunity, + }; +}; diff --git a/src/v4/social/internal-components/BrandBadge/BrandBadge.module.css b/src/v4/social/internal-components/BrandBadge/BrandBadge.module.css new file mode 100644 index 000000000..40c7d20ae --- /dev/null +++ b/src/v4/social/internal-components/BrandBadge/BrandBadge.module.css @@ -0,0 +1,32 @@ +.badge { + position: relative; + border-radius: var(--asc-border-radius-full); + padding: var(--asc-spacing-none) var(--asc-spacing-xxs3); +} + +.badge::before { + content: ''; + position: absolute; + inset: 0; + background-color: var(--asc-color-base-shade1); + opacity: 0.5; + border-radius: inherit; +} + +.badge__icon { + width: 0.75rem; + height: 0.75rem; +} + +.badge__text { + font-size: var(--asc-text-font-size-xs); + line-height: var(--asc-line-height-sm); + color: var(--asc-color-white); +} + +.badge__child { + position: relative; + display: flex; + gap: var(--asc-spacing-xxs1); + align-items: center; +} diff --git a/src/v4/social/internal-components/BrandBadge/BrandBadge.stories.tsx b/src/v4/social/internal-components/BrandBadge/BrandBadge.stories.tsx new file mode 100644 index 000000000..5cd8c80b2 --- /dev/null +++ b/src/v4/social/internal-components/BrandBadge/BrandBadge.stories.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BrandBadge } from './BrandBadge'; + +export default { + title: 'v4-social/internal-components/BrandBadge', +}; + +export const BrandBadgeStory = { + render: () => <BrandBadge />, +}; diff --git a/src/v4/social/internal-components/BrandBadge/BrandBadge.tsx b/src/v4/social/internal-components/BrandBadge/BrandBadge.tsx new file mode 100644 index 000000000..66b1ebab4 --- /dev/null +++ b/src/v4/social/internal-components/BrandBadge/BrandBadge.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import Brand from '~/v4/icons/Brand'; + +interface BrandBadgeProps { + className?: string; +} + +export const BrandBadge = ({ className }: BrandBadgeProps) => { + return <Brand className={className} />; +}; diff --git a/src/v4/social/internal-components/BrandBadge/index.tsx b/src/v4/social/internal-components/BrandBadge/index.tsx new file mode 100644 index 000000000..068a46276 --- /dev/null +++ b/src/v4/social/internal-components/BrandBadge/index.tsx @@ -0,0 +1 @@ +export { BrandBadge } from './BrandBadge'; diff --git a/src/v4/social/internal-components/CategoryImage/CategoryImage.module.css b/src/v4/social/internal-components/CategoryImage/CategoryImage.module.css new file mode 100644 index 000000000..78db3e0e2 --- /dev/null +++ b/src/v4/social/internal-components/CategoryImage/CategoryImage.module.css @@ -0,0 +1,7 @@ +.categoryImage__placeHolderRect { + fill: var(--asc-color-primary-shade2); +} + +.categoryImage__placeHolderPath { + fill: var(--asc-color-white); +} diff --git a/src/v4/social/internal-components/CategoryImage/CategoryImage.tsx b/src/v4/social/internal-components/CategoryImage/CategoryImage.tsx new file mode 100644 index 000000000..fc1100933 --- /dev/null +++ b/src/v4/social/internal-components/CategoryImage/CategoryImage.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import styles from './CategoryImage.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { Img } from '~/v4/core/natives/Img/Img'; + +const CategoryImagePlaceHolder = ({ className }: { className?: string }) => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="28" + height="28" + viewBox="0 0 28 28" + fill="none" + className={className} + > + <rect width="28" height="28" rx="14" className={styles.categoryImage__placeHolderRect} /> + <path + d="M19.6 14.7C19.6 14.3281 19.2719 14 18.9 14H15.4C15.0063 14 14.7 14.3281 14.7 14.7V18.2C14.7 18.5938 15.0063 18.9 15.4 18.9H18.9C19.2719 18.9 19.6 18.5938 19.6 18.2V14.7ZM9.8 13.3C8.24688 13.3 7 14.5688 7 16.1C7 17.6531 8.24688 18.9 9.8 18.9C11.3313 18.9 12.6 17.6531 12.6 16.1C12.6 14.5688 11.3313 13.3 9.8 13.3ZM18.8781 11.9C19.425 11.9 19.775 11.3313 19.4906 10.85L17.4125 7.35002C17.1281 6.89065 16.45 6.89065 16.1656 7.35002L14.0875 10.85C13.8031 11.3313 14.1531 11.9 14.7 11.9H18.8781Z" + className={styles.categoryImage__placeHolderPath} + /> + </svg> +); + +interface CategoryImageProps { + pageId: string; + componentId?: string; + elementId?: string; + imgSrc?: string; + className?: string; +} + +export const CategoryImage = ({ + imgSrc, + pageId = '*', + componentId = '*', + elementId = '*', + className, +}: CategoryImageProps) => { + const { themeStyles } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + return ( + <Img + style={themeStyles} + className={className} + src={imgSrc} + fallBackRenderer={() => <CategoryImagePlaceHolder className={className} />} + /> + ); +}; diff --git a/src/v4/social/internal-components/CategoryImage/index.tsx b/src/v4/social/internal-components/CategoryImage/index.tsx new file mode 100644 index 000000000..63ff08f1b --- /dev/null +++ b/src/v4/social/internal-components/CategoryImage/index.tsx @@ -0,0 +1 @@ +export { CategoryImage } from './CategoryImage'; diff --git a/src/v4/social/internal-components/Comment/index.tsx b/src/v4/social/internal-components/Comment/index.tsx index 92a7f8a06..6ca62385f 100644 --- a/src/v4/social/internal-components/Comment/index.tsx +++ b/src/v4/social/internal-components/Comment/index.tsx @@ -6,6 +6,7 @@ import { isCommunityMember, isNonNullable, Mentioned, + Mentionees, Metadata, parseMentionsMarkup, } from '~/v4/helpers/utils'; @@ -111,18 +112,14 @@ export const Comment = ({ return toggleFlagComment(); }; - const handleEditComment = async ( - text: string, - mentionees: Amity.UserMention[], - metadata: Metadata, - ) => + const handleEditComment = async (text: string, mentionees: Mentionees, metadata: Metadata) => commentId && CommentRepository.updateComment(commentId, { data: { text, }, metadata, - mentionees, + mentionees: mentionees as Amity.UserMention[], }); const handleDeleteComment = async () => commentId && CommentRepository.deleteComment(commentId); diff --git a/src/v4/social/internal-components/CommentAd/UICommentAd.tsx b/src/v4/social/internal-components/CommentAd/UICommentAd.tsx index eabf7afe1..2fbc4754a 100644 --- a/src/v4/social/internal-components/CommentAd/UICommentAd.tsx +++ b/src/v4/social/internal-components/CommentAd/UICommentAd.tsx @@ -6,6 +6,7 @@ import Broadcast from '~/v4/icons/Broadcast'; import InfoCircle from '~/v4/icons/InfoCircle'; import { Button } from '~/v4/core/natives/Button'; import { AdInformation } from '~/v4/social/internal-components/AdInformation/AdInformation'; +import clsx from 'clsx'; interface UICommentAdProps { ad: Amity.Ad; @@ -57,9 +58,13 @@ export const UICommentAd = ({ </div> <div className={styles.commentAd__adCard__detail}> <div className={styles.commentAd__adCard__textContainer}> - <Typography.Caption className={styles.commentAd__adCard__description}> - {ad.description} - </Typography.Caption> + <Typography.Caption + renderer={({ typoClassName }) => ( + <div className={clsx(typoClassName, styles.commentAd__adCard__description)}> + {ad.description} + </div> + )} + /> <Typography.BodyBold className={styles.commentAd__adCard__headline}> {ad.headline} </Typography.BodyBold> diff --git a/src/v4/social/internal-components/CommentComposeBar/CommentComposeBar.tsx b/src/v4/social/internal-components/CommentComposeBar/CommentComposeBar.tsx index 2803fab43..9a19fc1d2 100644 --- a/src/v4/social/internal-components/CommentComposeBar/CommentComposeBar.tsx +++ b/src/v4/social/internal-components/CommentComposeBar/CommentComposeBar.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import useUser from '~/core/hooks/useUser'; +import { useUser } from '~/v4/core/hooks/objects/useUser'; import useMention from '~/v4/chat/hooks/useMention'; import { Mentionees, Metadata } from '~/v4/helpers/utils'; @@ -36,7 +36,7 @@ export const CommentComposeBar = ({ targetType, }: CommentComposeBarProps) => { const { currentUserId } = useSDK(); - const user = useUser(currentUserId); + const { user } = useUser({ userId: currentUserId }); const avatarFileUrl = useImage({ fileId: user?.avatarFileId, imageSize: 'small' }); const { text, markup, mentions, mentionees, metadata, onChange, clearAll, queryMentionees } = useMention({ diff --git a/src/v4/social/internal-components/CommunityCategories/CommunityCategories.module.css b/src/v4/social/internal-components/CommunityCategories/CommunityCategories.module.css new file mode 100644 index 000000000..814b625a7 --- /dev/null +++ b/src/v4/social/internal-components/CommunityCategories/CommunityCategories.module.css @@ -0,0 +1,13 @@ +.communityCategories { + display: flex; + flex-wrap: nowrap; + gap: 0.25rem; +} + +.communityCategories__categoryChip { + width: unset; +} + +.communityCategories__categoryOverflow { + width: min-content; +} diff --git a/src/v4/social/internal-components/CommunityCategories/CommunityCategories.tsx b/src/v4/social/internal-components/CommunityCategories/CommunityCategories.tsx new file mode 100644 index 000000000..94db4eaa2 --- /dev/null +++ b/src/v4/social/internal-components/CommunityCategories/CommunityCategories.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import useCategoriesByIds from '~/v4/social/hooks/useCategoriesByIds'; + +import styles from './CommunityCategories.module.css'; +import { CommunityCategory } from '~/v4/social/elements/CommunityCategory/CommunityCategory'; +import clsx from 'clsx'; + +export const CommunityCategories = ({ + community, + pageId = '*', + componentId = '*', + onClick, + minCategoryCharacters, + maxCategoryCharacters, + maxCategoriesLength, + truncate = false, +}: { + community: Amity.Community; + pageId?: string; + componentId?: string; + onClick?: (categoryId: string) => void; + minCategoryCharacters?: number; + maxCategoryCharacters?: number; + maxCategoriesLength?: number; + truncate?: boolean; +}) => { + const categories = useCategoriesByIds(community.categoryIds); + + const overflowCategoriesLength = + typeof maxCategoriesLength === 'number' ? categories.length - maxCategoriesLength : 0; + + const categoriesLength = + typeof maxCategoriesLength === 'number' + ? Math.min(categories.length, maxCategoriesLength) + : categories.length; + + return ( + <div + className={styles.communityCategories} + style={ + { + '--asc-community-categories-length': + overflowCategoriesLength > 0 ? categoriesLength + 1 : categoriesLength, + } as React.CSSProperties + } + > + {categories.slice(0, categoriesLength).map((category) => ( + <CommunityCategory + key={category.categoryId} + pageId={pageId} + componentId={componentId} + categoryName={category.name} + minCharacters={minCategoryCharacters} + maxCharacters={maxCategoryCharacters} + truncate={truncate} + className={styles.communityCategories__categoryChip} + onClick={() => onClick?.(category.categoryId)} + /> + ))} + {overflowCategoriesLength > 0 && ( + <CommunityCategory + pageId={pageId} + componentId={componentId} + categoryName={`+${overflowCategoriesLength}`} + minCharacters={`+${overflowCategoriesLength}`.length} + className={clsx( + styles.communityCategories__categoryChip, + styles.communityCategories__categoryOverflow, + )} + /> + )} + </div> + ); +}; diff --git a/src/v4/social/internal-components/CommunityCategories/index.ts b/src/v4/social/internal-components/CommunityCategories/index.ts new file mode 100644 index 000000000..9941b843e --- /dev/null +++ b/src/v4/social/internal-components/CommunityCategories/index.ts @@ -0,0 +1 @@ +export { CommunityCategories } from './CommunityCategories'; diff --git a/src/v4/social/internal-components/CommunityMember/CommunityMember.module.css b/src/v4/social/internal-components/CommunityMember/CommunityMember.module.css index 9425aebbf..af4784bdc 100644 --- a/src/v4/social/internal-components/CommunityMember/CommunityMember.module.css +++ b/src/v4/social/internal-components/CommunityMember/CommunityMember.module.css @@ -18,6 +18,14 @@ height: 2.5rem; } +.communityMember__rightPane { + display: grid; + grid-template-columns: auto 1.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; +} + .communityMember__displayName { font-size: 1rem; display: block; @@ -26,3 +34,8 @@ overflow: hidden; color: var(--asc-color-base-default); } + +.communityMember__brandIcon { + width: 1.5rem; + height: 1.5rem; +} diff --git a/src/v4/social/internal-components/CommunityMember/CommunityMember.tsx b/src/v4/social/internal-components/CommunityMember/CommunityMember.tsx index c01cfe9ae..8cfe080e8 100644 --- a/src/v4/social/internal-components/CommunityMember/CommunityMember.tsx +++ b/src/v4/social/internal-components/CommunityMember/CommunityMember.tsx @@ -2,8 +2,11 @@ import React from 'react'; import styles from './CommunityMember.module.css'; import { UserAvatar } from '~/v4/social/internal-components/UserAvatar'; import { MentionTypeaheadOption } from '~/v4/social/internal-components/MentionTextInput/MentionTextInput'; +import { BrandBadge } from '~/v4/social/internal-components/BrandBadge'; interface CommunityMemberProps { + pageId?: string; + componentId?: string; isSelected: boolean; onClick: () => void; onMouseEnter: () => void; @@ -11,6 +14,8 @@ interface CommunityMemberProps { } export function CommunityMember({ + pageId = '*', + componentId = '*', isSelected, onClick, onMouseEnter, @@ -35,11 +40,18 @@ export function CommunityMember({ <div key={option.key} className={styles.communityMember__item}> <div> <UserAvatar + pageId={pageId} + componentId={componentId} className={styles.communityMember__avatar} userId={option.user.avatarFileId} /> </div> - <p className={styles.communityMember__displayName}>{option.user.displayName}</p> + <div className={styles.communityMember__rightPane}> + <p className={styles.communityMember__displayName}>{option.user.displayName}</p> + {option.user.isBrand ? ( + <BrandBadge className={styles.communityMember__brandIcon} /> + ) : null} + </div> </div> </div> ); diff --git a/src/v4/social/internal-components/CommunityRowItem/CommunityRowItem.module.css b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItem.module.css new file mode 100644 index 000000000..a346e87ec --- /dev/null +++ b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItem.module.css @@ -0,0 +1,93 @@ +.communityRowItem { + display: grid; + grid-template-columns: [image-start] 5rem [image-end content-start] 1fr 1fr 1fr 1fr 1fr 1fr [content-end]; + grid-template-rows: + [name-start] auto + [name-end member-start] auto + [member-end]; + gap: 0.75rem; + width: 100%; +} + +.communityRowItem[data-has-categories='true'] { + grid-template-rows: + [name-start] auto + [name-end cat-start] auto + [cat-end member-start] auto + [member-end]; +} + +.communityRowItem__image { + grid-column: image-start / image-end; + grid-row: name-start / member-end; + place-self: center; + height: 5rem; + width: 5rem; + border-radius: var(--asc-border-radius-sm); + position: relative; +} + +.communityRowItem__order { + position: absolute; + left: 0.5rem; + bottom: 0.37rem; + color: var(--asc-color-white); +} + +.communityRowItem__content { + display: grid; + grid-column: content-start / content-end; + grid-row: name-start / member-end; + grid-template-columns: subgrid [sub-a] [sub-b] [sub-c] [sub-d] [sub-e] [sub-f] [sub-g]; + grid-template-rows: subgrid; + width: 100%; + padding: 0.62rem; + justify-content: space-evenly; + gap: 0.5rem; +} + +.communityRowItem__communityName { + grid-column: sub-a / sub-g; + grid-row: name-start / name-end; + display: flex; + justify-content: start; + align-items: center; + gap: 0.25rem; + width: 100%; +} + +.communityRowItem__categories { + grid-column: sub-a / sub-g; + grid-row: cat-start / cat-end; +} + +.communityRowItem__categories[data-show-join-button='true'] { + grid-column: sub-a / sub-e; + grid-row: cat-start / cat-end; +} + +.communityRowItem__member { + grid-column: sub-a / sub-e; + grid-row: member-start / member-end; +} + +.communityRowItem__joinButton { + grid-row: name-start / member-end; + grid-column: sub-e / sub-g; + width: 4rem; + place-self: end end; +} + +.communityRowItem__joinButton[data-has-categories='true'] { + grid-row: cat-start / member-end; +} + +.communityRowItem__communityName__private { + width: 1.25rem; + height: 1.25rem; + display: flex; + justify-content: center; + align-items: center; + padding-top: 0.22rem; + padding-bottom: 0.28rem; +} diff --git a/src/v4/social/internal-components/CommunityRowItem/CommunityRowItem.tsx b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItem.tsx new file mode 100644 index 000000000..9aab70ca6 --- /dev/null +++ b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItem.tsx @@ -0,0 +1,142 @@ +import React from 'react'; + +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { CommunityJoinButton } from '~/v4/social/elements/CommunityJoinButton/CommunityJoinButton'; +import { CommunityMembersCount } from '~/v4/social/elements/CommunityMembersCount/CommunityMembersCount'; +import { CommunityCategories } from '~/v4/social/internal-components/CommunityCategories/CommunityCategories'; +import { CommunityPrivateBadge } from '~/v4/social/elements/CommunityPrivateBadge/CommunityPrivateBadge'; +import { CommunityDisplayName } from '~/v4/social/elements/CommunityDisplayName/CommunityDisplayName'; +import { CommunityOfficialBadge } from '~/v4/social/elements/CommunityOfficialBadge/CommunityOfficialBadge'; +import { Typography } from '~/v4/core/components'; +import { CommunityRowImage } from '~/v4/social/elements/CommunityRowImage/CommunityRowImage'; +import { useImage } from '~/v4/core/hooks/useImage'; +import { CommunityJoinedButton } from '~/v4/social/elements/CommunityJoinedButton/CommunityJoinedButton'; + +import styles from './CommunityRowItem.module.css'; +import { ClickableArea } from '~/v4/core/natives/ClickableArea/ClickableArea'; + +type CommunityRowItemProps<TShowJoinButton extends boolean | undefined> = { + community: Amity.Community; + pageId?: string; + componentId?: string; + elementId?: string; + key?: string; + order?: number; + minCategoryCharacters?: number; + maxCategoryCharacters?: number; + maxCategoriesLength?: number; + showJoinButton?: TShowJoinButton; + onClick: (communityId: string) => void; + onCategoryClick: (categoryId: string) => void; +} & (TShowJoinButton extends true + ? { + onJoinButtonClick: (communityId: string) => void; + onLeaveButtonClick: (communityId: string) => void; + } + : { + onJoinButtonClick?: undefined | null; + onLeaveButtonClick?: undefined | null; + }); + +const formatOrder = (order: number) => { + if (order < 10) { + return `0${order}`; + } + return `${order}`; +}; + +export const CommunityRowItem = <T extends boolean | undefined>({ + key, + pageId = '*', + componentId = '*', + elementId = '*', + community, + order, + showJoinButton, + minCategoryCharacters, + maxCategoryCharacters, + maxCategoriesLength, + onJoinButtonClick, + onLeaveButtonClick, + onClick, + onCategoryClick, +}: CommunityRowItemProps<T>) => { + const { themeStyles } = useAmityElement({ pageId, componentId, elementId }); + + const avatarUrl = useImage({ fileId: community.avatarFileId, imageSize: 'medium' }); + + return ( + <ClickableArea + key={key} + elementType="div" + className={styles.communityRowItem} + onPress={() => onClick(community.communityId)} + data-has-categories={community.categoryIds.length > 0} + style={themeStyles} + > + <div className={styles.communityRowItem__image}> + <CommunityRowImage pageId={pageId} componentId={componentId} imgSrc={avatarUrl} /> + {typeof order === 'number' ? ( + <Typography.BodyBold className={styles.communityRowItem__order}> + {formatOrder(order)} + </Typography.BodyBold> + ) : null} + </div> + <div className={styles.communityRowItem__content}> + <div className={styles.communityRowItem__communityName}> + {!community.isPublic && ( + <div className={styles.communityRowItem__communityName__private}> + <CommunityPrivateBadge pageId={pageId} componentId={componentId} /> + </div> + )} + <CommunityDisplayName pageId={pageId} componentId={componentId} community={community} /> + {community.isOfficial && ( + <CommunityOfficialBadge pageId={pageId} componentId={componentId} /> + )} + </div> + + <div + className={styles.communityRowItem__categories} + data-show-join-button={showJoinButton === true} + > + <CommunityCategories + pageId={pageId} + componentId={componentId} + community={community} + maxCategoriesLength={maxCategoriesLength} + onClick={onCategoryClick} + minCategoryCharacters={minCategoryCharacters} + maxCategoryCharacters={maxCategoryCharacters} + truncate + /> + </div> + + <div className={styles.communityRowItem__member}> + <CommunityMembersCount + pageId={pageId} + componentId={componentId} + memberCount={community.membersCount} + /> + </div> + {showJoinButton === true && + (community.isJoined ? ( + <CommunityJoinedButton + pageId={pageId} + componentId={componentId} + className={styles.communityRowItem__joinButton} + data-has-categories={community.categoryIds.length > 0} + onClick={() => onLeaveButtonClick?.(community.communityId)} + /> + ) : ( + <CommunityJoinButton + pageId={pageId} + componentId={componentId} + className={styles.communityRowItem__joinButton} + data-has-categories={community.categoryIds.length > 0} + onClick={() => onJoinButtonClick?.(community.communityId)} + /> + ))} + </div> + </ClickableArea> + ); +}; diff --git a/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider.module.css b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider.module.css new file mode 100644 index 000000000..9d4cd89d1 --- /dev/null +++ b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider.module.css @@ -0,0 +1,12 @@ +.communityRowItemDivider { + background-color: var(--asc-color-base-shade4); + height: 0.0625rem; + width: 100%; + margin-top: 0.75rem; + margin-bottom: 0.75rem; +} + +.communityRowItemDivider:first-child { + all: unset; + margin-top: 0.75rem; +} diff --git a/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider.tsx b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider.tsx new file mode 100644 index 000000000..00abdbff2 --- /dev/null +++ b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +import styles from './CommunityRowItemDivider.module.css'; + +export const CommunityRowItemDivider = () => <div className={styles.communityRowItemDivider} />; diff --git a/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton.module.css b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton.module.css new file mode 100644 index 000000000..2fa55fa12 --- /dev/null +++ b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton.module.css @@ -0,0 +1,53 @@ +.communityRowItemSkeleton { + width: 100%; + height: 5rem; + border-radius: 0.75rem; + display: inline-flex; + gap: 1rem; +} + +.communityRowItemSkeleton__avatar { + width: 5rem; + height: 5rem; + border-radius: var(--asc-border-radius-sm); + background-color: var(--asc-color-base-shade4); +} + +.communityRowItemSkeleton__content { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.75rem; +} + +.communityRowItemSkeleton__contentBar1 { + border-radius: 0.75rem; + background-color: var(--asc-color-base-shade4); + width: 12.25rem; + height: 0.75rem; +} + +.communityRowItemSkeleton__contentBar2 { + border-radius: 0.75rem; + background-color: var(--asc-color-base-shade4); + width: 5.8008rem; + height: 0.625rem; +} + +.communityRowItemSkeleton__animation { + animation: skeleton-pulse 1.5s ease-in-out infinite; +} + +@keyframes skeleton-pulse { + 0% { + opacity: 0.6; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0.6; + } +} diff --git a/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton.tsx b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton.tsx new file mode 100644 index 000000000..40991a43b --- /dev/null +++ b/src/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton.tsx @@ -0,0 +1,48 @@ +import clsx from 'clsx'; +import React from 'react'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; + +import styles from './CommunityRowItemSkeleton.module.css'; + +interface CommunityRowItemSkeleton { + pageId?: string; + componentId?: string; + elementId?: string; +} + +export const CommunityRowItemSkeleton = ({ + pageId = '*', + componentId = '*', + elementId = '*', +}: CommunityRowItemSkeleton) => { + const { themeStyles } = useAmityElement({ pageId, componentId, elementId }); + return ( + <div className={styles.communityRowItemSkeleton} style={themeStyles}> + <div + className={clsx( + styles.communityRowItemSkeleton__avatar, + styles.communityRowItemSkeleton__animation, + )} + /> + <div + className={clsx( + styles.communityRowItemSkeleton__content, + styles.communityRowItemSkeleton__animation, + )} + > + <div + className={clsx( + styles.communityRowItemSkeleton__contentBar1, + styles.communityRowItemSkeleton__animation, + )} + /> + <div + className={clsx( + styles.communityRowItemSkeleton__contentBar2, + styles.communityRowItemSkeleton__animation, + )} + /> + </div> + </div> + ); +}; diff --git a/src/v4/social/internal-components/CommunityRowItem/index.tsx b/src/v4/social/internal-components/CommunityRowItem/index.tsx new file mode 100644 index 000000000..05cd18738 --- /dev/null +++ b/src/v4/social/internal-components/CommunityRowItem/index.tsx @@ -0,0 +1 @@ +export { CommunityRowItem } from './CommunityRowItem'; diff --git a/src/v4/social/internal-components/EditPost/EditPost.tsx b/src/v4/social/internal-components/EditPost/EditPost.tsx index 3ef72f983..7d6353ffc 100644 --- a/src/v4/social/internal-components/EditPost/EditPost.tsx +++ b/src/v4/social/internal-components/EditPost/EditPost.tsx @@ -174,7 +174,7 @@ export function EditPost({ post }: AmityPostComposerEditOptions) { }} /> - <Thumbnail postMedia={postImages} onRemove={handleRemoveThumbnailImage} /> + <Thumbnail pageId={pageId} postMedia={postImages} onRemove={handleRemoveThumbnailImage} /> <Thumbnail postMedia={postVideos} onRemove={handleRemoveThumbnailVideo} /> <div ref={mentionRef} className={styles.mentionTextInput_item} /> diff --git a/src/v4/social/internal-components/EditPost/Thumbnail.tsx b/src/v4/social/internal-components/EditPost/Thumbnail.tsx index c83a8db82..076ffa1e8 100644 --- a/src/v4/social/internal-components/EditPost/Thumbnail.tsx +++ b/src/v4/social/internal-components/EditPost/Thumbnail.tsx @@ -44,9 +44,11 @@ const MediaComponent = ({ }; export const Thumbnail = ({ + pageId = '*', postMedia, onRemove, }: { + pageId?: string; postMedia: Amity.Post<'image' | 'video'>[]; onRemove: (fileId: string) => void; }) => { @@ -61,6 +63,7 @@ export const Thumbnail = ({ className={styles.thumbnail__wrapper} > <MediaComponent + pageId={pageId} type={file.dataType} key={index} fieldId={ diff --git a/src/v4/social/internal-components/Lexical/MentionItem.module.css b/src/v4/social/internal-components/Lexical/MentionItem.module.css index a060ebb2a..fdd8d4a9c 100644 --- a/src/v4/social/internal-components/Lexical/MentionItem.module.css +++ b/src/v4/social/internal-components/Lexical/MentionItem.module.css @@ -47,3 +47,16 @@ overflow: hidden; color: var(--asc-color-base-default); } + +.userMentionItem__rightPane { + display: grid; + grid-template-columns: auto 1.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; +} + +.userMentionItem__brandIcon { + width: 1.5rem; + height: 1.5rem; +} diff --git a/src/v4/social/internal-components/Lexical/MentionItem.tsx b/src/v4/social/internal-components/Lexical/MentionItem.tsx index 98059c869..9cd28181b 100644 --- a/src/v4/social/internal-components/Lexical/MentionItem.tsx +++ b/src/v4/social/internal-components/Lexical/MentionItem.tsx @@ -4,15 +4,30 @@ import { MentionTypeaheadOption } from './plugins/MentionPlugin'; import { MentionData } from './utils'; import styles from './MentionItem.module.css'; +import { BrandBadge } from '~/v4/social/internal-components/BrandBadge'; +import { useUser } from '~/v4/core/hooks/objects/useUser'; interface MentionItemProps { + pageId?: string; + componentId?: string; isSelected: boolean; onClick: () => void; onMouseEnter: () => void; option: MentionTypeaheadOption<MentionData>; } -export function MentionItem({ option, isSelected, onClick, onMouseEnter }: MentionItemProps) { +export function MentionItem({ + pageId = '*', + componentId = '*', + option, + isSelected, + onClick, + onMouseEnter, +}: MentionItemProps) { + const { user } = useUser({ + userId: option.data.userId, + }); + return ( <li key={option.key} @@ -26,9 +41,17 @@ export function MentionItem({ option, isSelected, onClick, onMouseEnter }: Menti onClick={onClick} > <div> - <UserAvatar className={styles.userMentionItem__avatar} userId={option.data.userId} /> + <UserAvatar + pageId={pageId} + componentId={componentId} + className={styles.userMentionItem__avatar} + userId={option.data.userId} + /> + </div> + <div className={styles.userMentionItem__rightPane}> + <p className={styles.userMentionItem__displayName}>{user?.displayName}</p> + {user?.isBrand ? <BrandBadge className={styles.userMentionItem__brandIcon} /> : null} </div> - <p className={styles.userMentionItem__displayName}>{option.data.displayName}</p> </li> ); } diff --git a/src/v4/social/internal-components/Lexical/__snapshots__/utils.test.ts.snap b/src/v4/social/internal-components/Lexical/__snapshots__/utils.test.ts.snap new file mode 100644 index 000000000..12666249b --- /dev/null +++ b/src/v4/social/internal-components/Lexical/__snapshots__/utils.test.ts.snap @@ -0,0 +1,292 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`v3 should return a same object 1`] = ` +{ + "root": { + "children": [ + { + "children": [ + { + "data": { + "displayName": "Web-Test", + "userId": "Web-Test", + }, + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "@Web-Test", + "type": "mention", + "version": 1, + }, + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": " hello ", + "type": "text", + "version": 1, + }, + { + "data": { + "displayName": "FonTS", + "userId": "FonTS", + }, + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "@FonTS", + "type": "mention", + "version": 1, + }, + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": " ", + "type": "text", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "textFormat": 0, + "textStyle": "", + "type": "paragraph", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "type": "root", + "version": 1, + }, +} +`; + +exports[`v4 should return a same object 1`] = ` +{ + "root": { + "children": [ + { + "children": [ + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "1 ", + "type": "text", + "version": 1, + }, + { + "data": { + "displayName": " android2", + "userId": " android2", + }, + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "@ android2", + "type": "mention", + "version": 1, + }, + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": " nstaehunstah ", + "type": "text", + "version": 1, + }, + { + "data": { + "displayName": " android2", + "userId": " android2", + }, + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "@ android2", + "type": "mention", + "version": 1, + }, + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": " ", + "type": "text", + "version": 1, + }, + { + "data": { + "displayName": "Web-Test", + "userId": "Web-Test", + }, + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "@Web-Test", + "type": "mention", + "version": 1, + }, + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": " aenotuhnasetouhnsateohunsatheunst sntaehuns", + "type": "text", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "textFormat": 0, + "textStyle": "", + "type": "paragraph", + "version": 1, + }, + { + "children": [ + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "STnseatu ", + "type": "text", + "version": 1, + }, + { + "data": { + "displayName": " android3", + "userId": " android3", + }, + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "@ android3", + "type": "mention", + "version": 1, + }, + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": " ", + "type": "text", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "textFormat": 0, + "textStyle": "", + "type": "paragraph", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "type": "root", + "version": 1, + }, +} +`; + +exports[`v4 should return a same object 2`] = ` +{ + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "www.google.com", + "type": "text", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "rel": null, + "target": null, + "title": null, + "type": "link", + "url": "http://www.google.com", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "textFormat": 0, + "textStyle": "", + "type": "paragraph", + "version": 1, + }, + { + "children": [ + { + "children": [ + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "www.youtube.com", + "type": "text", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "rel": null, + "target": null, + "title": null, + "type": "link", + "url": "http://www.youtube.com", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "textFormat": 0, + "textStyle": "", + "type": "paragraph", + "version": 1, + }, + ], + "direction": "ltr", + "format": "", + "indent": 0, + "type": "root", + "version": 1, + }, +} +`; diff --git a/src/v4/social/internal-components/Lexical/utils.test.ts b/src/v4/social/internal-components/Lexical/utils.test.ts new file mode 100644 index 000000000..3509ff1ec --- /dev/null +++ b/src/v4/social/internal-components/Lexical/utils.test.ts @@ -0,0 +1,97 @@ +import { Metadata, Mentionees } from '~/v4/helpers/utils'; +import { editorStateToText, textToEditorState } from './utils'; + +type Input = { + data: { + text: string; + }; + metadata: Metadata; + mentionees: Mentionees; +}; + +const inputsV3: Input[] = [ + { + data: { + text: '@Web-Test hello @FonTS ', + }, + metadata: { + mentioned: [ + { + index: 0, + length: 8, + type: 'user', + userId: 'Web-Test', + }, + { + index: 16, + length: 5, + type: 'user', + userId: 'FonTS', + }, + ], + }, + mentionees: [ + { + type: 'user', + userIds: ['Web-Test', 'FonTS'], + }, + ], + }, +]; + +const inputsV4: Input[] = [ + { + data: { + text: '1 @ android2 nstaehunstah @ android2 @Web-Test aenotuhnasetouhnsateohunsatheunst sntaehuns\nSTnseatu @ android3 ', + }, + metadata: { + mentioned: [ + { index: 2, length: 9, type: 'user', userId: ' android2', displayName: ' android2' }, + { index: 26, length: 9, type: 'user', userId: ' android2', displayName: ' android2' }, + { index: 37, length: 8, type: 'user', userId: 'Web-Test', displayName: 'Web-Test' }, + { index: 100, length: 9, type: 'user', userId: ' android3', displayName: ' android3' }, + ], + }, + mentionees: [{ type: 'user', userIds: [' android2', ' android2', 'Web-Test', ' android3'] }], + }, + { + data: { + text: 'www.google.com\nwww.youtube.com', + }, + metadata: { + mentioned: [], + }, + mentionees: [], + }, +]; + +describe('v3', () => { + test.each(inputsV3)('should return a same object', (input) => { + const editorState = textToEditorState(input); + + expect(editorState).toMatchSnapshot(); + + const actual = editorStateToText(editorState); + expect(actual.text).toEqual(input.data.text); + const expectedMentioned = (input.metadata.mentioned || []).map((m) => ({ + ...m, + displayName: m.userId, + })); + expect(actual.mentioned).toEqual(expectedMentioned); + expect(actual.mentionees).toEqual(input.mentionees); + }); +}); + +describe('v4', () => { + test.each(inputsV4)('should return a same object', (input) => { + const editorState = textToEditorState(input); + + expect(editorState).toMatchSnapshot(); + + const actual = editorStateToText(editorState); + expect(actual.text).toEqual(input.data.text); + const expectedMentioned = actual.mentioned.map(({ ...m }) => m); + expect(expectedMentioned).toEqual(input.metadata.mentioned); + expect(actual.mentionees).toEqual(input.mentionees); + }); +}); diff --git a/src/v4/social/internal-components/Lexical/utils.ts b/src/v4/social/internal-components/Lexical/utils.ts index 8c37f7b4d..20aaa20c4 100644 --- a/src/v4/social/internal-components/Lexical/utils.ts +++ b/src/v4/social/internal-components/Lexical/utils.ts @@ -1,10 +1,11 @@ -import { SerializedAutoLinkNode } from '@lexical/link'; +import { SerializedAutoLinkNode, SerializedLinkNode } from '@lexical/link'; import { InitialConfigType, InitialEditorStateType } from '@lexical/react/LexicalComposer'; import { EditorThemeClasses, Klass, LexicalEditor, LexicalNode, + SerializedEditorState, SerializedLexicalNode, SerializedParagraphNode, SerializedRootNode, @@ -12,6 +13,7 @@ import { } from 'lexical'; import { Mentioned, Mentionees } from '~/v4/helpers/utils'; import { SerializedMentionNode } from './nodes/MentionNode'; +import * as linkify from 'linkifyjs'; export interface EditorStateJson extends SerializedLexicalNode { children: []; @@ -33,6 +35,10 @@ export function $isSerializedAutoLinkNode( return node.type === 'autolink'; } +export function $isSerializedLinkNode(node: SerializedLexicalNode): node is SerializedLinkNode { + return node.type === 'link'; +} + export type MentionData = { userId: string; displayName?: string; @@ -51,6 +57,7 @@ function createRootNode(): SerializedRootNode<SerializedParagraphNode> { function createParagraphNode(): SerializedParagraphNode { return { + textStyle: '', children: [], direction: 'ltr', format: '', @@ -73,22 +80,58 @@ function createSerializeTextNode(text: string): SerializedTextNode { }; } -function createSerializeMentionNode(mention: Mentioned): SerializedMentionNode<MentionData> { +function createSerializeMentionNode({ + text, + mention, +}: { + text: string; + mention: Mentioned; +}): SerializedMentionNode<MentionData> { return { detail: 0, format: 0, mode: 'normal', style: '', - text: ('@' + mention.userId) as string, + text: text.substring(mention.index, mention.index + mention.length + 1), type: 'mention', version: 1, data: { - displayName: mention.userId as string, + displayName: mention.displayName || (mention.userId as string), userId: mention.userId as string, }, }; } +function createSerializeLinkNode({ + url, + title, +}: { + url: string; + title: string; +}): SerializedLinkNode { + return { + children: [createSerializeTextNode(title)], + format: '', + direction: 'ltr', + indent: 0, + type: 'link', + version: 1, + url, + rel: null, + target: null, + title: null, + }; +} + +type LinkData = Mentioned & { + href: string; + value: string; +}; + +function isLinkData(data: Mentioned | LinkData): data is LinkData { + return (data as LinkData)?.type === 'url'; +} + export function textToEditorState(value: { data: { text: string }; metadata?: { @@ -100,27 +143,76 @@ export function textToEditorState(value: { const textArray = value.data.text.split('\n'); - const mentions = value.metadata?.mentioned || []; + const mentions = value.metadata?.mentioned ?? []; - let mentionIndex = 0; + const links: Array<LinkData> = linkify + .find(value.data.text) + .filter((link) => link.type === 'url') + .map((link) => ({ + index: link.start, + length: link.end - link.start + 1, + href: link.href, + value: link.value, + type: 'url', + })); + + const indexMap: Record<number, boolean> = {}; + + const mentionsAndLinks = [...mentions, ...links] + .sort((a, b) => a.index - b.index) + .reduce((acc, mentionAndLink) => { + // this function is used to remove duplicate mentions and links that cause an infinite loop + if (indexMap[mentionAndLink.index]) { + return acc; + } + + indexMap[mentionAndLink.index] = true; + return [...acc, mentionAndLink]; + }, [] as Mentioned[]); + + let mentionAndLinkIndex = 0; let globalIndex = 0; for (let i = 0; i < textArray.length; i += 1) { const paragraph = createParagraphNode(); let runningIndex = 0; - while (runningIndex < textArray[i].length) { - if (mentionIndex < mentions.length && mentions[mentionIndex].index === globalIndex) { - paragraph.children.push(createSerializeMentionNode(mentions[mentionIndex])); - runningIndex += mentions[mentionIndex].length; - globalIndex += mentions[mentionIndex].length; - mentionIndex += 1; + const currentLine = textArray[i]; + + while (runningIndex < currentLine.length) { + const mentionOrLink = mentionsAndLinks[mentionAndLinkIndex]; + + if (mentionAndLinkIndex < mentionsAndLinks.length && mentionOrLink.index === globalIndex) { + const mentionOrLink = mentionsAndLinks[mentionAndLinkIndex]; + + if (isLinkData(mentionOrLink)) { + paragraph.children.push( + createSerializeLinkNode({ + title: mentionOrLink.value, + url: mentionOrLink.href, + }), + ); + runningIndex += mentionOrLink.value.length; + globalIndex += mentionOrLink.value.length; + } else { + paragraph.children.push( + createSerializeMentionNode({ + text: value.data.text, + mention: mentionOrLink, + }), + ); + runningIndex += mentionOrLink.length + 1; + globalIndex += mentionOrLink.length + 1; + } + + mentionAndLinkIndex += 1; } else { const nextMentionIndex = - mentionIndex < mentions.length - ? mentions[mentionIndex].index - : globalIndex + textArray[i].length; - const textSegment = textArray[i].slice( + mentionAndLinkIndex < mentionsAndLinks.length + ? mentionOrLink.index + : globalIndex + currentLine.length; + + const textSegment = currentLine.slice( runningIndex, nextMentionIndex - globalIndex + runningIndex, ); @@ -132,8 +224,8 @@ export function textToEditorState(value: { } } - if (runningIndex < textArray[i].length) { - const textSegment = textArray[i].slice(runningIndex); + if (runningIndex < currentLine.length) { + const textSegment = currentLine.slice(runningIndex); if (textSegment) { paragraph.children.push(createSerializeTextNode(textSegment)); } @@ -148,9 +240,14 @@ export function textToEditorState(value: { return { root: rootNode }; } -export function editorStateToText(editor: LexicalEditor) { +export function editorToText(editor: LexicalEditor) { + const editorState = editor.getEditorState().toJSON(); + return editorStateToText(editorState); +} + +export function editorStateToText(editorState: SerializedEditorState) { const editorStateTextString: string[] = []; - const paragraphs = editor.getEditorState().toJSON().root.children as EditorStateJson[]; + const paragraphs = editorState.root.children as EditorStateJson[]; const mentioned: Mentioned[] = []; let isChannelMentioned = false; @@ -167,7 +264,7 @@ export function editorStateToText(editor: LexicalEditor) { paragraphText.push(child.text); runningIndex += child.text.length; } - if ($isSerializedAutoLinkNode(child)) { + if ($isSerializedLinkNode(child) || $isSerializedAutoLinkNode(child)) { child.children.forEach((c) => { if (!$isSerializedTextNode(c)) return; paragraphText.push(c.text); @@ -176,6 +273,8 @@ export function editorStateToText(editor: LexicalEditor) { } if ($isSerializedMentionNode<MentionData>(child)) { + const isStartWithAtSign = child.text.charAt(0) === '@'; + if (child.data.userId === 'all') { mentioned.push({ index: runningIndex, @@ -184,11 +283,13 @@ export function editorStateToText(editor: LexicalEditor) { }); isChannelMentioned = true; } else { + const textLength = isStartWithAtSign ? child.text.length - 1 : child.text.length; mentioned.push({ index: runningIndex, - length: child.text.length, + length: textLength, type: 'user', userId: child.data.userId, + displayName: child.data.displayName, }); mentioneeUserIds.push(child.data.userId); } diff --git a/src/v4/social/internal-components/PostAd/UIPostAd.tsx b/src/v4/social/internal-components/PostAd/UIPostAd.tsx index 6dfb7c943..284431958 100644 --- a/src/v4/social/internal-components/PostAd/UIPostAd.tsx +++ b/src/v4/social/internal-components/PostAd/UIPostAd.tsx @@ -7,6 +7,7 @@ import { Button } from '~/v4/core/natives/Button'; import { AdInformation } from '~/v4/social/internal-components/AdInformation/AdInformation'; import styles from './UIPostAd.module.css'; +import clsx from 'clsx'; interface UIPostAdProps { ad: Amity.Ad; @@ -60,9 +61,13 @@ export const UIPostAd = ({ onPress={handleCallToActionClick} > <div className={styles.footer__left}> - <Typography.Body className={styles.footer__content__description}> - {ad.description} - </Typography.Body> + <Typography.Body + renderer={({ typoClassName }) => ( + <div className={clsx(typoClassName, styles.footer__content__description)}> + {ad.description} + </div> + )} + /> <Typography.BodyBold className={styles.footer__content__headline}> {ad.headline} </Typography.BodyBold> @@ -70,9 +75,13 @@ export const UIPostAd = ({ {ad.callToActionUrl ? ( <div className={styles.footer__right}> <Button className={styles.footer__content__button} onPress={handleCallToActionClick}> - <Typography.CaptionBold className={styles.footer__content__button__text}> - {ad.callToAction} - </Typography.CaptionBold> + <Typography.CaptionBold + renderer={({ typoClassName }) => ( + <div className={clsx(typoClassName, styles.footer__content__button__text)}> + {ad.callToAction} + </div> + )} + /> </Button> </div> ) : null} diff --git a/src/v4/social/internal-components/PostMenu/PostMenu.tsx b/src/v4/social/internal-components/PostMenu/PostMenu.tsx index 7ac2186a6..a4dfe9d80 100644 --- a/src/v4/social/internal-components/PostMenu/PostMenu.tsx +++ b/src/v4/social/internal-components/PostMenu/PostMenu.tsx @@ -165,6 +165,7 @@ export const PostMenu = ({ <div className={styles.postMenu}> {showReportPostButton && !isLoading ? ( <Button + data-qa-anchor={`${pageId}/${componentId}/report_post_button`} className={styles.postMenu__item} onPress={() => { if (isFlaggedByMe) { diff --git a/src/v4/social/internal-components/StoryCommentComposeBar/StoryCommentComposeBar.tsx b/src/v4/social/internal-components/StoryCommentComposeBar/StoryCommentComposeBar.tsx index 12e39766f..9784a3452 100644 --- a/src/v4/social/internal-components/StoryCommentComposeBar/StoryCommentComposeBar.tsx +++ b/src/v4/social/internal-components/StoryCommentComposeBar/StoryCommentComposeBar.tsx @@ -1,6 +1,5 @@ import { CommentRepository } from '@amityco/ts-sdk'; import React from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; import { Mentionees, Metadata } from '~/v4/helpers/utils'; import { Close, Lock2Icon } from '~/icons'; import { CommentComposeBar } from '~/v4/social/internal-components/CommentComposeBar'; @@ -27,8 +26,6 @@ export const StoryCommentComposeBar = ({ onCancelReply, referenceId, }: StoryCommentComposeBarProps) => { - const { formatMessage } = useIntl(); - const handleAddComment = async ( commentText: string, mentionees: Mentionees, @@ -68,7 +65,7 @@ export const StoryCommentComposeBar = ({ return ( <div className={styles.disabledCommentComposerBarContainer}> <Lock2Icon /> - {formatMessage({ id: 'storyViewer.commentSheet.disabled' })} + Comments are disabled for this story </div> ); } @@ -78,7 +75,7 @@ export const StoryCommentComposeBar = ({ {isReplying && ( <div className={styles.replyingBlock}> <div className={styles.replyingToText}> - <FormattedMessage id="storyViewer.commentSheet.replyingTo" />{' '} + {'Replying to '} <span className={styles.replyingToUsername}>{replyTo?.userId}</span> </div> <Close onClick={onCancelReply} className={styles.closeButton} /> @@ -86,12 +83,14 @@ export const StoryCommentComposeBar = ({ )} {!isReplying ? ( <CommentComposeBar + targetType="story" targetId={communityId} onSubmit={(text, mentionees, metadata) => handleAddComment(text, mentionees, metadata)} /> ) : ( <CommentComposeBar targetId={communityId} + targetType="story" userToReply={replyTo?.userId} onSubmit={(replyText, mentionees, metadata) => { handleReplyToComment(replyText, mentionees, metadata); diff --git a/src/v4/social/internal-components/TabsBar/TabsBar.module.css b/src/v4/social/internal-components/TabsBar/TabsBar.module.css index 76579fd01..d40698df5 100644 --- a/src/v4/social/internal-components/TabsBar/TabsBar.module.css +++ b/src/v4/social/internal-components/TabsBar/TabsBar.module.css @@ -1,10 +1,3 @@ -/* TODO: remove this block */ -button, -fieldset, -input { - all: unset; -} - .tabsRoot { display: flex; flex-direction: column; @@ -19,6 +12,7 @@ input { } .tabsTrigger { + all: unset; height: 3rem; color: var(--asc-color-base-shade3); } diff --git a/src/v4/social/internal-components/TextWithMention/TextWithMention.module.css b/src/v4/social/internal-components/TextWithMention/TextWithMention.module.css index 7d43d48c7..9bbdb3bef 100644 --- a/src/v4/social/internal-components/TextWithMention/TextWithMention.module.css +++ b/src/v4/social/internal-components/TextWithMention/TextWithMention.module.css @@ -18,6 +18,12 @@ max-height: max-content; } +.textWithMention__paragraph { + height: 100%; + margin: 0; + padding: 0; +} + .textWithMention__fullContent { visibility: hidden; position: relative; @@ -43,6 +49,13 @@ display: inline; } +.textWithMention__link { + display: inline; + color: var(--asc-color-primary-shade1); + text-decoration: none; + cursor: pointer; +} + .textWithMention__showMoreLess { padding-left: 0.2rem; color: var(--asc-color-primary-default); diff --git a/src/v4/social/internal-components/TextWithMention/TextWithMention.tsx b/src/v4/social/internal-components/TextWithMention/TextWithMention.tsx index abb21b98d..0ad4ab6a1 100644 --- a/src/v4/social/internal-components/TextWithMention/TextWithMention.tsx +++ b/src/v4/social/internal-components/TextWithMention/TextWithMention.tsx @@ -1,41 +1,71 @@ import { SerializedParagraphNode, SerializedTextNode } from 'lexical'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Typography } from '~/v4/core/components'; -import { Mentioned } from '~/v4/helpers/utils'; -import { TextToEditorState } from '~/v4/social/components/CommentComposer/CommentInput'; +import { Mentioned, Mentionees } from '~/v4/helpers/utils'; import styles from './TextWithMention.module.css'; -import { v4 } from 'uuid'; +import { + textToEditorState, + $isSerializedMentionNode, + $isSerializedAutoLinkNode, + $isSerializedTextNode, + MentionData, + $isSerializedLinkNode, +} from '~/v4/social/internal-components/Lexical/utils'; +import clsx from 'clsx'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { Button } from '~/v4/core/natives/Button/Button'; interface TextWithMentionProps { + pageId?: string; + componentId?: string; maxLines?: number; data: { text: string; }; - mentionees: Amity.UserMention[]; + mentionees: Mentionees; metadata?: { mentioned?: Mentioned[]; }; } -export const TextWithMention = ({ maxLines = 8, ...props }: TextWithMentionProps) => { +export const TextWithMention = ({ + pageId = '*', + componentId = '*', + maxLines = 8, + data, + mentionees, + metadata, +}: TextWithMentionProps) => { const [isExpanded, setIsExpanded] = useState(false); const [isClamped, setIsClamped] = useState(false); const [isHidden, setIsHidden] = useState(false); - const contentRef = useRef<HTMLDivElement>(null); const fullContentRef = useRef<HTMLDivElement>(null); - const editorState = useMemo(() => { - return TextToEditorState(props); - }, [props]); + const { goToUserProfilePage } = useNavigation(); + + const editorState = useMemo( + () => + textToEditorState({ + data, + mentionees, + metadata, + }), + [data, mentionees, metadata], + ); useEffect(() => { // check if should be clamped or not, then hide the full content const fullContentHeight = fullContentRef.current?.clientHeight || 0; + const rootFontSize = parseInt( + window.getComputedStyle(document.body).getPropertyValue('font-size'), + 10, + ); + const clampHeight = parseFloat( getComputedStyle(document.documentElement).getPropertyValue('--asc-line-height-md'), - ) * 16; + ) * rootFontSize; if (fullContentHeight > clampHeight * maxLines) { setIsClamped(true); @@ -44,21 +74,40 @@ export const TextWithMention = ({ maxLines = 8, ...props }: TextWithMentionProps setIsHidden(true); }, []); - const renderText = (paragraph: SerializedParagraphNode) => { - return paragraph.children.map((text) => { - const uid = v4(); - if ((text as SerializedTextNode).type === 'mention') { + const renderText = (paragraph: SerializedParagraphNode, typoClassName: string) => { + return paragraph.children.map((child) => { + if ($isSerializedMentionNode<MentionData>(child)) { + return ( + <Button + data-qa-anchor={`${pageId}/${componentId}/mention`} + key={child.data.userId} + className={clsx(typoClassName, styles.textWithMention__mention)} + onPress={() => goToUserProfilePage(child.data.userId)} + > + {child.text} + </Button> + ); + } + if ($isSerializedAutoLinkNode(child) || $isSerializedLinkNode(child)) { return ( - <span key={uid} className={styles.textWithMention__mention}> - {(text as SerializedTextNode).text} - </span> + <a + key={child.url} + href={child.url} + className={clsx(typoClassName, styles.textWithMention__link)} + data-qa-anchor={`${pageId}/${componentId}/post_link`} + > + {$isSerializedTextNode(child.children[0]) ? child.children[0]?.text : child.url} + </a> ); } - return ( - <span key={uid} className={styles.textWithMention__text}> - {(text as SerializedTextNode).text} - </span> - ); + + if ($isSerializedTextNode(child)) { + return ( + <span className={clsx(typoClassName, styles.textWithMention__text)}>{child.text}</span> + ); + } + + return null; }); }; @@ -69,13 +118,24 @@ export const TextWithMention = ({ maxLines = 8, ...props }: TextWithMentionProps className={styles.textWithMention__fullContent} data-hidden={isHidden} > - {editorState.root.children.map((child) => { - const uuid = v4(); - return <Typography.Body key={uuid}>{renderText(child)}</Typography.Body>; + {editorState.root.children.map((paragraph) => { + return ( + <p className={styles.textWithMention__paragraph}> + {paragraph.children.length > 0 ? ( + <Typography.Body + renderer={({ typoClassName }) => { + return <>{renderText(paragraph, typoClassName)}</>; + }} + /> + ) : ( + <br /> + )} + </p> + ); })} </div> <div - key={isExpanded ? 'expanded' : 'collapsed'} + data-qa-anchor={`${pageId}/${componentId}/text_with_mention`} data-expanded={isExpanded} className={styles.textWithMention__clamp} style={ @@ -89,9 +149,20 @@ export const TextWithMention = ({ maxLines = 8, ...props }: TextWithMentionProps <Typography.Body>...See more</Typography.Body> </div> )} - {editorState.root.children.map((child, index) => { - const uuid = v4(); - return <Typography.Body key={uuid}>{renderText(child)}</Typography.Body>; + {editorState.root.children.map((paragraph, index) => { + return ( + <p className={styles.textWithMention__paragraph} key={index}> + {paragraph.children.length > 0 ? ( + <Typography.Body + renderer={({ typoClassName }) => { + return <>{renderText(paragraph, typoClassName)}</>; + }} + /> + ) : ( + <br /> + )} + </p> + ); })} </div> </div> diff --git a/src/v4/social/internal-components/UserAvatar/UserAvatar.tsx b/src/v4/social/internal-components/UserAvatar/UserAvatar.tsx index a487f819e..ff2f27db9 100644 --- a/src/v4/social/internal-components/UserAvatar/UserAvatar.tsx +++ b/src/v4/social/internal-components/UserAvatar/UserAvatar.tsx @@ -27,19 +27,32 @@ const UserSvg = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => ( ); interface UserAvatarProps { + pageId?: string; + componentId?: string; userId?: string | null; className?: string; } -export function UserAvatar({ userId, className }: UserAvatarProps) { - const { user } = useUser(userId); +export function UserAvatar({ + pageId = '*', + componentId = '*', + userId, + className, +}: UserAvatarProps) { + const elementId = 'user_avatar'; + const { user } = useUser({ userId }); const userImage = useImage({ fileId: user?.avatar?.fileId }); if (user == null || userId == null || userImage == null) return <UserSvg className={className} />; return ( - <object data={userImage} type="image/png" className={clsx(styles.userAvatar__img, className)}> + <object + data-qa-anchor={`${pageId}/${componentId}/${elementId}`} + data={userImage} + type="image/png" + className={clsx(styles.userAvatar__img, className)} + > <UserSvg className={className} /> </object> ); diff --git a/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.module.css b/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.module.css new file mode 100644 index 000000000..4fb4a2a05 --- /dev/null +++ b/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.module.css @@ -0,0 +1,55 @@ +.allCategoriesPage { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding-left: 1rem; + padding-right: 1rem; +} + +.allCategoriesPage__navigation { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content minmax(0, 1fr); + align-items: center; + background-color: var(--asc-color-background-default); + height: 3.625rem; +} + +.allCategoriesList { + display: flex; + flex-direction: column; +} + +.allCategoriesList__divider { + height: 0.0625rem; + background-color: var(--asc-color-base-shade4); + width: 100%; +} + +.allCategoryItem { + display: grid; + align-items: center; + grid-template-columns: 2.5rem minmax(0, 1fr) min-content; + gap: 0.75rem; + padding: 0.5rem 0; +} + +.allCategoryItem__categoryName { + align-self: left; +} + +.allCategoryItem__image { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; +} + +.allCategoryItem__arrow { + width: 1.5rem; + height: 1.125rem; + fill: var(--asc-color-secondary-default); +} + +.allCategoriesList__intersectionNode { + width: 100%; + height: 1px; +} diff --git a/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.stories.tsx b/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.stories.tsx new file mode 100644 index 000000000..01f6c29b2 --- /dev/null +++ b/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.stories.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { AllCategoriesPage } from './AllCategoriesPage'; + +export default { + title: 'v4-social/pages/AllCategoriesPage', +}; + +export const AllCategoriesPageStories = { + render: () => { + return <AllCategoriesPage />; + }, +}; diff --git a/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.tsx b/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.tsx new file mode 100644 index 000000000..78aee9b68 --- /dev/null +++ b/src/v4/social/pages/AllCategoriesPage/AllCategoriesPage.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import styles from './AllCategoriesPage.module.css'; +import { useAmityPage } from '~/v4/core/hooks/uikit'; +import { Typography } from '~/v4/core/components'; +import { Button } from '~/v4/core/natives/Button'; +import useCategoriesCollection from '~/v4/core/hooks/collections/useCategoriesCollection'; +import useImage from '~/core/hooks/useImage'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { BackButton } from '~/v4/social/elements/BackButton'; +import { AllCategoriesTitle } from '~/v4/social/elements/AllCategoriesTitle'; +import ChevronRight from '~/v4/icons/ChevronRight'; +import { CategoryImage } from '~/v4/social/internal-components/CategoryImage'; +import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; + +interface AllCategoryItemProps { + category: Amity.Category; + pageId: string; + onClick: (categoryId: string) => void; +} + +const AllCategoryItem = ({ category, pageId, onClick }: AllCategoryItemProps) => { + const avatarImg = useImage({ + fileId: category.avatarFileId, + imageSize: 'small', + }); + + return ( + <Button className={styles.allCategoryItem} onPress={() => onClick(category.categoryId)}> + <CategoryImage imgSrc={avatarImg} className={styles.allCategoryItem__image} pageId={pageId} /> + <Typography.BodyBold className={styles.allCategoryItem__categoryName}> + {category.name} + </Typography.BodyBold> + <ChevronRight className={styles.allCategoryItem__arrow} /> + </Button> + ); +}; + +export function AllCategoriesPage() { + const pageId = 'all_categories_page'; + + const [intersectionNode, setIntersectionNode] = useState<HTMLDivElement | null>(null); + + const { goToCommunitiesByCategoryPage, onBack } = useNavigation(); + + const { themeStyles, accessibilityId } = useAmityPage({ + pageId, + }); + + const { categories, isLoading, error, loadMore, hasMore } = useCategoriesCollection({ + query: { limit: 20 }, + }); + + useIntersectionObserver({ + node: intersectionNode, + options: { + threshold: 0.8, + }, + onIntersect: () => { + if (hasMore && isLoading === false) { + loadMore(); + } + }, + }); + + return ( + <div className={styles.allCategoriesPage} style={themeStyles} data-qa-anchor={accessibilityId}> + <div className={styles.allCategoriesPage__navigation}> + <BackButton pageId={pageId} onPress={onBack} /> + <AllCategoriesTitle pageId={pageId} /> + </div> + <div className={styles.allCategoriesList}> + {categories.map((category, index) => ( + <React.Fragment key={category.categoryId}> + <AllCategoryItem + category={category} + pageId={pageId} + onClick={(categoryId) => { + goToCommunitiesByCategoryPage({ + categoryId, + }); + }} + /> + {index < categories.length - 1 && <div className={styles.allCategoriesList__divider} />} + </React.Fragment> + ))} + <div + ref={(node) => setIntersectionNode(node)} + className={styles.allCategoriesList__intersectionNode} + /> + </div> + </div> + ); +} diff --git a/src/v4/social/pages/AllCategoriesPage/index.tsx b/src/v4/social/pages/AllCategoriesPage/index.tsx new file mode 100644 index 000000000..0b2fe38e6 --- /dev/null +++ b/src/v4/social/pages/AllCategoriesPage/index.tsx @@ -0,0 +1 @@ +export { AllCategoriesPage } from './AllCategoriesPage'; diff --git a/src/v4/social/pages/Application/index.tsx b/src/v4/social/pages/Application/index.tsx index ce8ed9141..a2cbea094 100644 --- a/src/v4/social/pages/Application/index.tsx +++ b/src/v4/social/pages/Application/index.tsx @@ -19,9 +19,12 @@ import CommunityEditPage from '~/social/pages/CommunityEdit'; import ProfileSettings from '~/social/components/ProfileSettings'; import { CommunityProfilePage } from '~/v4/social/pages/CommunityProfilePage'; import { CommunityTabProvider } from '~/v4/core/providers/CommunityTabProvider'; +import { AllCategoriesPage } from '~/v4/social/pages/AllCategoriesPage'; +import { CommunitiesByCategoryPage } from '~/v4/social/pages/CommunitiesByCategoryPage'; +import CommunityCreationModal from '~/social/components/CommunityCreationModal'; const Application = () => { - const { page } = useNavigation(); + const { page, onBack } = useNavigation(); const [open, setOpen] = useState(false); const [socialSettings, setSocialSettings] = useState<Amity.SocialSettings | null>(null); @@ -69,8 +72,11 @@ const Application = () => { )} {page.type === PageTypes.SelectPostTargetPage && <SelectPostTargetPage />} {page.type === PageTypes.MyCommunitiesSearchPage && <MyCommunitiesSearchPage />} + {page.type === PageTypes.AllCategoriesPage && <AllCategoriesPage />} + {page.type === PageTypes.CommunitiesByCategoryPage && ( + <CommunitiesByCategoryPage categoryId={page.context.categoryId} /> + )} {/* V3 */} - {page.type === PageTypes.Explore} {page.type === PageTypes.CommunityFeed && ( <CommunityFeed communityId={page.context.communityId} @@ -87,6 +93,9 @@ const Application = () => { )} {page.type === PageTypes.UserEdit && <ProfileSettings userId={page.context.userId} />} + {page.type === PageTypes.CommunityCreatePage && ( + <CommunityCreationModal isOpen={true} onClose={onBack} /> + )} {/*End of V3 */} </div> diff --git a/src/v4/social/pages/CommunitiesByCategoryPage/CommunitiesByCategoryPage.module.css b/src/v4/social/pages/CommunitiesByCategoryPage/CommunitiesByCategoryPage.module.css new file mode 100644 index 000000000..ac766f612 --- /dev/null +++ b/src/v4/social/pages/CommunitiesByCategoryPage/CommunitiesByCategoryPage.module.css @@ -0,0 +1,131 @@ +.communitiesByCategoryPage { + display: flex; + flex-direction: column; + padding: 1rem; + gap: 1rem; + height: 100%; +} + +.communitiesByCategoryPage__navigation { + display: grid; + grid-template: + 'back title title title title title title title title title title' auto + / minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax( + 0, + 1fr + ) + minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); + align-items: center; + background-color: var(--asc-color-background-default); + height: 3.625rem; +} + +.communitiesByCategoryPage__navigationBackButton { + grid-area: back; +} + +.communitiesByCategoryPage__navigationTitle { + overflow: hidden; + grid-area: title; + display: flex; + justify-content: center; + align-items: center; +} + +.communitiesByCategoryPage__intersectionNode { + height: 1px; + width: 100%; +} + +.communitiesByCategoryItem { + display: grid; + grid-template-columns: [image-start] 5rem [image-end content-start] 1fr 1fr 1fr 1fr 1fr 1fr [content-end]; + grid-template-rows: + [name-start] auto + [name-end member-start] auto + [member-end]; + gap: 0.75rem; + width: 100%; +} + +.communitiesByCategoryItem[data-has-categories='true'] { + grid-template-rows: + [name-start] auto + [name-end cat-start] auto + [cat-end member-start] auto + [member-end]; +} + +.communitiesByCategoryItem__image { + grid-column: image-start / image-end; + grid-row: name-start / member-end; + place-self: center; + height: 5rem; + width: 5rem; + border-radius: var(--asc-border-radius-sm); +} + +.communitiesByCategoryItem__img { + width: 100%; + height: 100%; + overflow: hidden; + object-fit: cover; + border-radius: var(--asc-border-radius-sm); +} + +.communitiesByCategoryItem__content { + display: grid; + grid-column: content-start / content-end; + grid-row: name-start / member-end; + grid-template-columns: subgrid [sub-a] [sub-b] [sub-c] [sub-d] [sub-e] [sub-f] [sub-g]; + grid-template-rows: subgrid; + width: 100%; + justify-content: space-evenly; + gap: 0.5rem; +} + +.communitiesByCategoryItem__communityName { + grid-column: sub-a / sub-g; + grid-row: name-start / name-end; + display: flex; + justify-content: start; + align-items: center; + gap: 0.25rem; + width: 100%; +} + +.communitiesByCategoryItem__categories { + grid-column: sub-a / sub-e; + grid-row: cat-start / cat-end; +} + +.communitiesByCategoryItem__categories__container { + display: flex; + align-items: center; +} + +.communitiesByCategoryItem__member { + grid-column: sub-a / sub-e; + grid-row: member-start / member-end; +} + +.communitiesByCategoryItem__joinButton { + grid-row: name-start / member-end; + grid-column: sub-e / sub-g; + width: 4rem; + place-self: end end; +} + +.communitiesByCategoryItem__joinButton[data-has-categories='true'] { + grid-row: cat-start / member-end; +} + +.communitiesByCategoryItem__communityName__private { + width: 1.25rem; + height: 1.25rem; + display: flex; + justify-content: center; + align-items: center; + padding-top: 0.22rem; + padding-bottom: 0.28rem; +} diff --git a/src/v4/social/pages/CommunitiesByCategoryPage/CommunitiesByCategoryPage.tsx b/src/v4/social/pages/CommunitiesByCategoryPage/CommunitiesByCategoryPage.tsx new file mode 100644 index 000000000..6b211db3c --- /dev/null +++ b/src/v4/social/pages/CommunitiesByCategoryPage/CommunitiesByCategoryPage.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import useCommunitiesCollection from '~/v4/core/hooks/collections/useCommunitiesCollection'; +import { useAmityPage } from '~/v4/core/hooks/uikit'; +import { useCategory } from '~/v4/core/hooks/useCategory'; +import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { CategoryTitle } from '~/v4/social/elements/CategoryTitle'; +import { BackButton } from '~/v4/social/elements/BackButton'; +import { CommunityRowItem } from '~/v4/social/internal-components/CommunityRowItem'; +import { CommunityRowItemSkeleton } from '~/v4/social/internal-components/CommunityRowItem/CommunityRowItemSkeleton'; +import { EmptyCommunity } from './EmptyCommunity'; +import { CommunityRowItemDivider } from '~/v4/social/internal-components/CommunityRowItem/CommunityRowItemDivider'; +import { useCommunityActions } from '~/v4/social/hooks/useCommunityActions'; +import styles from './CommunitiesByCategoryPage.module.css'; + +interface CommunitiesByCategoryPageProps { + categoryId: string; +} + +export function CommunitiesByCategoryPage({ categoryId }: CommunitiesByCategoryPageProps) { + const pageId = 'communities_by_category_page'; + const { themeStyles, accessibilityId } = useAmityPage({ + pageId, + }); + + const { onBack, goToCommunityProfilePage, goToCommunitiesByCategoryPage } = useNavigation(); + + const [intersectionNode, setIntersectionNode] = useState<HTMLDivElement | null>(null); + + const { communities, isLoading, loadMore, hasMore } = useCommunitiesCollection({ + categoryId, + limit: 20, + }); + + const category = useCategory({ categoryId }); + + const { joinCommunity, leaveCommunity } = useCommunityActions(); + + const handleJoinButtonClick = (communityId: string) => joinCommunity(communityId); + const handleLeaveButtonClick = (communityId: string) => leaveCommunity(communityId); + + useIntersectionObserver({ + onIntersect: () => { + if (isLoading === false) { + loadMore(); + } + }, + node: intersectionNode, + options: { + threshold: 0.7, + }, + }); + + const isEmpty = communities.length === 0 && !isLoading; + + return ( + <div + className={styles.communitiesByCategoryPage} + style={themeStyles} + data-qa-anchor={accessibilityId} + > + <div className={styles.communitiesByCategoryPage__navigation}> + <div className={styles.communitiesByCategoryPage__navigationBackButton}> + <BackButton pageId={pageId} onPress={onBack} /> + </div> + <div className={styles.communitiesByCategoryPage__navigationTitle}> + <CategoryTitle pageId={pageId} categoryName={category?.name || ''} /> + </div> + </div> + {isEmpty ? ( + <EmptyCommunity pageId={pageId} /> + ) : ( + <div> + {communities.map((community, index) => ( + <React.Fragment key={community.communityId}> + <CommunityRowItemDivider /> + <CommunityRowItem + community={community} + pageId={pageId} + onClick={(communityId) => goToCommunityProfilePage(communityId)} + onCategoryClick={(categoryId) => goToCommunitiesByCategoryPage({ categoryId })} + onJoinButtonClick={handleJoinButtonClick} + onLeaveButtonClick={handleLeaveButtonClick} + showJoinButton + minCategoryCharacters={4} + maxCategoryCharacters={30} + maxCategoriesLength={2} + /> + </React.Fragment> + ))} + {isLoading && + Array.from({ length: 10 }).map((_, index) => ( + <React.Fragment key={index}> + <CommunityRowItemDivider /> + <CommunityRowItemSkeleton /> + </React.Fragment> + ))} + {hasMore && ( + <div + ref={(node) => setIntersectionNode(node)} + className={styles.communitiesByCategoryPage__intersectionNode} + /> + )} + </div> + )} + </div> + ); +} diff --git a/src/v4/social/pages/CommunitiesByCategoryPage/EmptyCommunity.module.css b/src/v4/social/pages/CommunitiesByCategoryPage/EmptyCommunity.module.css new file mode 100644 index 000000000..2b7e8dbba --- /dev/null +++ b/src/v4/social/pages/CommunitiesByCategoryPage/EmptyCommunity.module.css @@ -0,0 +1,10 @@ +.emptyCommunity { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: var(--asc-color-background-default); + gap: 0.5rem; +} diff --git a/src/v4/social/pages/CommunitiesByCategoryPage/EmptyCommunity.tsx b/src/v4/social/pages/CommunitiesByCategoryPage/EmptyCommunity.tsx new file mode 100644 index 000000000..274499361 --- /dev/null +++ b/src/v4/social/pages/CommunitiesByCategoryPage/EmptyCommunity.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { CommunityEmptyTitle } from '~/v4/social/elements/CommunityEmptyTitle'; +import { CommunityEmptyImage } from '~/v4/social/elements/CommunityEmptyImage'; + +import styles from './EmptyCommunity.module.css'; + +export const EmptyCommunity = ({ pageId }: { pageId: string }) => { + return ( + <div className={styles.emptyCommunity}> + <CommunityEmptyImage /> + <CommunityEmptyTitle pageId={pageId} /> + </div> + ); +}; diff --git a/src/v4/social/pages/CommunitiesByCategoryPage/index.tsx b/src/v4/social/pages/CommunitiesByCategoryPage/index.tsx new file mode 100644 index 000000000..ef93cc2bd --- /dev/null +++ b/src/v4/social/pages/CommunitiesByCategoryPage/index.tsx @@ -0,0 +1 @@ +export { CommunitiesByCategoryPage } from './CommunitiesByCategoryPage'; diff --git a/src/v4/social/pages/CommunityProfilePage/CommunityProfilePage.module.css b/src/v4/social/pages/CommunityProfilePage/CommunityProfilePage.module.css index dc39006e0..65d6c74cd 100644 --- a/src/v4/social/pages/CommunityProfilePage/CommunityProfilePage.module.css +++ b/src/v4/social/pages/CommunityProfilePage/CommunityProfilePage.module.css @@ -22,19 +22,6 @@ transform: translateX(-50%); } -.communityProfilePage__tabContainer { - position: relative; - z-index: 1; -} - -.communityProfilePage__tabContainer.sticky { - position: fixed; - top: 0; - left: 0; - right: 0; - background-color: var(--asc-color-background-default); -} - .communityProfilePage__createPostButton { position: fixed; bottom: 1rem; diff --git a/src/v4/social/pages/PostDetailPage/PostDetailPage.tsx b/src/v4/social/pages/PostDetailPage/PostDetailPage.tsx index be15a52bf..89f7d104c 100644 --- a/src/v4/social/pages/PostDetailPage/PostDetailPage.tsx +++ b/src/v4/social/pages/PostDetailPage/PostDetailPage.tsx @@ -75,7 +75,12 @@ export function PostDetailPage({ id, hideTarget, category }: PostDetailPageProps defaultClassName={styles.postDetailPage__backIcon} onPress={() => onBack()} /> - <Typography.Title className={styles.postDetailPage__topBar__title}>Post</Typography.Title> + <Typography.Title + data-qa-anchor={`${pageId}/page_title`} + className={styles.postDetailPage__topBar__title} + > + Post + </Typography.Title> <div className={styles.postDetailPage__topBar__menuBar}> <MenuButton pageId={pageId} diff --git a/src/v4/social/pages/SelectPostTargetPage/SelectPostTargetPage.tsx b/src/v4/social/pages/SelectPostTargetPage/SelectPostTargetPage.tsx index e9614d730..9bfda3379 100644 --- a/src/v4/social/pages/SelectPostTargetPage/SelectPostTargetPage.tsx +++ b/src/v4/social/pages/SelectPostTargetPage/SelectPostTargetPage.tsx @@ -30,7 +30,7 @@ export function SelectPostTargetPage() { const { AmityPostTargetSelectionPage } = usePageBehavior(); const [intersectionNode, setIntersectionNode] = useState<HTMLDivElement | null>(null); const { currentUserId } = useSDK(); - const { user } = useUser(currentUserId); + const { user } = useUser({ userId: currentUserId }); useIntersectionObserver({ onIntersect: () => { if (hasMore && isLoading === false) { diff --git a/src/v4/social/pages/SocialHomePage/Explore.module.css b/src/v4/social/pages/SocialHomePage/Explore.module.css new file mode 100644 index 000000000..57a41aede --- /dev/null +++ b/src/v4/social/pages/SocialHomePage/Explore.module.css @@ -0,0 +1,90 @@ +.explore { + display: flex; + flex-direction: column; +} + +.explore__recommendedForYou { + width: 100%; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + padding: 0.5rem 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.explore__recommendedForYou::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +.explore__recommendedForYou[data-is-loading='true'] { + padding: 1.5rem 1rem; +} + +.explore__trendingNow { + padding: 0.5rem 1rem; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.explore__trendingTitleSkeleton { + width: 9.75rem; + height: 0.75rem; + flex-shrink: 0; + border-radius: 0.75rem; + background: var(--asc-color-base-shade4); + animation: skeleton-pulse 1.5s ease-in-out infinite; + margin-top: 0.75rem; + margin-bottom: 0.31rem; +} + +.explore__recommendedForYouTitle { + color: var(--asc-color-base-default); +} + +.explore__exploreCategories { + width: 100%; + padding: 0.75rem 1rem; +} + +.explore__exploreCategories::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +.explore__pullToRefresh { + height: var(--asc-pull-to-refresh-height); + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; +} + +.explore__pullToRefresh__spinner { + width: 1.25rem; + height: 1.25rem; + animation-name: spin; + animation-duration: 1000ms; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +.explore__divider { + background-color: var(--asc-color-base-shade4); + height: 0.5rem; + width: 100%; +} + +@keyframes skeleton-pulse { + 0% { + opacity: 0.6; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0.6; + } +} diff --git a/src/v4/social/pages/SocialHomePage/Explore.tsx b/src/v4/social/pages/SocialHomePage/Explore.tsx new file mode 100644 index 000000000..bb56d098f --- /dev/null +++ b/src/v4/social/pages/SocialHomePage/Explore.tsx @@ -0,0 +1,103 @@ +import React, { useRef, useState } from 'react'; +import { ExploreCommunityCategories } from '~/v4/social/components/ExploreCommunityCategories'; +import { RecommendedCommunities } from '~/v4/social/components/RecommendedCommunities'; +import { TrendingCommunities } from '~/v4/social/components/TrendingCommunities'; +import { useExplore } from '~/v4/social/providers/ExploreProvider'; + +import styles from './Explore.module.css'; +import { ExploreError } from './ExploreError'; +import { ExploreEmpty } from '~/v4/social/components/ExploreEmpty'; +import { ExploreCommunityEmpty } from '~/v4/social/components/ExploreCommunityEmpty'; +import { ExploreTrendingTitle } from '~/v4/social/elements/ExploreTrendingTitle'; +import { ExploreRecommendedTitle } from '~/v4/social/elements/ExploreRecommendedTitle'; +import { RefreshSpinner } from '~/v4/icons/RefreshSpinner'; + +interface ExploreProps { + pageId: string; +} + +export function Explore({ pageId }: ExploreProps) { + const touchStartY = useRef(0); + const [touchDiff, setTouchDiff] = useState(0); + + const { + refresh, + isLoading, + isEmpty, + isCommunityEmpty, + noRecommendedCommunities, + noTrendingCommunities, + error, + } = useExplore(); + + if (error != null) { + return <ExploreError />; + } + + if (isEmpty) { + return <ExploreEmpty pageId={pageId} />; + } + + if (isCommunityEmpty) { + return <ExploreCommunityEmpty pageId={pageId} />; + } + + return ( + <div + className={styles.explore} + onDrag={(event) => event.stopPropagation()} + onTouchStart={(ev) => { + touchStartY.current = ev.touches[0].clientY; + }} + onTouchMove={(ev) => { + const touchY = ev.touches[0].clientY; + + if (touchStartY.current > touchY) { + return; + } + + setTouchDiff(Math.min(touchY - touchStartY.current, 100)); + }} + onTouchEnd={(ev) => { + touchStartY.current = 0; + if (touchDiff >= 75) { + refresh(); + } + setTouchDiff(0); + }} + > + <div + style={ + { + '--asc-pull-to-refresh-height': `${touchDiff}px`, + } as React.CSSProperties + } + className={styles.explore__pullToRefresh} + > + <RefreshSpinner className={styles.explore__pullToRefresh__spinner} /> + </div> + <div className={styles.explore__divider} /> + <div className={styles.explore__exploreCategories}> + <ExploreCommunityCategories pageId={pageId} /> + </div> + {isLoading ? <div className={styles.explore__divider} /> : null} + {noRecommendedCommunities === false ? ( + <div className={styles.explore__recommendedForYou} data-is-loading={isLoading === true}> + {isLoading ? null : <ExploreRecommendedTitle pageId={pageId} />} + <RecommendedCommunities pageId={pageId} /> + </div> + ) : null} + {isLoading ? <div className={styles.explore__divider} /> : null} + {noTrendingCommunities === false ? ( + <div className={styles.explore__trendingNow}> + {isLoading ? ( + <div className={styles.explore__trendingTitleSkeleton} /> + ) : ( + <ExploreTrendingTitle pageId={pageId} /> + )} + <TrendingCommunities pageId={pageId} /> + </div> + ) : null} + </div> + ); +} diff --git a/src/v4/social/pages/SocialHomePage/ExploreError.module.css b/src/v4/social/pages/SocialHomePage/ExploreError.module.css new file mode 100644 index 000000000..f5ccc192e --- /dev/null +++ b/src/v4/social/pages/SocialHomePage/ExploreError.module.css @@ -0,0 +1,18 @@ +.exploreError { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: var(--asc-color-background-default); + gap: 1rem; +} + +.exploreError__text { + padding-bottom: 1.0625rem; + color: var(--asc-color-base-shade3); + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/src/v4/social/pages/SocialHomePage/ExploreError.tsx b/src/v4/social/pages/SocialHomePage/ExploreError.tsx new file mode 100644 index 000000000..b9bf9536a --- /dev/null +++ b/src/v4/social/pages/SocialHomePage/ExploreError.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Typography } from '~/v4/core/components/Typography'; + +import styles from './ExploreError.module.css'; + +const ExploreErrorIcon = () => ( + <svg xmlns="http://www.w3.org/2000/svg" width="62" height="42" viewBox="0 0 62 42" fill="none"> + <path + d="M58.7773 0.841919C60.1484 0.841919 61.3086 2.00208 61.3086 3.37317V38.8107C61.3086 40.2872 60.1484 41.3419 58.7773 41.3419H5.62109C2.77344 41.3419 0.558594 39.1271 0.558594 36.2794V6.74817C0.558594 5.37708 1.61328 4.21692 3.08984 4.21692H7.30859V3.37317C7.30859 2.00208 8.36328 0.841919 9.83984 0.841919H58.7773ZM3.93359 36.2794C3.93359 37.2286 4.67188 37.9669 5.62109 37.9669C6.46484 37.9669 7.30859 37.2286 7.30859 36.2794V7.59192H3.93359V36.2794ZM57.9336 37.9669V4.21692H10.6836V36.2794V36.3849C10.6836 36.8068 10.4727 37.545 10.3672 37.9669H57.9336Z" + fill="#EBECEF" + /> + </svg> +); + +export const ExploreError = () => { + return ( + <div className={styles.exploreError}> + <ExploreErrorIcon /> + <div className={styles.exploreError__text}> + <Typography.Title>Something went wrong</Typography.Title> + <Typography.Caption>Please try again.</Typography.Caption> + </div> + </div> + ); +}; diff --git a/src/v4/social/pages/SocialHomePage/SocialHomePage.tsx b/src/v4/social/pages/SocialHomePage/SocialHomePage.tsx index 9fb77328f..07cf323c7 100644 --- a/src/v4/social/pages/SocialHomePage/SocialHomePage.tsx +++ b/src/v4/social/pages/SocialHomePage/SocialHomePage.tsx @@ -10,7 +10,8 @@ import { Newsfeed } from '~/v4/social/components/Newsfeed'; import { useAmityPage } from '~/v4/core/hooks/uikit'; import { CreatePostMenu } from '~/v4/social/components/CreatePostMenu'; import { useGlobalFeedContext } from '~/v4/social/providers/GlobalFeedProvider'; -import ExplorePage from '~/social/pages/Explore'; +import { Explore } from './Explore'; +import { ExploreProvider } from '~/v4/social/providers/ExploreProvider'; export enum HomePageTab { Newsfeed = 'Newsfeed', @@ -33,18 +34,20 @@ export function SocialHomePage() { const initialLoad = useRef(true); useEffect(() => { + if (activeTab !== HomePageTab.Newsfeed) return; if (!containerRef.current) return; containerRef.current.scrollTop = scrollPosition; setTimeout(() => { initialLoad.current = false; }, 100); - }, [containerRef.current]); + }, [containerRef.current, activeTab]); const handleClickButton = () => { setIsShowCreatePostMenu((prev) => !prev); }; const handleScroll = (event: React.UIEvent<HTMLDivElement, UIEvent>) => { + if (activeTab !== HomePageTab.Newsfeed) return; if (initialLoad.current) return; onScroll(event); }; @@ -94,7 +97,9 @@ export function SocialHomePage() { <div className={styles.socialHomePage__contents} ref={containerRef} onScroll={handleScroll}> {activeTab === HomePageTab.Newsfeed && <Newsfeed pageId={pageId} />} {activeTab === HomePageTab.Explore && ( - <ExplorePage isOpen={false} toggleOpen={() => {}} hideSideMenu={true} /> + <ExploreProvider> + <Explore pageId={pageId} /> + </ExploreProvider> )} {activeTab === HomePageTab.MyCommunities && <MyCommunities pageId={pageId} />} </div> diff --git a/src/v4/social/providers/ExploreProvider.tsx b/src/v4/social/providers/ExploreProvider.tsx new file mode 100644 index 000000000..543c7bebd --- /dev/null +++ b/src/v4/social/providers/ExploreProvider.tsx @@ -0,0 +1,118 @@ +import React, { createContext, useContext, useState } from 'react'; +import useCategoriesCollection from '~/v4/core/hooks/collections/useCategoriesCollection'; +import { useRecommendedCommunitiesCollection } from '~/v4/core/hooks/collections/useRecommendedCommunitiesCollection'; +import { useTrendingCommunitiesCollection } from '~/v4/core/hooks/collections/useTrendingCommunitiesCollection'; + +type ExploreContextType = { + fetchTrendingCommunities: () => void; + fetchRecommendedCommunities: () => void; + fetchCommunityCategories: () => void; + refetchRecommendedCommunities: () => void; + isLoading: boolean; + error: Error | null; + trendingCommunities: Amity.Community[]; + recommendedCommunities: Amity.Community[]; + noRecommendedCommunities: boolean; + noTrendingCommunities: boolean; + isEmpty: boolean; + isCommunityEmpty: boolean; + categories: Amity.Category[]; + refresh: () => void; +}; + +const ExploreContext = createContext<ExploreContextType>({ + fetchTrendingCommunities: () => {}, + fetchRecommendedCommunities: () => {}, + fetchCommunityCategories: () => {}, + refetchRecommendedCommunities: () => {}, + trendingCommunities: [], + recommendedCommunities: [], + categories: [], + isEmpty: false, + noRecommendedCommunities: false, + noTrendingCommunities: false, + isCommunityEmpty: false, + isLoading: false, + error: null, + refresh: () => {}, +}); + +export const useExplore = () => useContext(ExploreContext); + +type ExploreProviderProps = { + children: React.ReactNode; +}; + +export const ExploreProvider: React.FC<ExploreProviderProps> = ({ children }) => { + const [trendingCommunitiesEnable, setTrendingCommunitiesEnable] = useState(false); + const [recommendedCommunitiesEnable, setRecommendedCommunitiesEnable] = useState(false); + const [communityCategoriesEnable, setCommunityCategoriesEnable] = useState(false); + + const trendingData = useTrendingCommunitiesCollection({ + params: { limit: 5 }, + enabled: trendingCommunitiesEnable, + }); + + const recommendedData = useRecommendedCommunitiesCollection({ + params: { limit: 4 }, + enabled: recommendedCommunitiesEnable, + }); + + const categoriesData = useCategoriesCollection({ + query: { + limit: 5, + sortBy: 'name', + }, + enabled: communityCategoriesEnable, + }); + + const isLoading = trendingData.isLoading || recommendedData.isLoading || categoriesData.isLoading; + const error = trendingData.error && recommendedData.error && categoriesData.error; + + const refetchRecommendedCommunities = () => recommendedData.refresh(); + + const refresh = () => { + trendingData.refresh(); + refetchRecommendedCommunities(); + categoriesData.refresh(); + }; + + const noCategories = categoriesData.categories.length === 0 && !categoriesData.isLoading; + + const noRecommendedCommunities = + recommendedData.recommendedCommunities.length === 0 && !recommendedData.isLoading; + + const noTrendingCommunities = + trendingData.trendingCommunities.length === 0 && !trendingData.isLoading; + + const isCommunityEmpty = noRecommendedCommunities && noTrendingCommunities; + + const isEmpty = noCategories && isCommunityEmpty; + + const fetchTrendingCommunities = () => setTrendingCommunitiesEnable(true); + const fetchRecommendedCommunities = () => setRecommendedCommunitiesEnable(true); + const fetchCommunityCategories = () => setCommunityCategoriesEnable(true); + + return ( + <ExploreContext.Provider + value={{ + fetchTrendingCommunities, + fetchRecommendedCommunities, + fetchCommunityCategories, + refetchRecommendedCommunities, + trendingCommunities: trendingData.trendingCommunities, + recommendedCommunities: recommendedData.recommendedCommunities, + categories: categoriesData.categories, + noRecommendedCommunities, + noTrendingCommunities, + isEmpty, + isCommunityEmpty, + isLoading, + error, + refresh, + }} + > + {children} + </ExploreContext.Provider> + ); +};