diff --git a/.storybook/decorators/FluidControl.tsx b/.storybook/decorators/FluidControl.tsx index 263b0a973..b2aa92b04 100644 --- a/.storybook/decorators/FluidControl.tsx +++ b/.storybook/decorators/FluidControl.tsx @@ -143,7 +143,7 @@ const decorator: NonNullable[number] = ( Story, { globals: { [GLOBAL_NAME]: val } }, ) => { - if (val === 'none') return {Story()}; + if (val === 'none') return {Story()}; else if (val === 'fullscreen') return {Story()}; else if (val === 'framed') return {Story()}; else if (val === 'boundingbox') diff --git a/.storybook/decorators/UiKitDecorator.tsx b/.storybook/decorators/UiKitDecorator.tsx index b030a36da..d21f85bf3 100644 --- a/.storybook/decorators/UiKitDecorator.tsx +++ b/.storybook/decorators/UiKitDecorator.tsx @@ -1,36 +1,30 @@ import React, { useCallback } from 'react'; import UiKitProvider from '../../src/core/providers/UiKitProvider'; import { Preview } from '@storybook/react'; -import amityConfig from '../../amity-uikit.config.json'; - -const users = import.meta.env.STORYBOOK_USERS.split(','); - -const GLOBAL_NAME = 'user'; -const global = { - [GLOBAL_NAME]: { - name: 'User selector', - description: 'User switcher for SDK', - defaultValue: 'Web-Test', - toolbar: { - icon: 'user', - items: [ - { value: 'Web-Test,Web-test', title: 'Web-Test' }, - ...users.map((user) => { - return { value: `${user},${user}`, title: user }; - }), - ], - }, - }, -}; +import { useState } from 'react'; +import { useEffect } from 'react'; + +const FALLBACK_USER = 'Web-Test'; + +const decorator: NonNullable[number] = (Story, context) => { + const { args } = context; + + const [userId, setUserId] = useState(args.userId || FALLBACK_USER); + const [displayNameState, setDisplayNameState] = useState( + args.displayName || args.userId || userId, + ); -const FALLBACK_USER = 'Web-Test,Web-Test'; + useEffect(() => { + if (!args.submit) return; + if (args.userId) { + setUserId(args.userId); + } + if (args.displayName) { + setDisplayNameState(args.displayName); + } + }, [args.submit]); -const decorator: NonNullable[number] = ( - Story, - { globals: { [GLOBAL_NAME]: val, theme } }, -) => { - const user = val || FALLBACK_USER; - const [userId, displayName] = user.split(','); + const displayName = displayNameState || userId; const handleConnectionStatusChange = useCallback((...args) => { console.log(`[UiKitProvider.handleConnectionStatusChange]`, ...args); @@ -46,8 +40,8 @@ const decorator: NonNullable[number] = ( return ( [number] = ( ); }; -export default { global, decorator }; +export default { decorator }; diff --git a/.storybook/decorators/UiKitV4Decorator.tsx b/.storybook/decorators/UiKitV4Decorator.tsx index 0bc9887e5..802529925 100644 --- a/.storybook/decorators/UiKitV4Decorator.tsx +++ b/.storybook/decorators/UiKitV4Decorator.tsx @@ -3,35 +3,30 @@ import { AmityUIKitProvider } from '../../src/v4/core/providers'; import { Preview } from '@storybook/react'; import amityConfig from '../../amity-uikit.config.json'; import { Config } from '../../src/v4/core/providers/CustomizationProvider'; +import { useState } from 'react'; +import { useEffect } from 'react'; -const users = import.meta.env.STORYBOOK_USERS.split(','); +const FALLBACK_USER = 'Web-Test'; -const GLOBAL_NAME = 'user'; -const global = { - [GLOBAL_NAME]: { - name: 'User selector', - description: 'User switcher for SDK', - defaultValue: 'Web-Test', - toolbar: { - icon: 'user', - items: [ - { value: 'Web-Test,Web-test', title: 'Web-Test' }, - ...users.map((user) => { - return { value: `${user},${user}`, title: user }; - }), - ], - }, - }, -}; +const decorator: NonNullable[number] = (Story, context) => { + const { args } = context; + + const [userId, setUserId] = useState(args.userId || FALLBACK_USER); + const [displayNameState, setDisplayNameState] = useState( + args.displayName || args.userId || userId, + ); -const FALLBACK_USER = 'Web-Test,Web-Test'; + useEffect(() => { + if (!args.submit) return; + if (args.userId) { + setUserId(args.userId); + } + if (args.displayName) { + setDisplayNameState(args.displayName); + } + }, [args.submit]); -const decorator: NonNullable[number] = ( - Story, - { globals: { [GLOBAL_NAME]: val, theme } }, -) => { - const user = val || FALLBACK_USER; - const [userId, displayName] = user.split(','); + const displayName = displayNameState || userId; const handleConnectionStatusChange = useCallback((...args) => { console.log(`[UiKitProvider.handleConnectionStatusChange]`, ...args); @@ -47,8 +42,8 @@ const decorator: NonNullable[number] = ( return ( { @@ -29,8 +43,6 @@ const preview: Preview = { }, globalTypes: { ...FluidControl.global, - ...UiKitDecorator.global, - ...UiKitV4Decorator.global, }, }; diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c348ea7..70df6d687 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.12 (2024-09-02) + ## 4.0.0-beta.11 (2024-08-16) ## 4.0.0-beta.10 (2024-07-24) diff --git a/package.json b/package.json index e5009886a..384dac6d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@amityco/ui-kit-open-source", - "version": "4.0.0-beta.11", + "version": "4.0.0-beta.12", "engines": { "node": ">=20", "pnpm": "9" @@ -39,12 +39,12 @@ "tsc": "tsc" }, "peerDependencies": { - "@amityco/ts-sdk": "^6.27.0", + "@amityco/ts-sdk": "^6.30.0", "react": ">=17.0.2", "react-dom": ">=17.0.2" }, "devDependencies": { - "@amityco/ts-sdk": "^6.27.0", + "@amityco/ts-sdk": "^6.30.0", "@eslint/js": "^9.4.0", "@storybook/addon-a11y": "^7.6.7", "@storybook/addon-actions": "^7.6.7", @@ -159,5 +159,5 @@ } }, "license": "LGPL-2.1-only", - "packageManager": "pnpm@9.5.0" + "packageManager": "pnpm@9.9.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 956fa2f80..254f406ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,8 +136,8 @@ importers: version: 3.23.8 devDependencies: '@amityco/ts-sdk': - specifier: ^6.27.0 - version: 6.27.0 + specifier: ^6.30.0 + version: 6.30.0 '@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.27.0': - resolution: {integrity: sha512-6I55XYqhet6MDlB/qw9YunFO1BD2TwZSr2BBZ3+dWsBjBVp8BTDr7rPy74gpoVmPWRVbD4XTRM1c8dPeWeLEPQ==} + '@amityco/ts-sdk@6.30.0': + resolution: {integrity: sha512-kyW0ytV6Go3tAZ5EFmzeVLm2PE2jzgmlKnKygpK9iJPgY46wsGCVkhRFMCLMXGvpNPz3D1Tnmq+83wL/xNTJKg==} engines: {node: '>=12', npm: '>=6'} '@ampproject/remapping@2.3.0': @@ -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.27.0': + '@amityco/ts-sdk@6.30.0': dependencies: agentkeepalive: 4.5.0 axios: 1.7.2(debug@4.3.5) diff --git a/src/chat/components/Chat/index.tsx b/src/chat/components/Chat/index.tsx index 917502530..b8df3a044 100644 --- a/src/chat/components/Chat/index.tsx +++ b/src/chat/components/Chat/index.tsx @@ -13,6 +13,8 @@ import ChatHeader from '~/chat/components/ChatHeader'; import { ChannelContainer } from './styles'; import { useCustomComponent } from '~/core/providers/CustomComponentsProvider'; +import { useChannelPermission } from '~/chat/hooks/useChannelPermission'; +import useChannel from '~/chat/hooks/useChannel'; interface ChatProps { channelId: string; @@ -27,6 +29,9 @@ const Chat = ({ channelId, onChatDetailsClick, shouldShowChatDetails }: ChatProp }; }, [channelId]); + const { isModerator } = useChannelPermission(channelId); + const channel = useChannel(channelId); + const sendMessage = async (text: string) => { return MessageRepository.createMessage({ subChannelId: channelId, @@ -35,6 +40,13 @@ const Chat = ({ channelId, onChatDetailsClick, shouldShowChatDetails }: ChatProp }); }; + const renderMessageComposeBar = () => { + if (channel?.type !== 'broadcast' || (channel?.type === 'broadcast' && isModerator)) { + return ; + } + return null; + }; + return ( - + {renderMessageComposeBar()} ); }; diff --git a/src/chat/hooks/collections/useChannelMembersCollection.ts b/src/chat/hooks/collections/useChannelMembersCollection.ts index 7042aa42c..80e4288d0 100644 --- a/src/chat/hooks/collections/useChannelMembersCollection.ts +++ b/src/chat/hooks/collections/useChannelMembersCollection.ts @@ -7,7 +7,7 @@ import useLiveCollection from '~/core/hooks/useLiveCollection'; export default function useChannelMembersCollection(channelId?: string) { const { items, ...rest } = useLiveCollection({ fetcher: ChannelRepository.Membership.getMembers, - params: { channelId: channelId as string }, + params: { channelId: channelId as string, includeDeleted: false }, shouldCall: () => !!channelId, }); diff --git a/src/chat/hooks/useChannelPermission.ts b/src/chat/hooks/useChannelPermission.ts new file mode 100644 index 000000000..a1ae6bf54 --- /dev/null +++ b/src/chat/hooks/useChannelPermission.ts @@ -0,0 +1,18 @@ +import { useMemo } from 'react'; +import useSDK from '~/core/hooks/useSDK'; + +export const useChannelPermission = (subChannelId?: Amity.SubChannel['subChannelId']) => { + const { client } = useSDK(); + + const isModerator = useMemo(() => { + if (!subChannelId) return false; + const currentUser = client?.hasPermission('MUTE_CHANNEL').currentUser() || false; + const currentUserInChannel = + client?.hasPermission('MUTE_CHANNEL').channel(subChannelId) || false; + return currentUser || currentUserInChannel; + }, [subChannelId]); + + return { + isModerator, + }; +}; diff --git a/src/core/components/Confirm/index.tsx b/src/core/components/Confirm/index.tsx index 9470c6621..9abd1ae94 100644 --- a/src/core/components/Confirm/index.tsx +++ b/src/core/components/Confirm/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useConfirmContext } from '~/core/providers/ConfirmProvider'; +import { ModalContainer } from '../ModalContainer'; import { ConfirmModal, @@ -61,9 +62,14 @@ export const ConfirmComponent = () => { const onOk = () => { closeConfirm(); confirmData?.onOk && confirmData.onOk(); + confirmData?.onSuccess && confirmData?.onSuccess(); }; - return ; + return ( + + + + ); }; export default Confirm; diff --git a/src/core/components/Dropdown/styles.tsx b/src/core/components/Dropdown/styles.tsx index a9c005e92..748f10110 100644 --- a/src/core/components/Dropdown/styles.tsx +++ b/src/core/components/Dropdown/styles.tsx @@ -26,7 +26,7 @@ export const Frame = styled.div<{ scrollableHeight?: number; }>` position: absolute; - z-index: 2; + z-index: 10; ${({ position }) => getCssPosition(position)} ${({ align }) => align && getCssPosition(align)} background: ${({ theme }) => theme.palette.system.background}; diff --git a/src/core/components/GalleryGrid/TruncatedGrid.tsx b/src/core/components/GalleryGrid/TruncatedGrid.tsx index d6f22d283..96bee1b42 100644 --- a/src/core/components/GalleryGrid/TruncatedGrid.tsx +++ b/src/core/components/GalleryGrid/TruncatedGrid.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import cx from 'clsx'; import Square from '~/core/components/Square'; @@ -34,64 +34,101 @@ import Square from '~/core/components/Square'; => ((100% / 3) / .75) */ -const Gallery = styled.div<{ count?: number }>` +const Gallery = styled.div<{ count?: number; grid?: boolean }>` display: grid; width: 100%; - height: 100%; - grid-template-columns: repeat(6, 1fr); - grid-template-rows: 1fr calc((100% / 3) / 0.75); - grid-gap: 0.5rem; - border-radius: 4px; - - &.one > :nth-child(1) { - grid-column: 1 / 7; - grid-row: 1 / 3; - } - - &.two > :nth-child(1) { - grid-column: 1 / 4; - grid-row: 1 / 3; - } - - &.two > :nth-child(2) { - grid-column: 4 / 7; - grid-row: 1 / 3; - } - - &.three > :nth-child(1), - &.four > :nth-child(1), - &.many > :nth-child(1) { - grid-column: 1 / 7; - grid-row: 1 / 2; - } - &.three > :nth-child(2) { - grid-column: 1 / 4; - grid-row: 2 / 3; - } - - &.three > :nth-child(3) { - grid-column: 4 / 7; - grid-row: 2 / 3; - } - - &.four > :nth-child(2), - &.many > :nth-child(2) { - grid-column: 1 / 3; - grid-row: 2 / 3; - } - - &.four > :nth-child(3), - &.many > :nth-child(3) { - grid-column: 3 / 5; - grid-row: 2 / 3; - } + gap: 0.5rem; + border-radius: 4px; - &.four > :nth-child(4), - &.many > :nth-child(4) { - grid-column: 5 / 7; - grid-row: 2 / 3; - } + ${({ grid }) => + grid && + css` + grid-template-columns: repeat(auto-fill, minmax(30%, 1fr)); + + > * { + position: relative; + box-sizing: border-box; + + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + + > * { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + } + + &::before { + content: ''; + display: block; + padding-top: 100%; + } + } + `} + + ${({ grid }) => + !grid && + css` + height: 100%; + grid-template-columns: repeat(6, 1fr); + grid-template-rows: 1fr calc((100% / 3) / 0.75); + + &.one > :nth-child(1) { + grid-column: 1 / 7; + grid-row: 1 / 3; + } + + &.two > :nth-child(1) { + grid-column: 1 / 4; + grid-row: 1 / 3; + } + + &.two > :nth-child(2) { + grid-column: 4 / 7; + grid-row: 1 / 3; + } + + &.three > :nth-child(1), + &.four > :nth-child(1), + &.many > :nth-child(1) { + grid-column: 1 / 7; + grid-row: 1 / 2; + } + + &.three > :nth-child(2) { + grid-column: 1 / 4; + grid-row: 2 / 3; + } + + &.three > :nth-child(3) { + grid-column: 4 / 7; + grid-row: 2 / 3; + } + + &.four > :nth-child(2), + &.many > :nth-child(2) { + grid-column: 1 / 3; + grid-row: 2 / 3; + } + + &.four > :nth-child(3), + &.many > :nth-child(3) { + grid-column: 3 / 5; + grid-row: 2 / 3; + } + + &.four > :nth-child(4), + &.many > :nth-child(4) { + grid-column: 5 / 7; + grid-row: 2 / 3; + } + `} `; export const TruncatedGridCell = styled.div` @@ -128,6 +165,7 @@ interface TruncatedGridProps { itemKey?: keyof T; onClick?: (index: number) => void; renderItem: (item: T) => React.ReactNode; + grid?: boolean; } const TruncatedGrid = ({ @@ -136,6 +174,7 @@ const TruncatedGrid = ({ renderItem, items = [], itemKey, + grid, }: TruncatedGridProps) => { const config = useMemo(() => { switch (items.length) { @@ -154,7 +193,7 @@ const TruncatedGrid = ({ return ( - + {items.slice(0, 3).map((item, index) => ( { truncate?: boolean; onClick?: (index: number) => void; renderItem: (item: T) => React.ReactNode; + grid?: boolean; } const GalleryGrid = ({ @@ -19,6 +20,7 @@ const GalleryGrid = ({ truncate, onClick, renderItem, + grid, }: GalleryGridProps) => { if (truncate || items.length <= 4) { return ( @@ -28,6 +30,7 @@ const GalleryGrid = ({ renderItem={renderItem} items={items} itemKey={itemKey} + grid={grid} /> ); } diff --git a/src/core/components/ImageGallery/index.tsx b/src/core/components/ImageGallery/index.tsx index cb27222ba..8dbd889f7 100644 --- a/src/core/components/ImageGallery/index.tsx +++ b/src/core/components/ImageGallery/index.tsx @@ -2,8 +2,18 @@ import React, { ReactNode } from 'react'; import useKeyboard from '~/core/hooks/useKeyboard'; -import { Container, Frame, Counter, LeftButton, RightButton, CloseButton } from './styles'; +import { + Container, + Frame, + Counter, + LeftButton, + RightButton, + CloseButton, + InnerContainer, + GridContainer, +} from './styles'; import { useCustomComponent } from '~/core/providers/CustomComponentsProvider'; +import { useMedia } from 'react-use'; interface ImageGalleryProps { index?: number; @@ -29,16 +39,40 @@ const ImageGallery = ({ useKeyboard('ArrowRight', next); useKeyboard('Escape', handleClose); + const isDesktop = useMedia('(min-width: 768px)'); + + if (isDesktop) { + return ( + + + + {renderItem(items[index])} + + {showCounter && {`${index + 1} / ${items.length}`}} + + {items.length > 1 && } + {items.length > 1 && } + + + + + + ); + } + return ( - {renderItem(items[index])} - - {showCounter && {`${index + 1} / ${items.length}`}} + + {renderItem(items[index])} + + {showCounter && {`${index + 1} / ${items.length}`}} - {items.length > 1 && } - {items.length > 1 && } + {items.length > 1 && } + {items.length > 1 && } - + + + ); }; diff --git a/src/core/components/ImageGallery/styles.tsx b/src/core/components/ImageGallery/styles.tsx index 583cb6cc7..a00999bf3 100644 --- a/src/core/components/ImageGallery/styles.tsx +++ b/src/core/components/ImageGallery/styles.tsx @@ -8,14 +8,6 @@ export const Container = styled.div` position: fixed; overflow: hidden; - display: grid; - grid-gap: 1rem 3rem; - grid-template-columns: 2rem auto 2rem; - grid-template-rows: min-content auto; - grid-template-areas: - 'none counter close' - 'left image right'; - align-items: center; top: 0; @@ -24,7 +16,6 @@ export const Container = styled.div` left: 0; width: 100vw; height: 100vh; - padding: 3rem; background: rgba(0, 0, 0, 0.75); color: ${({ theme }) => theme.palette.system.background}; @@ -45,6 +36,34 @@ export const Container = styled.div` } `; +export const InnerContainer = styled.div` + z-index: 9999; + width: 100%; + height: 100%; + position: relative; +`; + +export const GridContainer = styled.div` + position: relative; + z-index: 9999; + display: grid; + grid-gap: 1rem 3rem; + grid-template-columns: 2rem auto 2rem; + grid-template-rows: min-content auto; + grid-template-areas: + 'none counter close' + 'left image right'; + + width: 100%; + height: 100%; + + padding: 2rem 1rem 2rem 1rem; + + @media (min-width: 768px) { + padding: 3rem; + } +`; + const Image = styled.img.attrs({ loading: 'lazy' })` display: block; width: 100%; @@ -56,10 +75,21 @@ const Image = styled.img.attrs({ loading: 'lazy' })` export const ImageRenderer = (url: string) => ; export const Frame = styled.div` - grid-area: image; + z-index: 9999; width: 100%; height: 100%; overflow: hidden; + + position: absolute; + top: 0; + left: 0; + + @media (min-width: 768px) { + position: unset; + top: unset; + left: unset; + grid-area: image; + } `; export const Counter = styled.div` diff --git a/src/core/components/Modal/styles.tsx b/src/core/components/Modal/styles.tsx index 1e277816f..98f3543ec 100644 --- a/src/core/components/Modal/styles.tsx +++ b/src/core/components/Modal/styles.tsx @@ -44,10 +44,16 @@ export const ModalWindow = styled.div` max-width: 520px; min-width: 360px; ${({ theme }) => theme.typography.body} + color: ${({ theme }) => theme.palette.neutral.main}; &:focus { outline: none; } + + @media (max-width: 520px) { + width: 95vw; + min-width: unset; + } `; const SmallModalWindow = styled(ModalWindow)` diff --git a/src/core/components/ModalContainer.tsx b/src/core/components/ModalContainer.tsx new file mode 100644 index 000000000..6a5a6db0c --- /dev/null +++ b/src/core/components/ModalContainer.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { createPortal } from 'react-dom'; + +export const ModalContainer = ({ children }: { children: React.ReactNode }) => { + return createPortal(children, document.body); +}; diff --git a/src/core/components/Notification/index.tsx b/src/core/components/Notification/index.tsx index 0a84c14fa..b494a7049 100644 --- a/src/core/components/Notification/index.tsx +++ b/src/core/components/Notification/index.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import { useNotificationData } from '~/core/providers/NotificationProvider'; +import { ModalContainer } from '../ModalContainer'; import { NotificationContainer, Notifications } from './styles'; @@ -20,11 +21,13 @@ export const NotificationsContainer = () => { const notifications = useNotificationData(); return ( - - {notifications.map((notificationData) => { - return ; - })} - + + + {notifications.map((notificationData) => { + return ; + })} + + ); }; diff --git a/src/core/components/Notification/styles.tsx b/src/core/components/Notification/styles.tsx index 0fbc266c6..b8ebf89c0 100644 --- a/src/core/components/Notification/styles.tsx +++ b/src/core/components/Notification/styles.tsx @@ -54,4 +54,12 @@ export const NotificationContainer = styled.div` } } pointer-events: auto; + + @media (max-width: 780px) { + width: calc(100% - 32px); + padding: 8px 16px; + display: flex; + justify-content: center; + text-align: center; + } `; diff --git a/src/core/components/SideMenuActionItem/index.tsx b/src/core/components/SideMenuActionItem/index.tsx index 7af742320..c07b7342a 100644 --- a/src/core/components/SideMenuActionItem/index.tsx +++ b/src/core/components/SideMenuActionItem/index.tsx @@ -26,7 +26,12 @@ const SideMenuActionItem = ({ }: SideMenuActionItemProps) => { if (element === 'a') { return ( - + {icon && {icon}} {children} diff --git a/src/core/components/SideMenuActionItem/styles.tsx b/src/core/components/SideMenuActionItem/styles.tsx index 1b5dcf3a6..aa464daaa 100644 --- a/src/core/components/SideMenuActionItem/styles.tsx +++ b/src/core/components/SideMenuActionItem/styles.tsx @@ -20,7 +20,7 @@ const actionItemContainerStyles = css` color: ${({ theme }) => theme.palette.neutral.main}; justify-content: left; &:hover:not(:disabled) { - background-color: ${({ theme }) => theme.palette.base.shade4}; + background-color: ${({ theme }) => theme.palette.primary.shade3}; } &:disabled { color: ${({ theme }) => theme.palette.neutral.shade2}; @@ -34,11 +34,17 @@ export const ButtonActionItem = styled(SecondaryButton)` width: 100%; `; -export const AnchorActionItem = styled.a` +export const AnchorActionItem = styled.a<{ active?: boolean }>` cursor: pointer; border-radius: 4px; ${actionItemContainerStyles} ${({ theme }) => theme.typography.bodyBold} + ${({ active, theme }) => + active && + css` + color: ${theme.palette.primary.main}; + background-color: ${theme.palette.primary.shade3}; + `}; `; export const IconWrapper = styled.div<{ active?: boolean }>` diff --git a/src/core/components/Uploaders/File/StyledFile.tsx b/src/core/components/Uploaders/File/StyledFile.tsx index 33f473150..b6f7151c6 100644 --- a/src/core/components/Uploaders/File/StyledFile.tsx +++ b/src/core/components/Uploaders/File/StyledFile.tsx @@ -79,7 +79,8 @@ const StyledFile = ({ )} diff --git a/src/core/providers/ConfirmProvider.tsx b/src/core/providers/ConfirmProvider.tsx index 2e70970d5..7441aa146 100644 --- a/src/core/providers/ConfirmProvider.tsx +++ b/src/core/providers/ConfirmProvider.tsx @@ -12,6 +12,7 @@ type ConfirmType = { okText?: ReactNode; cancelText?: ReactNode; 'data-qa-anchor'?: string; + onSuccess?: () => void; }; interface ConfirmContextProps { diff --git a/src/core/providers/UiKitProvider/fonts/inter.css b/src/core/providers/UiKitProvider/fonts/inter.css index d92263820..302e658ed 100644 --- a/src/core/providers/UiKitProvider/fonts/inter.css +++ b/src/core/providers/UiKitProvider/fonts/inter.css @@ -3,14 +3,17 @@ font-style: normal; font-weight: 100; font-display: swap; - src: url('Inter-Thin.woff2?v=3.19') format('woff2'), url('Inter-Thin.woff?v=3.19') format('woff'); + src: + url('Inter-Thin.woff2?v=3.19') format('woff2'), + url('Inter-Thin.woff?v=3.19') format('woff'); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 100; font-display: swap; - src: url('Inter-ThinItalic.woff2?v=3.19') format('woff2'), + src: + url('Inter-ThinItalic.woff2?v=3.19') format('woff2'), url('Inter-ThinItalic.woff?v=3.19') format('woff'); } @@ -19,7 +22,8 @@ font-style: normal; font-weight: 200; font-display: swap; - src: url('Inter-ExtraLight.woff2?v=3.19') format('woff2'), + src: + url('Inter-ExtraLight.woff2?v=3.19') format('woff2'), url('Inter-ExtraLight.woff?v=3.19') format('woff'); } @font-face { @@ -27,7 +31,8 @@ font-style: italic; font-weight: 200; font-display: swap; - src: url('Inter-ExtraLightItalic.woff2?v=3.19') format('woff2'), + src: + url('Inter-ExtraLightItalic.woff2?v=3.19') format('woff2'), url('Inter-ExtraLightItalic.woff?v=3.19') format('woff'); } @@ -36,7 +41,8 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url('Inter-Light.woff2?v=3.19') format('woff2'), + src: + url('Inter-Light.woff2?v=3.19') format('woff2'), url('Inter-Light.woff?v=3.19') format('woff'); } @font-face { @@ -44,7 +50,8 @@ font-style: italic; font-weight: 300; font-display: swap; - src: url('Inter-LightItalic.woff2?v=3.19') format('woff2'), + src: + url('Inter-LightItalic.woff2?v=3.19') format('woff2'), url('Inter-LightItalic.woff?v=3.19') format('woff'); } @@ -53,7 +60,8 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url('Inter-Regular.woff2?v=3.19') format('woff2'), + src: + url('Inter-Regular.woff2?v=3.19') format('woff2'), url('Inter-Regular.woff?v=3.19') format('woff'); } @font-face { @@ -61,7 +69,8 @@ font-style: italic; font-weight: 400; font-display: swap; - src: url('Inter-Italic.woff2?v=3.19') format('woff2'), + src: + url('Inter-Italic.woff2?v=3.19') format('woff2'), url('Inter-Italic.woff?v=3.19') format('woff'); } @@ -70,7 +79,8 @@ font-style: normal; font-weight: 500; font-display: swap; - src: url('Inter-Medium.woff2?v=3.19') format('woff2'), + src: + url('Inter-Medium.woff2?v=3.19') format('woff2'), url('Inter-Medium.woff?v=3.19') format('woff'); } @font-face { @@ -78,7 +88,8 @@ font-style: italic; font-weight: 500; font-display: swap; - src: url('Inter-MediumItalic.woff2?v=3.19') format('woff2'), + src: + url('Inter-MediumItalic.woff2?v=3.19') format('woff2'), url('Inter-MediumItalic.woff?v=3.19') format('woff'); } @@ -87,7 +98,8 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url('Inter-SemiBold.woff2?v=3.19') format('woff2'), + src: + url('Inter-SemiBold.woff2?v=3.19') format('woff2'), url('Inter-SemiBold.woff?v=3.19') format('woff'); } @font-face { @@ -95,7 +107,8 @@ font-style: italic; font-weight: 600; font-display: swap; - src: url('Inter-SemiBoldItalic.woff2?v=3.19') format('woff2'), + src: + url('Inter-SemiBoldItalic.woff2?v=3.19') format('woff2'), url('Inter-SemiBoldItalic.woff?v=3.19') format('woff'); } @@ -104,14 +117,17 @@ font-style: normal; font-weight: 700; font-display: swap; - src: url('Inter-Bold.woff2?v=3.19') format('woff2'), url('Inter-Bold.woff?v=3.19') format('woff'); + src: + url('Inter-Bold.woff2?v=3.19') format('woff2'), + url('Inter-Bold.woff?v=3.19') format('woff'); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 700; font-display: swap; - src: url('Inter-BoldItalic.woff2?v=3.19') format('woff2'), + src: + url('Inter-BoldItalic.woff2?v=3.19') format('woff2'), url('Inter-BoldItalic.woff?v=3.19') format('woff'); } @@ -120,7 +136,8 @@ font-style: normal; font-weight: 800; font-display: swap; - src: url('Inter-ExtraBold.woff2?v=3.19') format('woff2'), + src: + url('Inter-ExtraBold.woff2?v=3.19') format('woff2'), url('Inter-ExtraBold.woff?v=3.19') format('woff'); } @font-face { @@ -128,7 +145,8 @@ font-style: italic; font-weight: 800; font-display: swap; - src: url('Inter-ExtraBoldItalic.woff2?v=3.19') format('woff2'), + src: + url('Inter-ExtraBoldItalic.woff2?v=3.19') format('woff2'), url('Inter-ExtraBoldItalic.woff?v=3.19') format('woff'); } @@ -137,7 +155,8 @@ font-style: normal; font-weight: 900; font-display: swap; - src: url('Inter-Black.woff2?v=3.19') format('woff2'), + src: + url('Inter-Black.woff2?v=3.19') format('woff2'), url('Inter-Black.woff?v=3.19') format('woff'); } @font-face { @@ -145,7 +164,8 @@ font-style: italic; font-weight: 900; font-display: swap; - src: url('Inter-BlackItalic.woff2?v=3.19') format('woff2'), + src: + url('Inter-BlackItalic.woff2?v=3.19') format('woff2'), url('Inter-BlackItalic.woff?v=3.19') format('woff'); } diff --git a/src/core/providers/UiKitProvider/index.css b/src/core/providers/UiKitProvider/index.css deleted file mode 100644 index a1f55f93f..000000000 --- a/src/core/providers/UiKitProvider/index.css +++ /dev/null @@ -1,55 +0,0 @@ -@keyframes react-loading-skeleton { - 100% { - transform: translateX(100%); - } -} - -.react-loading-skeleton { - --base-color: #ebebeb; - --highlight-color: #f5f5f5; - --animation-duration: 1.5s; - --animation-direction: normal; - --pseudo-element-display: block; /* Enable animation */ - - background-color: var(--base-color); - - width: 100%; - border-radius: 0.25rem; - display: inline-flex; - line-height: 1; - - position: relative; - user-select: none; - overflow: hidden; - z-index: 1; /* Necessary for overflow: hidden to work correctly in Safari */ -} - -.react-loading-skeleton::after { - content: ' '; - display: var(--pseudo-element-display); - position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; - background-repeat: no-repeat; - background-image: linear-gradient( - 90deg, - var(--base-color), - var(--highlight-color), - var(--base-color) - ); - transform: translateX(-100%); - - animation-name: react-loading-skeleton; - animation-direction: var(--animation-direction); - animation-duration: var(--animation-duration); - animation-timing-function: ease-in-out; - animation-iteration-count: infinite; -} - -@media (prefers-reduced-motion) { - .react-loading-skeleton { - --pseudo-element-display: none; /* Disable animation */ - } -} diff --git a/src/core/providers/UiKitProvider/index.tsx b/src/core/providers/UiKitProvider/index.tsx index 00bcb24b6..06a5f433a 100644 --- a/src/core/providers/UiKitProvider/index.tsx +++ b/src/core/providers/UiKitProvider/index.tsx @@ -2,7 +2,7 @@ import './inter.css'; import React, { useState, useEffect, useMemo, useRef } from 'react'; import { Client as ASCClient } from '@amityco/ts-sdk'; -import { ThemeProvider } from 'styled-components'; +import { ThemeProvider, createGlobalStyle } from 'styled-components'; import { NotificationsContainer } from '~/core/components/Notification'; import { ConfirmComponent } from '~/core/components/Confirm'; import { NotificationsContainer as NotificationsContainerV4 } from '~/v4/core/components/Notification'; diff --git a/src/core/providers/UiKitProvider/styles.tsx b/src/core/providers/UiKitProvider/styles.tsx index 04dd10f86..9c8cd6f54 100644 --- a/src/core/providers/UiKitProvider/styles.tsx +++ b/src/core/providers/UiKitProvider/styles.tsx @@ -1,6 +1,9 @@ import styled from 'styled-components'; +import skeletonCss from 'react-loading-skeleton/dist/skeleton.css?inline'; + export const UIStyles = styled.div` + color-scheme: only light; ${({ theme }) => theme.typography.body}; color: ${({ theme }) => theme.palette.base.main}; width: 100%; @@ -20,4 +23,12 @@ export const UIStyles = styled.div` & pre { ${({ theme }) => theme.typography.body} } + + @keyframes react-loading-skeleton { + 100% { + transform: translateX(100%); + } + } + + ${skeletonCss} `; diff --git a/src/global.d.ts b/src/global.d.ts index a52b35ee9..6bc62bb56 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -14,6 +14,11 @@ interface ImportMeta { readonly env: ImportMetaEnv; } +declare module '*.css?inline' { + const classes: string; + export default classes; +} + declare module '*.module.css' { const classes: { [key: string]: string }; export default classes; diff --git a/src/i18n/en.json b/src/i18n/en.json index 458f64e2d..3bd42e1d2 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -204,6 +204,7 @@ "files.all": "View all files", "sidebar.community": "Community", + "sidebar.explore": "Explore", "CategoryCommunitiesList.emptyTitle": "It's empty here...", "CategoryCommunitiesList.emptyDescription": "No community found in this category", diff --git a/src/index.ts b/src/index.ts index eaacd94d9..0987ab370 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,10 +20,6 @@ export { useSDK as useAmitySDK } from '~/core/hooks/useSDK'; // v4 export { AmityUIKitManager } from '~/v4/core/AmityUIKitManager'; -export { - CommentTray as AmityCommentTrayComponent, - StoryTab as AmityStoryTabComponent, -} from '~/v4/social/components'; // Chat v4 @@ -60,6 +56,7 @@ export { MyCommunitiesSearchPage as AmityMyCommunitiesSearchPage, SelectPostTargetPage as AmityPostTargetSelectionPage, PostComposerPage as AmityPostComposerPage, + CommunityProfilePage as AmityCommunityProfilePage, } from '~/v4/social/pages'; export { @@ -76,6 +73,11 @@ export { CreatePostMenu as AmityCreatePostMenuComponent, ReactionList as AmityReactionListComponent, TopNavigation as AmitySocialHomeTopNavigationComponent, + CommentTray as AmityCommentTrayComponent, + StoryTab as AmityStoryTabComponent, + CommunityHeader as AmityCommunityHeaderComponent, + CommunityFeed as AmityCommunityFeedComponent, + CommunityPinnedPost as AmityCommunityPinnedPostComponent, } from '~/v4/social/components/'; export { HomePageTab as AmitySocialHomePageTab } from '~/v4/social/pages/SocialHomePage'; diff --git a/src/social/components/Comment/StyledComment.tsx b/src/social/components/Comment/StyledComment.tsx index 6958cacd3..3a082e065 100644 --- a/src/social/components/Comment/StyledComment.tsx +++ b/src/social/components/Comment/StyledComment.tsx @@ -170,6 +170,7 @@ interface StyledCommentProps { isBanned?: boolean; mentionees?: Mentioned[]; metadata?: Metadata; + onClickUser?: () => void; } const StyledComment = (props: StyledCommentProps) => { @@ -192,6 +193,7 @@ const StyledComment = (props: StyledCommentProps) => { queryMentionees, isBanned, mentionees, + onClickUser, } = props; const { formatMessage } = useIntl(); @@ -214,7 +216,7 @@ const StyledComment = (props: StyledCommentProps) => { return ( <> - + { lines={2} > - {authorName} + {authorName} + <> {isBanned && ( diff --git a/src/social/components/Comment/index.tsx b/src/social/components/Comment/index.tsx index f032b7290..b0d5089af 100644 --- a/src/social/components/Comment/index.tsx +++ b/src/social/components/Comment/index.tsx @@ -38,6 +38,7 @@ import useCommentSubscription from '~/social/hooks/useCommentSubscription'; import { ERROR_RESPONSE } from '~/social/constants'; import { useConfirmContext } from '~/core/providers/ConfirmProvider'; import { useNotifications } from '~/core/providers/NotificationProvider'; +import { useNavigation } from '~/social/providers/NavigationProvider'; const REPLIES_PER_PAGE = 5; @@ -87,6 +88,7 @@ interface CommentProps { } const Comment = ({ commentId, readonly }: CommentProps) => { + const { onClickUser } = useNavigation(); const comment = useComment(commentId); const post = usePost(comment?.referenceId); const { confirm } = useConfirmContext(); @@ -101,10 +103,6 @@ const Comment = ({ commentId, readonly }: CommentProps) => { const { formatMessage } = useIntl(); const [isExpanded, setExpanded] = useState(false); - useCommentSubscription({ - commentId, - }); - const { text, markup, mentions, onChange, queryMentionees, resetState, clearAll } = useSocialMention({ targetId: post?.targetId, @@ -250,6 +248,7 @@ const Comment = ({ commentId, readonly }: CommentProps) => { isReplyComment={isReplyComment} onClickReply={onClickReply} onChange={onChange} + onClickUser={() => onClickUser(comment.userId)} /> ); diff --git a/src/social/components/Comment/styles.tsx b/src/social/components/Comment/styles.tsx index ef860fc01..fc93fc08c 100644 --- a/src/social/components/Comment/styles.tsx +++ b/src/social/components/Comment/styles.tsx @@ -91,6 +91,8 @@ export const AuthorName = styled.span` // react-truncate-markup tries to set to inline-block display: inline !important; ${({ theme }) => theme.typography.body} + cursor: pointer; + font-weight: 600; `; export const CommentDate = styled(Time)` diff --git a/src/social/components/CommentLikeButton/index.tsx b/src/social/components/CommentLikeButton/index.tsx index 9a1e67988..9acf233b8 100644 --- a/src/social/components/CommentLikeButton/index.tsx +++ b/src/social/components/CommentLikeButton/index.tsx @@ -18,16 +18,6 @@ const CommentLikeButton = ({ }: CommentLikeButtonProps) => { const comment = useComment(commentId); - useUserReactionSubscription({ - userId: comment?.targetId, - shouldSubscribe: () => comment?.targetType === 'user', - }); - - useCommunityReactionSubscription({ - communityId: comment?.targetId, - shouldSubscribe: () => comment?.targetType === 'community', - }); - return ( { - const { - parentId, - referenceId, - referenceType, - limit = 5, - // TODO: breaking change - // filterByParentId = false, - readonly = false, - isExpanded = true, - } = props; - const { formatMessage } = useIntl(); - const [firstRender, setFirstRender] = useState(true); - const isReplyComment = !!parentId; - const post = usePost(referenceId); - - usePostSubscription({ - postId: referenceId, - level: SubscriptionLevels.COMMENT, - shouldSubscribe: () => referenceType === 'post' && !parentId, - }); - - const loadMoreText = isReplyComment - ? formatMessage({ id: 'collapsible.viewMoreReplies' }) - : formatMessage({ id: 'collapsible.viewMoreComments' }); - - const prependIcon = isReplyComment ? ( - - - - ) : null; - - if (firstRender) { - return ( - (post?.latestComments?.length || 0)} - loadMore={() => { - setFirstRender(false); - }} - text={loadMoreText} - className={isReplyComment ? 'reply-button' : 'comments-button'} - prependIcon={prependIcon} - appendIcon={null} - isExpanded={isExpanded} - contentSlot={(post?.latestComments || []).map((comment: Amity.Comment) => ( - - ))} - /> - ); - } - return ; }; diff --git a/src/social/components/CommunityForm/AvatarUploader.tsx b/src/social/components/CommunityForm/AvatarUploader.tsx index c5862c7cc..49648f22a 100644 --- a/src/social/components/CommunityForm/AvatarUploader.tsx +++ b/src/social/components/CommunityForm/AvatarUploader.tsx @@ -41,6 +41,13 @@ const AvatarUploadButton = styled.div` padding: 10px 16px; border-radius: 4px; color: #ffffff; + @media (max-width: 768px) { + display: flex; + flex-direction: column; + gap: 4px; + align-items: center; + text-align: center; + } `; const CoverImageLoader = styled(Loader)` diff --git a/src/social/components/CommunityForm/styles.tsx b/src/social/components/CommunityForm/styles.tsx index b1f4c9ade..568e302ff 100644 --- a/src/social/components/CommunityForm/styles.tsx +++ b/src/social/components/CommunityForm/styles.tsx @@ -44,6 +44,9 @@ export const Selector = styled.div` cursor: pointer; max-height: 200px; overflow-y: auto; + @media (max-width: 768px) { + max-height: unset; + } `; export const IconWrapper = styled.div` @@ -66,7 +69,7 @@ export const Counter = styled.div` export const Label = styled.label` ${({ theme }) => theme.typography.bodyBold}; - margin-bottom: 4px; + margin: 8px 0; ${({ theme }) => css` &.required { &::after { @@ -92,6 +95,10 @@ export const Radio = styled.input.attrs({ type: 'radio' })` export const Form = styled.form` min-width: 520px; + + @media (max-width: 768px) { + min-width: 100%; + } `; export const SubmitButton = styled(PrimaryButton).attrs<{ edit?: boolean }>({ @@ -120,9 +127,6 @@ export const FormBlockContainer = styled.div<{ edit?: boolean }>` ${({ theme, edit }) => edit ? css` - > :not(:first-child) { - margin-top: 12px; - } border: 1px solid #edeef2; border-radius: 4px; ` @@ -163,6 +167,10 @@ export const Description = styled.div` color: ${({ theme }) => theme.palette.base.shade1}; ${({ theme }) => theme.typography.body}; width: 357px; + + @media (max-width: 768px) { + width: unset; + } `; export const InformationBlock = styled.div` @@ -240,6 +248,7 @@ export const AboutTextarea = styled(TextareaAutosize).attrs({ minRows: 3, maxRow resize: none; border: 1px solid #e3e4e8; padding: 10px 12px; + background: ${({ theme }) => theme.palette.system.background}; &:focus-within { border-color: ${({ theme }) => theme.palette.primary.main}; } @@ -261,11 +270,11 @@ export const SelectIcon = styled(ChevronDown).attrs({ width: 16, height: 16 })` `; export const Field = styled.div<{ error?: ReactNode }>` - > :not(:first-child) { - margin-top: 20px; - } display: flex; flex-direction: column; + gap: 8px; + padding-top: 8px; + padding-bottom: 8px; ${({ error }) => error && diff --git a/src/social/components/CommunityMembers/index.tsx b/src/social/components/CommunityMembers/index.tsx index f8b66c81d..1abe0d463 100644 --- a/src/social/components/CommunityMembers/index.tsx +++ b/src/social/components/CommunityMembers/index.tsx @@ -199,11 +199,6 @@ interface CommunityMembersProps { const CommunityMembers = ({ communityId }: CommunityMembersProps) => { const { formatMessage } = useIntl(); - useCommunitySubscription({ - level: SubscriptionLevels.COMMUNITY, - communityId, - }); - const { hasMore, loadMore, loadMoreHasBeenCalled, isLoading, members } = useCommunityMembersCollection(communityId); diff --git a/src/social/components/EngagementBar/UIEngagementBar.tsx b/src/social/components/EngagementBar/UIEngagementBar.tsx index 7abef7342..8b155d4f4 100644 --- a/src/social/components/EngagementBar/UIEngagementBar.tsx +++ b/src/social/components/EngagementBar/UIEngagementBar.tsx @@ -38,11 +38,6 @@ const UIEngagementBar = ({ }: UIEngagementBarProps) => { const { postId, targetType, targetId, reactions = {}, commentsCount, latestComments } = post; - usePostSubscription({ - postId, - level: SubscriptionLevels.POST, - }); - const totalLikes = reactions[LIKE_REACTION_KEY] || 0; return ( diff --git a/src/social/components/EngagementBar/index.tsx b/src/social/components/EngagementBar/index.tsx index 23887c296..77cbcec7d 100644 --- a/src/social/components/EngagementBar/index.tsx +++ b/src/social/components/EngagementBar/index.tsx @@ -30,17 +30,6 @@ const EngagementBar = ({ postId, readonly = false }: EngagementBarProps) => { targetId: post?.targetId, }); - usePostSubscription({ - postId, - level: SubscriptionLevels.POST, - }); - - useReactionSubscription({ - targetType: post?.targetType, - targetId: post?.targetId, - shouldSubscribe: () => !!post, - }); - if (!post) return null; const handleAddComment = async ( diff --git a/src/social/components/Feed/index.tsx b/src/social/components/Feed/index.tsx index c77c83f76..0bf2716e8 100644 --- a/src/social/components/Feed/index.tsx +++ b/src/social/components/Feed/index.tsx @@ -239,11 +239,6 @@ const CommunityFeed = ({ feedType, }); - useCommunitySubscription({ - communityId: targetId, - level: SubscriptionLevels.COMMUNITY, - }); - function renderLoadingSkeleton() { return new Array(3).fill(3).map((_, index) => ); } diff --git a/src/social/components/MediaGallery/index.tsx b/src/social/components/MediaGallery/index.tsx index 7d0a2c69d..b34857704 100644 --- a/src/social/components/MediaGallery/index.tsx +++ b/src/social/components/MediaGallery/index.tsx @@ -13,9 +13,10 @@ import useMediaCollection from '~/social/hooks/collections/useMediaCollection'; interface MediaGalleryProps { targetId?: string | null; targetType: string; + grid?: boolean; } -const MediaGallery = ({ targetId, targetType }: MediaGalleryProps) => { +const MediaGallery = ({ targetId, targetType, grid = false }: MediaGalleryProps) => { const [activeTab, setActiveTab] = useState(EnumContentType.Image); const { media, isLoading, hasMore, loadMore, loadMoreHasBeenCalled } = useMediaCollection({ @@ -43,6 +44,7 @@ const MediaGallery = ({ targetId, targetType }: MediaGalleryProps) => { items={media} loading={isLoading} loadingMore={loadMoreHasBeenCalled} + grid={grid} renderVideoThumbnail={(item) => ( )} diff --git a/src/social/components/ProfileSettings/index.tsx b/src/social/components/ProfileSettings/index.tsx index be00afd2b..42b12c48f 100644 --- a/src/social/components/ProfileSettings/index.tsx +++ b/src/social/components/ProfileSettings/index.tsx @@ -20,18 +20,16 @@ import { Avatar, AvatarContainer, } from './styles'; -import useSDK from '~/core/hooks/useSDK'; -import useFile from '~/core/hooks/useFile'; import { UserRepository } from '@amityco/ts-sdk'; import { useCustomComponent } from '~/core/providers/CustomComponentsProvider'; import useImage from '~/core/hooks/useImage'; +import { useNotifications } from '~/core/providers/NotificationProvider'; interface ProfileSettingsProps { userId?: string; } const ProfileSettings = ({ userId }: ProfileSettingsProps) => { - // const { currentUserId } = useSDK(); const { formatMessage } = useIntl(); const { onClickUser } = useNavigation(); @@ -39,6 +37,13 @@ const ProfileSettings = ({ userId }: ProfileSettingsProps) => { const user = useUser(userId); const avatarFileUrl = useImage({ fileId: user?.avatarFileId, imageSize: 'small' }); + const notification = useNotifications(); + + const handleError = (error: Error) => { + notification.error({ + content: error.message, + }); + }; const handleSubmit = async ( data: Partial> & @@ -49,6 +54,7 @@ const ProfileSettings = ({ userId }: ProfileSettingsProps) => { await UserRepository.updateUser(userId, data); onClickUser(userId); } catch (err) { + handleError(err as Error); console.log(err); } }; diff --git a/src/social/components/ProfileSettings/styles.tsx b/src/social/components/ProfileSettings/styles.tsx index 45ca85c26..41ea8a2fa 100644 --- a/src/social/components/ProfileSettings/styles.tsx +++ b/src/social/components/ProfileSettings/styles.tsx @@ -12,6 +12,10 @@ export const Container = styled.div` display: flex; flex-direction: column; min-width: 600px; + + @media (max-width: 768px) { + min-width: 100%; + } `; export const PageHeader = styled.div` diff --git a/src/social/components/SideSectionMyCommunity/index.tsx b/src/social/components/SideSectionMyCommunity/index.tsx index fad1a9e3f..a8ebdeb54 100644 --- a/src/social/components/SideSectionMyCommunity/index.tsx +++ b/src/social/components/SideSectionMyCommunity/index.tsx @@ -1,5 +1,6 @@ import React, { memo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { ModalContainer } from '~/core/components/ModalContainer'; import SideMenuActionItem from '~/core/components/SideMenuActionItem'; import SideMenuSection from '~/core/components/SideMenuSection'; @@ -22,6 +23,7 @@ const SideSectionMyCommunity = ({ className, activeCommunity }: SideSectionMyCom const open = () => setIsOpen(true); const close = (communityId?: string) => { + console.log('communityId', communityId); setIsOpen(false); communityId && onCommunityCreated(communityId); }; @@ -45,7 +47,9 @@ const SideSectionMyCommunity = ({ className, activeCommunity }: SideSectionMyCom activeCommunity={activeCommunity} /> - + + + ); }; diff --git a/src/social/components/UserInfo/styles.tsx b/src/social/components/UserInfo/styles.tsx index dd0f24f60..bccd3f1df 100644 --- a/src/social/components/UserInfo/styles.tsx +++ b/src/social/components/UserInfo/styles.tsx @@ -44,12 +44,15 @@ export const Avatar = styled(UIAvatar)` export const ActionButtonContainer = styled.div` display: flex; - gap: 8px; margin-right: 8px; > button { - min-width: 136px; - height: 40px; + min-width: 160px; + gap: 8px; + padding: 10px 16px; + @media (max-width: 768px) { + min-width: 130px; + } } `; diff --git a/src/social/components/UserProfileForm/index.tsx b/src/social/components/UserProfileForm/index.tsx index f5b8282c4..d289cc547 100644 --- a/src/social/components/UserProfileForm/index.tsx +++ b/src/social/components/UserProfileForm/index.tsx @@ -4,7 +4,6 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { Controller, useForm } from 'react-hook-form'; import { PrimaryButton } from '~/core/components/Button'; - import AvatarUploader from '~/social/components/CommunityForm/AvatarUploader'; // TODO: should not be importing styles from another component. @@ -22,9 +21,6 @@ import { LabelCounterWrapper, TextField, } from '~/social/components/CommunityForm/styles'; -import useSDK from '~/core/hooks/useSDK'; -import useUser from '~/core/hooks/useUser'; -import { isAdmin as isAdminFn } from '~/helpers/permissions'; const ButtonContainer = styled.div` margin-top: 16px; @@ -47,14 +43,13 @@ interface UserProfileFormProps { onSubmit: ( data: Partial> & Pick, - ) => void; + ) => Promise; className?: string; } const UserProfileForm = ({ user, onSubmit, className }: UserProfileFormProps) => { - const { currentUserId } = useSDK(); - const currentUser = useUser(currentUserId); const { formatMessage } = useIntl(); + const { register, handleSubmit, @@ -69,8 +64,6 @@ const UserProfileForm = ({ user, onSubmit, className }: UserProfileFormProps) => }, }); - const isAdmin = isAdminFn(currentUser?.roles); - const description = watch('description'); const displayName = watch('displayName'); @@ -78,13 +71,13 @@ const UserProfileForm = ({ user, onSubmit, className }: UserProfileFormProps) =>
{ - if (isAdmin) { - onSubmit(data); - } else { + if (user.displayName === data.displayName) { onSubmit({ description: data.description, avatarFileId: data.avatarFileId, }); + } else { + onSubmit(data); } })} > @@ -111,7 +104,6 @@ const UserProfileForm = ({ user, onSubmit, className }: UserProfileFormProps) => data-qa-anchor="user-profile-form-display-name-input" placeholder={formatMessage({ id: 'UserProfileForm.namePlaceholder' })} maxLength={100} - disabled={!isAdmin} /> diff --git a/src/social/components/category/CategoriesCard/index.tsx b/src/social/components/category/CategoriesCard/index.tsx index d68de1e66..abb713673 100644 --- a/src/social/components/category/CategoriesCard/index.tsx +++ b/src/social/components/category/CategoriesCard/index.tsx @@ -51,7 +51,6 @@ const List = () => { return ( theme.palette.base.shade4}; + background-color: ${({ theme }) => theme.palette.primary.shade3}; } `} diff --git a/src/social/components/community/RecommendedList/index.tsx b/src/social/components/community/RecommendedList/index.tsx index 08671bb1f..60dde5cb9 100644 --- a/src/social/components/community/RecommendedList/index.tsx +++ b/src/social/components/community/RecommendedList/index.tsx @@ -22,7 +22,13 @@ const RecommendedList = () => { if (!communities?.length) return null; return ( - + {isLoading && new Array(4).fill(1).map((x, index) => )} {!isLoading && diff --git a/src/social/components/post/Creator/index.tsx b/src/social/components/post/Creator/index.tsx index c86652f04..572c50789 100644 --- a/src/social/components/post/Creator/index.tsx +++ b/src/social/components/post/Creator/index.tsx @@ -286,6 +286,16 @@ const PostCreatorBar = ({ title: , content: , okText: , + onOk: () => { + clearAll(); + setPostImages([]); + setPostVideos([]); + setPostFiles([]); + setIncomingImages([]); + setIncomingVideos([]); + setIncomingFiles([]); + setNavigationBlocker?.(null); + }, }); } else { setNavigationBlocker?.(null); diff --git a/src/social/components/post/Editor/styles.tsx b/src/social/components/post/Editor/styles.tsx index e4db1955c..e89819814 100644 --- a/src/social/components/post/Editor/styles.tsx +++ b/src/social/components/post/Editor/styles.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; import { PrimaryButton } from '~/core/components/Button'; export const PostEditorContainer = styled.div` - width: 520px; + width: 100%; padding: 0; border: none; display: flex; diff --git a/src/social/components/post/Editor/usePostEditor.ts b/src/social/components/post/Editor/usePostEditor.ts index 4aa4f739f..7bd75a153 100644 --- a/src/social/components/post/Editor/usePostEditor.ts +++ b/src/social/components/post/Editor/usePostEditor.ts @@ -33,15 +33,27 @@ export const usePostEditor = ({ postId, onSave }: { postId?: string; onSave: () setLocalRemovedChildren((prevRemovedChildren) => [...prevRemovedChildren, childPostId]); }; - const handleSave = async () => { - localRemovedChildren.forEach((childPostId) => { - PostRepository.deletePost(childPostId); - }); + const formattedAttachment = (post: Amity.Post) => { + if (post.dataType === 'file' || post.dataType === 'image') { + return { + type: post.dataType, + fileId: post.data.fileId, + }; + } + if (post.dataType === 'video') { + return { + type: post.dataType, + fileId: post.data.videoFileId.original, + }; + } + }; + const handleSave = async () => { await PostRepository.updatePost(post.postId, { data: { text }, mentionees, metadata, + attachments: childrenPosts.map(formattedAttachment), }); clearAll(); onSave(); diff --git a/src/social/components/post/GalleryContent/index.tsx b/src/social/components/post/GalleryContent/index.tsx index 7fac4f6db..903a03b65 100644 --- a/src/social/components/post/GalleryContent/index.tsx +++ b/src/social/components/post/GalleryContent/index.tsx @@ -54,6 +54,7 @@ export interface GalleryContentProps { renderVideoThumbnail?: (item: Amity.Post<'video'>) => ReactNode; renderImageThumbnail?: (item: Amity.Post<'image'>) => ReactNode; renderLiveStreamThumbnail?: (item: Amity.Post<'liveStream'>) => ReactNode; + grid?: boolean; } const GalleryContent = ({ @@ -63,6 +64,7 @@ const GalleryContent = ({ loadingMore = false, showCounter = false, truncate = false, + grid = false, renderVideoItem, renderImageItem, renderLiveStreamItem, @@ -112,6 +114,7 @@ const GalleryContent = ({ className={className} items={items} truncate={truncate} + grid={grid} onClick={(i) => { if (!isLoadingItem(items[i])) { setIndex(i); diff --git a/src/social/components/post/PollComposer/styles.tsx b/src/social/components/post/PollComposer/styles.tsx index 78ab088df..cf7098465 100644 --- a/src/social/components/post/PollComposer/styles.tsx +++ b/src/social/components/post/PollComposer/styles.tsx @@ -46,6 +46,7 @@ export const OptionInput = styled(TextInput)` background: ${({ theme }) => theme.palette.base.shade4}; width: 100%; padding-right: 60px; + color: ${({ theme }) => theme.palette.neutral.main}; `; export const CloseIcon = styled(CircleRemove)``; diff --git a/src/social/components/post/Post/DefaultPostRenderer.tsx b/src/social/components/post/Post/DefaultPostRenderer.tsx index ce553f85a..3b29c0093 100644 --- a/src/social/components/post/Post/DefaultPostRenderer.tsx +++ b/src/social/components/post/Post/DefaultPostRenderer.tsx @@ -216,11 +216,6 @@ const DefaultPostRenderer = (props: DefaultPostRendererProps) => { const community = useCommunity(communityId); const { currentUserId } = useSDK(); - usePostSubscription({ - postId: post?.postId, - level: SubscriptionLevels.POST, - }); - const { canReview, isPostUnderReview } = useCommunityPostPermission({ community, post, diff --git a/src/social/components/post/Post/index.tsx b/src/social/components/post/Post/index.tsx index 9b474e2a3..89fdba1d3 100644 --- a/src/social/components/post/Post/index.tsx +++ b/src/social/components/post/Post/index.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { memo, useEffect } from 'react'; import usePost from '~/social/hooks/usePost'; import usePoll from '~/social/hooks/usePoll'; @@ -30,17 +30,6 @@ const Post = ({ postId, className, hidePostTarget, readonly, onDeleted }: PostPr const postRenderFn = usePostRenderer(post?.dataType); const { currentUserId } = useSDK(); - usePostSubscription({ - postId, - level: SubscriptionLevels.POST, - }); - - useReactionSubscription({ - targetType: post?.targetType, - targetId: post?.targetId, - shouldSubscribe: () => !!post, - }); - const pollPost = (childrenPosts || []).find((childPost) => childPost.dataType === 'poll'); const poll = usePoll((pollPost?.data as Amity.ContentDataPoll)?.pollId); diff --git a/src/social/constants.ts b/src/social/constants.ts index 037a8097f..d863e575c 100644 --- a/src/social/constants.ts +++ b/src/social/constants.ts @@ -47,10 +47,10 @@ export const Permissions = Object.freeze({ DeleteUserFeedPostPermission: 'DELETE_USER_FEED_POST', EditUserFeedCommentPermission: 'EDIT_USER_FEED_COMMENT', DeleteUserFeedCommentPermission: 'DELETE_USER_FEED_COMMENT', - EditCommunityFeedPostPermission: 'EDIT_COMMUNITY_FEED_POST', - DeleteCommunityFeedPostPermission: 'DELETE_COMMUNITY_FEED_POST', - EditCommunityFeedCommentPermission: 'EDIT_COMMUNITY_FEED_COMMENT', - DeleteCommunityFeedCommentPermission: 'DELETE_COMMUNITY_FEED_COMMENT', + EditCommunityFeedPostPermission: 'EDIT_COMMUNITY_POST', + DeleteCommunityFeedPostPermission: 'DELETE_COMMUNITY_POST', + EditCommunityFeedCommentPermission: 'EDIT_COMMUNITY_COMMENT', + DeleteCommunityFeedCommentPermission: 'DELETE_COMMUNITY_COMMENT', CreateCommunityCategoryPermission: 'CREATE_COMMUNITY_CATEGORY', EditCommunityCategoryPermission: 'EDIT_COMMUNITY_CATEGORY', DeleteCommunityCategoryPermission: 'DELETE_COMMUNITY_CATEGORY', diff --git a/src/social/hooks/collections/useCommunityMembersCollection.ts b/src/social/hooks/collections/useCommunityMembersCollection.ts index be5668978..9e1b08403 100644 --- a/src/social/hooks/collections/useCommunityMembersCollection.ts +++ b/src/social/hooks/collections/useCommunityMembersCollection.ts @@ -4,7 +4,12 @@ import useLiveCollection from '~/core/hooks/useLiveCollection'; export default function useCommunityMembersCollection(communityId?: string, limit: number = 5) { const { items, ...rest } = useLiveCollection({ fetcher: CommunityRepository.Membership.getMembers, - params: { communityId: communityId as string, limit, memberships: ['member'] }, + params: { + communityId: communityId as string, + limit, + memberships: ['member'], + includeDeleted: false, + }, shouldCall: () => !!communityId, }); diff --git a/src/social/hooks/useCommunityPostPermission.ts b/src/social/hooks/useCommunityPostPermission.ts index eb3ddd027..ce3ab2bf8 100644 --- a/src/social/hooks/useCommunityPostPermission.ts +++ b/src/social/hooks/useCommunityPostPermission.ts @@ -1,9 +1,9 @@ import { CommunityPostSettings } from '@amityco/ts-sdk'; import { useMemo } from 'react'; -import useUser from '~/core/hooks/useUser'; import usePostsCollection from '~/social/hooks/collections/usePostsCollection'; -import useCommunityModeratorsCollection from './collections/useCommunityModeratorsCollection'; -import useCommunityMembersCollection from './collections/useCommunityMembersCollection'; +import useSDK from '~/core/hooks/useSDK'; +import { Permissions } from '~/social/constants'; +import useCommunityModeratorsCollection from '~/social/hooks/collections/useCommunityModeratorsCollection'; const useCommunityPostPermission = ({ post, @@ -17,13 +17,13 @@ const useCommunityPostPermission = ({ userId?: string; }) => { const { moderators } = useCommunityModeratorsCollection(community?.communityId); - const { members } = useCommunityMembersCollection(community?.communityId); + const { client } = useSDK(); + const { posts: reviewingPosts } = usePostsCollection({ targetType: 'community', targetId: community?.communityId, feedType: 'reviewing', }); - const user = useUser(userId); const isEditable = useMemo(() => { if ( @@ -36,7 +36,6 @@ const useCommunityPostPermission = ({ return true; }, [childrenPosts]); - const member = members.find((member) => member.userId === userId); const moderator = moderators.find((moderator) => moderator.userId === userId); const isMyPost = post?.postedUserId === userId; const isPostUnderReview = useMemo(() => { @@ -45,29 +44,57 @@ const useCommunityPostPermission = ({ } return false; }, [community, reviewingPosts]); - const isGlobalAdmin = user?.roles.find((role) => role === 'global-admin') != null; const isModerator = moderator != null; - const isMember = member != null; - if (community == null) { - return { - isPostUnderReview: false, - isModerator: false, - canEdit: (isGlobalAdmin || isMyPost) && isEditable, - canReport: isGlobalAdmin || !isMyPost, - canDelete: isGlobalAdmin || isMyPost, - canReview: false, - }; + const permissions: { + canEdit: boolean; + canReport: boolean; + canDelete: boolean; + canReview: boolean; + } = { + canEdit: false, + canReport: false, + canDelete: false, + canReview: false, + }; + + if (isMyPost) { + if (!isPostUnderReview && isEditable) { + permissions.canEdit = true; + } + permissions.canDelete = true; + } else { + if (community != null) { + const canEdit = + client + ?.hasPermission(Permissions.EditCommunityFeedPostPermission) + .community(community.communityId) ?? false; + + permissions.canEdit = canEdit && isEditable; + + const canDelete = + client + ?.hasPermission(Permissions.DeleteCommunityFeedPostPermission) + .community(community.communityId) ?? false; + + permissions.canDelete = canDelete; + } else { + const canDelete = + client?.hasPermission(Permissions.EditUserFeedPostPermission).currentUser() ?? false; + + permissions.canDelete = canDelete; + } + + if (!isPostUnderReview) { + permissions.canReport = true; + } } return { isPostUnderReview, isModerator, - canEdit: (isGlobalAdmin || isModerator) && isEditable, - canReview: isGlobalAdmin || isModerator, - canDelete: (!isPostUnderReview && isModerator) || (isMyPost && isMember), - canReport: !isPostUnderReview ? !isMyPost && (isModerator || isMember) : !isMyPost, + ...permissions, }; }; diff --git a/src/social/hooks/usePostByIds.ts b/src/social/hooks/usePostByIds.ts index 720a92f99..f38b970e1 100644 --- a/src/social/hooks/usePostByIds.ts +++ b/src/social/hooks/usePostByIds.ts @@ -6,9 +6,12 @@ const usePostByIds = (postIds: Parameters[0] useEffect(() => { async function run() { - if (!postIds || postIds?.length === 0) return; - const response = await PostRepository.getPostByIds(postIds); - setPosts(response.data); + if (!postIds || postIds?.length === 0) { + setPosts([]); + } else { + const response = await PostRepository.getPostByIds(postIds); + setPosts(response.data); + } } run(); }, [postIds]); diff --git a/src/social/layouts/Main/index.tsx b/src/social/layouts/Main/index.tsx index 4af3bbefd..835f2805c 100644 --- a/src/social/layouts/Main/index.tsx +++ b/src/social/layouts/Main/index.tsx @@ -22,6 +22,11 @@ const Main = styled.div` min-width: 20rem; max-width: 90.75rem; margin: 0 auto; + + @media (max-width: 768px) { + max-width: unset; + min-width: unset; + } `; const Side = styled.div` diff --git a/src/social/layouts/Page/index.tsx b/src/social/layouts/Page/index.tsx index a61a4f8fe..6c2d4630a 100644 --- a/src/social/layouts/Page/index.tsx +++ b/src/social/layouts/Page/index.tsx @@ -3,7 +3,6 @@ import styled from 'styled-components'; const Container = styled.div<{ withHeader?: boolean }>` display: grid; - grid-template-areas: 'main side'; grid-template-columns: auto min-content; @@ -21,12 +20,27 @@ const Container = styled.div<{ withHeader?: boolean }>` overflow: hidden; margin: 0 auto; padding: 20px 0; + + @media (max-width: 768px) { + grid-template-areas: + ${({ withHeader }) => (withHeader ? `'header'` : ``)} + 'main' + 'side'; + grid-template-columns: 1fr; + grid-template-rows: auto auto; + height: auto; + overflow: unset; + } `; const HeaderContainer = styled.div` grid-area: header; width: 100%; height: 100%; + + @media (max-width: 768px) { + height: unset; + } `; const Main = styled.div` @@ -34,6 +48,10 @@ const Main = styled.div` width: 100%; height: 100%; overflow: auto; + + @media (max-width: 768px) { + height: unset; + } `; const Side = styled.div` @@ -43,6 +61,11 @@ const Side = styled.div` max-width: 20rem; overflow: auto; + @media (max-width: 768px) { + max-width: unset; + height: unset; + } + & > :not(:first-child) { margin-top: 20px; } diff --git a/src/social/pages/Application/index.tsx b/src/social/pages/Application/index.tsx index 3eb8f88b9..c6fca118e 100644 --- a/src/social/pages/Application/index.tsx +++ b/src/social/pages/Application/index.tsx @@ -70,11 +70,17 @@ const Community = () => { run(); }, [client]); + useEffect(() => { + if (open) { + setOpen(false); + } + }, [page.type, page.communityId]); + return ( }> - {page.type === PageTypes.Explore && } + {page.type === PageTypes.Explore && } {page.type === PageTypes.NewsFeed && ( diff --git a/src/social/pages/CommunityEdit/styles.tsx b/src/social/pages/CommunityEdit/styles.tsx index b7073103c..ade9087ee 100644 --- a/src/social/pages/CommunityEdit/styles.tsx +++ b/src/social/pages/CommunityEdit/styles.tsx @@ -11,6 +11,9 @@ export const ExtraActionContainer = styled.div` padding: 16px; width: 330px; flex-shrink: 0; + @media (max-width: 768px) { + width: 100%; + } `; export const ExtraActionContainerHeader = styled.div` diff --git a/src/social/pages/CommunityFeed/index.tsx b/src/social/pages/CommunityFeed/index.tsx index f2eb42bbe..690f9df8f 100644 --- a/src/social/pages/CommunityFeed/index.tsx +++ b/src/social/pages/CommunityFeed/index.tsx @@ -23,11 +23,10 @@ import { CommunitySideMenuOverlay, HeadTitle, MobileContainer, + StyledBarsIcon, StyledCommunitySideMenu, } from '../NewsFeed/styles'; -import { BarsIcon } from '~/icons'; - import { useNavigation } from '~/social/providers/NavigationProvider'; import { useGetActiveStoriesByTarget } from '~/v4/social/hooks/useGetActiveStories'; @@ -68,11 +67,6 @@ const CommunityFeed = ({ communityId, isNewCommunity, isOpen, toggleOpen }: Comm const [activeTab, setActiveTab] = useState(CommunityFeedTabs.TIMELINE); - useCommunitySubscription({ - communityId, - level: SubscriptionLevels.POST, - }); - const isJoined = community?.isJoined || false; const [isCreatedModalOpened, setCreatedModalOpened] = useState(isNewCommunity); @@ -88,7 +82,7 @@ const CommunityFeed = ({ communityId, isNewCommunity, isOpen, toggleOpen }: Comm - + @@ -112,7 +106,7 @@ const CommunityFeed = ({ communityId, isNewCommunity, isOpen, toggleOpen }: Comm )} {activeTab === CommunityFeedTabs.GALLERY && ( - + )} {activeTab === CommunityFeedTabs.MEMBERS && } diff --git a/src/social/pages/CommunityFeed/styles.tsx b/src/social/pages/CommunityFeed/styles.tsx index e80cda536..62ccbe583 100644 --- a/src/social/pages/CommunityFeed/styles.tsx +++ b/src/social/pages/CommunityFeed/styles.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; export const Wrapper = styled.div` height: 100%; - max-width: 550px; + max-width: 700px; margin: 0 auto; padding: 28px 0; overflow-y: auto; diff --git a/src/social/pages/Explore/index.tsx b/src/social/pages/Explore/index.tsx index 486dad725..424a11c0a 100644 --- a/src/social/pages/Explore/index.tsx +++ b/src/social/pages/Explore/index.tsx @@ -5,13 +5,55 @@ import TrendingList from '~/social/components/community/TrendingList'; import CategoriesCard from '~/social/components/category/CategoriesCard'; import { PageContainer } from './styles'; +import { + CommunitySideMenuOverlay, + HeadTitle, + MobileContainer, + StyledCommunitySideMenu, + StyledBarsIcon, +} from '../NewsFeed/styles'; +import { useIntl } from 'react-intl'; +import { styled } from 'styled-components'; -const ExplorePage = () => ( - - - - - -); +const StyledMobileContainer = styled(MobileContainer)` + background-color: #f7f7f8; +`; + +export const Wrapper = styled.div` + height: 100%; + margin: 0 auto; + padding: 28px 0; + overflow-y: auto; +`; + +interface ExplorePageProps { + isOpen: boolean; + toggleOpen: () => void; + hideSideMenu?: boolean; +} + +const ExplorePage = ({ isOpen, toggleOpen, hideSideMenu }: ExplorePageProps) => { + const { formatMessage } = useIntl(); + + return ( + + {hideSideMenu !== true && ( + <> + + + + + {formatMessage({ id: 'sidebar.explore' })} + + + )} + + + + + + + ); +}; export default ExplorePage; diff --git a/src/social/pages/NewsFeed/index.tsx b/src/social/pages/NewsFeed/index.tsx index 7caa3bd99..937a452f2 100644 --- a/src/social/pages/NewsFeed/index.tsx +++ b/src/social/pages/NewsFeed/index.tsx @@ -9,10 +9,10 @@ import { CommunitySideMenuOverlay, HeadTitle, MobileContainer, + StyledBarsIcon, StyledCommunitySideMenu, Wrapper, } from './styles'; -import { BarsIcon } from '~/icons'; import { useIntl } from 'react-intl'; import { StoryTab } from '~/social/components/StoryTab'; @@ -30,7 +30,7 @@ const NewsFeed = ({ isOpen, toggleOpen }: NewsFeedProps) => { - + {formatMessage({ id: 'sidebar.community' })} diff --git a/src/social/pages/NewsFeed/styles.tsx b/src/social/pages/NewsFeed/styles.tsx index ee9f08309..df00274b3 100644 --- a/src/social/pages/NewsFeed/styles.tsx +++ b/src/social/pages/NewsFeed/styles.tsx @@ -1,9 +1,10 @@ import styled from 'styled-components'; import CommunitySideMenu from '~/social/components/CommunitySideMenu'; +import { BarsIcon } from '~/icons/index'; export const Wrapper = styled.div` height: 100%; - max-width: 550px; + max-width: 700px; margin: 0 auto; padding: 28px 0; overflow-y: auto; @@ -57,3 +58,7 @@ export const StyledCommunitySideMenu = styled(CommunitySideMenu)<{ isOpen: boole transform: translateX(${({ isOpen }) => (isOpen ? 0 : '-100%')}); transition: transform 0.3s ease-in-out; `; + +export const StyledBarsIcon = styled(BarsIcon)` + cursor: pointer; +`; diff --git a/src/social/pages/UserFeed/styles.tsx b/src/social/pages/UserFeed/styles.tsx index 6bab3f2f8..2ff85fd44 100644 --- a/src/social/pages/UserFeed/styles.tsx +++ b/src/social/pages/UserFeed/styles.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; export const Wrapper = styled.div` height: 100%; - max-width: 550px; + max-width: 700px; margin: 0 auto; padding: 28px 0; overflow-y: auto; diff --git a/src/social/providers/NavigationProvider.tsx b/src/social/providers/NavigationProvider.tsx index 056501c3d..31ffb6e89 100644 --- a/src/social/providers/NavigationProvider.tsx +++ b/src/social/providers/NavigationProvider.tsx @@ -75,6 +75,7 @@ type ContextValue = { title: ReactNode; content: ReactNode; okText: ReactNode; + onOk?: () => void; } | null | undefined, @@ -182,6 +183,7 @@ export default function NavigationProvider({ title: ReactNode; content: ReactNode; okText: ReactNode; + onOk?: () => void; } | null | undefined @@ -207,7 +209,7 @@ export default function NavigationProvider({ } return true; - }, [askForConfirmation, navigationBlocker]); + }, [askForConfirmation, navigationBlocker, setNavigationBlocker]); const pushPage = useCallback( async (newPage) => { diff --git a/src/v4/chat/components/MessageComposer/MessageComposer.tsx b/src/v4/chat/components/MessageComposer/MessageComposer.tsx index a2994ada8..9d92de65b 100644 --- a/src/v4/chat/components/MessageComposer/MessageComposer.tsx +++ b/src/v4/chat/components/MessageComposer/MessageComposer.tsx @@ -19,11 +19,8 @@ import { LexicalNode, } from 'lexical'; import { AutoLinkNode, LinkNode } from '@lexical/link'; -import { - MentionPlugin, - MentionTypeaheadOption, -} from '~/v4/social/internal-components/Lexical/plugins/MentionPlugin'; -import { UserAvatar } from '~/v4/social/internal-components/UserAvatar/UserAvatar'; +import { MentionPlugin } from '~/v4/social/internal-components/Lexical/plugins/MentionPlugin'; + import { useMutation } from '@tanstack/react-query'; import { editorStateToText, @@ -44,6 +41,8 @@ import { LinkPlugin } from '~/v4/social/internal-components/Lexical/plugins/Link import { AutoLinkPlugin } from '~/v4/social/internal-components/Lexical/plugins/AutoLinkPlugin'; import { EditorRefPlugin } from '@lexical/react/LexicalEditorRefPlugin'; import { EnterKeyInterceptorPlugin } from '~/v4/social/internal-components/Lexical/plugins/EnterKeyInterceptorPlugin'; +import { AllMentionItem } from '~/v4/social/internal-components/Lexical/AllMentionItem'; +import { MentionItem } from '~/v4/social/internal-components/Lexical/MentionItem'; const COMPOSEBAR_MAX_CHARACTER_LIMIT = 200; @@ -92,53 +91,6 @@ const useSuggestions = (channelId?: string | null) => { return { suggestions, queryString, onQueryChange }; }; -interface MentionItemProps { - isSelected: boolean; - onClick: () => void; - onMouseEnter: () => void; - option: MentionTypeaheadOption; -} - -function MentionItem({ option, isSelected, onClick, onMouseEnter }: MentionItemProps) { - return ( -
  • -
    - -
    -

    {option.data.displayName}

    -
  • - ); -} - -function AllMentionItem({ option, isSelected, onClick, onMouseEnter }: MentionItemProps) { - return ( -
  • -
    @
    -

    {option.data.displayName}

    -
  • - ); -} - const nodes = [AutoLinkNode, LinkNode, MentionNode] as Array>; export const MessageComposer = ({ diff --git a/src/v4/chat/hooks/collections/useSearchChannelUser.ts b/src/v4/chat/hooks/collections/useSearchChannelUser.ts index 429bacb9c..8fc9d283c 100644 --- a/src/v4/chat/hooks/collections/useSearchChannelUser.ts +++ b/src/v4/chat/hooks/collections/useSearchChannelUser.ts @@ -16,7 +16,7 @@ export const useSearchChannelUser = ({ }) => { const { items, ...rest } = useLiveCollection({ fetcher: ChannelRepository.Membership.searchMembers, - params: { channelId, search: search || '', memberships, limit }, + params: { channelId, search: search || '', memberships, limit, includeDeleted: false }, shouldCall: !!channelId && shouldCall, }); diff --git a/src/v4/core/hooks/useIntersectionObserver.ts b/src/v4/core/hooks/useIntersectionObserver.ts index 5dc33f156..4e3dce2ce 100644 --- a/src/v4/core/hooks/useIntersectionObserver.ts +++ b/src/v4/core/hooks/useIntersectionObserver.ts @@ -1,25 +1,25 @@ -import { MutableRefObject, useEffect } from 'react'; +import { useEffect } from 'react'; const useIntersectionObserver = ({ - ref, + node, onIntersect, options, }: { - ref: MutableRefObject; + node?: HTMLElement | null; onIntersect: () => void; options?: IntersectionObserverInit; }) => { useEffect(() => { - if (!ref?.current) return; + if (node == null) return; const observer = new IntersectionObserver( (entries) => entries[0]?.isIntersecting && onIntersect(), options, ); - observer.observe(ref.current); + observer.observe(node); return () => observer.disconnect(); - }, [ref, onIntersect, options]); + }, [node, onIntersect, options]); }; export default useIntersectionObserver; diff --git a/src/v4/core/hooks/useLiveCollection.ts b/src/v4/core/hooks/useLiveCollection.ts index 586e25bcc..23de3dab5 100644 --- a/src/v4/core/hooks/useLiveCollection.ts +++ b/src/v4/core/hooks/useLiveCollection.ts @@ -24,6 +24,7 @@ function useLiveCollection({ loadMore: () => void; error: Error | null; loadMoreHasBeenCalled: boolean; + refresh: () => void; } { const { subscribe } = useSDKLiveCollectionConnector(); const [loadMoreHasBeenCalled, setLoadMoreHasBeenCalled] = useState(false); @@ -32,6 +33,7 @@ function useLiveCollection({ const [error, setError] = useState(null); const [hasMore, setHasMore] = useState(false); const loadMoreFnRef = useRef<(() => void) | null>(null); + const unsubscribeRef = useRef(null); const loadMore = useCallback(() => { if (loadMoreFnRef.current) { @@ -60,12 +62,30 @@ function useLiveCollection({ params, callback: callbackFn, }); + unsubscribeRef.current = unsubscribe; return () => { unsubscribe(); }; }, [params, shouldCall]); + const refresh = useCallback(() => { + if (unsubscribeRef.current) { + unsubscribeRef.current(); + } + + const { unsubscribe } = subscribe({ + fetcher, + params, + callback: callbackFn, + refresh: true, + }); + + unsubscribeRef.current = unsubscribe; + + return () => unsubscribe(); + }, []); + return { items, hasMore, @@ -73,6 +93,7 @@ function useLiveCollection({ loadMore, error, loadMoreHasBeenCalled, + refresh, }; } diff --git a/src/v4/core/providers/CommunityTabProvider.tsx b/src/v4/core/providers/CommunityTabProvider.tsx new file mode 100644 index 000000000..76eb71e2e --- /dev/null +++ b/src/v4/core/providers/CommunityTabProvider.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useContext, useState } from 'react'; + +type CommunityTabContextType = { + activeTab: 'community_feed' | 'community_pin'; + setActiveTab: (tab: 'community_feed' | 'community_pin') => void; +}; + +const CommunityTabContext = createContext({ + activeTab: 'community_feed', + setActiveTab: () => {}, +}); + +export const useCommunityTabContext = () => useContext(CommunityTabContext); + +type CommunityTabProviderProps = { + children: React.ReactNode; +}; + +export const CommunityTabProvider: React.FC = ({ children }) => { + const [activeTab, setActiveTab] = + useState('community_feed'); + + const value: CommunityTabContextType = { + activeTab, + setActiveTab, + }; + + return {children}; +}; diff --git a/src/v4/core/providers/CustomizationProvider.tsx b/src/v4/core/providers/CustomizationProvider.tsx index 9f11687c2..2c59cf297 100644 --- a/src/v4/core/providers/CustomizationProvider.tsx +++ b/src/v4/core/providers/CustomizationProvider.tsx @@ -490,6 +490,50 @@ export const defaultConfig: DefaultConfig = { 'my_communities_search_page/top_search_bar/cancel_button': { text: 'Cancel', }, + 'community_profile_page/*/*': {}, + 'community_profile_page/community_feed/*': {}, + '*/post_content/announcement_badge': { + image: 'value', + }, + '*/post_content/pin_badge': { + image: 'value', + }, + '*/post_content/non_member_section': { + image: 'value', + }, + 'community_profile_page/community_header/*': {}, + 'community_profile_page/community_header/community_cover': {}, + 'community_profile_page/community_header/community_name': {}, + 'community_profile_page/community_header/community_verify_badge': { + image: 'value', + }, + 'community_profile_page/community_header/community_category': {}, + 'community_profile_page/community_header/community_description': {}, + 'community_profile_page/community_header/community_info': {}, + 'community_profile_page/community_header/community_join_button': { + image: 'value', + }, + 'community_profile_page/community_header/community_pending_post': { + image: 'value', + }, + 'community_profile_page/community_header/back_button': { + image: 'value', + }, + 'community_profile_page/community_header/menu_button': { + image: 'value', + }, + 'community_profile_page/community_profile_tab/*': {}, + 'community_profile_page/community_profile_tab/community_feed_tab_button': { + image: 'value', + }, + 'community_profile_page/community_profile_tab/community_pin_tab_button': { + image: 'value', + }, + 'community_profile_page/community_pin/*': {}, + 'community_profile_page/community_pin/community_create_post_button': { + image: 'value', + }, + 'community_profile_page/post_content/*': {}, }, }; diff --git a/src/v4/core/providers/NavigationProvider.tsx b/src/v4/core/providers/NavigationProvider.tsx index 3dbe45f54..3608cdcfd 100644 --- a/src/v4/core/providers/NavigationProvider.tsx +++ b/src/v4/core/providers/NavigationProvider.tsx @@ -2,6 +2,7 @@ import React, { createContext, useCallback, useContext, useState, useMemo, React import { AmityStoryMediaType } from '~/v4/social/pages/DraftsPage/DraftsPage'; import { Mode } from '~/v4/social/pages/PostComposerPage/PostComposerPage'; import { NavigationContext as NavigationContextV3 } from '~/social/providers/NavigationProvider'; +import { AmityPostCategory } from '~/v4/social/components/PostContent/PostContent'; export enum PageTypes { Explore = 'explore', @@ -70,6 +71,8 @@ type Page = context: { postId: string; communityId?: string; + hideTarget?: boolean; + category?: AmityPostCategory; }; } | { type: PageTypes.CommunityProfilePage; context: { communityId: string } } @@ -114,7 +117,7 @@ type ContextValue = { onMessageUser: (userId: string) => void; onBack: () => void; goToUserProfilePage: (userId: string) => void; - goToPostDetailPage: (postId: string) => void; + goToPostDetailPage: (postId: string, hideTarget?: boolean, category?: AmityPostCategory) => void; goToCommunityProfilePage: (communityId: string) => void; goToSocialGlobalSearchPage: (tab?: string) => void; goToMyCommunitiesSearchPage: () => void; @@ -177,7 +180,7 @@ let defaultValue: ContextValue = { onEditUser: (userId: string) => {}, onMessageUser: (userId: string) => {}, goToUserProfilePage: (userId: string) => {}, - goToPostDetailPage: (postId: string) => {}, + goToPostDetailPage: (postId: string, hideTarget?: boolean, category?: AmityPostCategory) => {}, goToViewStoryPage: (context: { targetId: string; targetType: Amity.StoryTargetType; @@ -227,7 +230,8 @@ if (process.env.NODE_ENV !== 'production') { onBack: () => console.log('NavigationContext onBack()'), goToUserProfilePage: (userId) => console.log(`NavigationContext goToUserProfilePage(${userId})`), - goToPostDetailPage: (postId) => console.log(`NavigationContext goToPostDetailPage(${postId})`), + goToPostDetailPage: (postId, hideTarget, category) => + console.log(`NavigationContext goToPostDetailPage(${postId} ${hideTarget} ${category})`), goToCommunityProfilePage: (communityId) => console.log(`NavigationContext goToCommunityProfilePage(${communityId})`), goToSocialGlobalSearchPage: (tab) => @@ -360,7 +364,7 @@ export default function NavigationProvider({ const handleClickCommunity = useCallback( (communityId) => { const next = { - type: PageTypes.CommunityFeed, + type: PageTypes.CommunityProfilePage, context: { communityId, }, @@ -496,7 +500,7 @@ export default function NavigationProvider({ const goToUserProfilePage = useCallback( (userId) => { const next = { - type: PageTypes.UserProfilePage, + type: PageTypes.UserFeed, context: { userId, }, @@ -508,11 +512,13 @@ export default function NavigationProvider({ ); const goToPostDetailPage = useCallback( - (postId) => { + (postId, hideTarget, category) => { const next = { type: PageTypes.PostDetailPage, context: { postId, + hideTarget, + category, }, }; @@ -524,7 +530,7 @@ export default function NavigationProvider({ const goToCommunityProfilePage = useCallback( (communityId) => { const next = { - type: PageTypes.CommunityFeed, + type: PageTypes.CommunityProfilePage, context: { communityId, }, diff --git a/src/v4/core/providers/PageBehaviorProvider.tsx b/src/v4/core/providers/PageBehaviorProvider.tsx index a8a3f1f81..d9921ea59 100644 --- a/src/v4/core/providers/PageBehaviorProvider.tsx +++ b/src/v4/core/providers/PageBehaviorProvider.tsx @@ -1,5 +1,6 @@ import React, { useContext } from 'react'; import { PageTypes, useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { AmityPostCategory } from '~/v4/social/components/PostContent/PostContent'; import { Mode } from '~/v4/social/pages/PostComposerPage/PostComposerPage'; export interface PageBehavior { @@ -31,6 +32,9 @@ export interface PageBehavior { AmityCommunitySearchResultComponentBehavior?: { goToCommunityProfilePage?: (context: { communityId: string }) => void; }; + AmityUserSearchResultComponentBehavior?: { + goToUserProfilePage?: (context: { userId: string }) => void; + }; AmityCreatePostMenuComponentBehavior?: { goToSelectPostTargetPage?(): void; goToStoryTargetSelectionPage?(): void; @@ -54,6 +58,24 @@ export interface PageBehavior { AmityPostComposerPageBehavior?: { goToSocialHomePage?(): void; }; + AmityCommunityProfilePageBehavior?: { + goToPostComposerPage?(context: { + mode: Mode.CREATE | Mode.EDIT; + targetId: string | null; + targetType: 'community' | 'user'; + community?: Amity.Community; + post?: Amity.Post; + }): void; + goToPostDetailPage?(context: { + postId: string; + hideTarget?: boolean; + category?: AmityPostCategory; + }): void; + goToCreateStoryPage?(context: { communityId: string }): void; + // goToCommunitySettingPage?(context: { communityId: string }): void; + // goToPendingPostPage?(context: { communityId: string }): void; + // goToMemberListPage?(context: { communityId: string }): void; + }; } const PageBehaviorContext = React.createContext(undefined); @@ -164,6 +186,14 @@ export const PageBehaviorProvider: React.FC = ({ goToCommunityProfilePage(context.communityId); }, }, + AmityUserSearchResultComponentBehavior: { + goToUserProfilePage: (context: { userId: string }) => { + if (pageBehavior?.AmityUserSearchResultComponentBehavior?.goToUserProfilePage) { + return pageBehavior.AmityUserSearchResultComponentBehavior.goToUserProfilePage(context); + } + goToUserProfilePage(context.userId); + }, + }, AmityCreatePostMenuComponentBehavior: { goToSelectPostTargetPage() { if (pageBehavior?.AmityCreatePostMenuComponentBehavior?.goToSelectPostTargetPage) { @@ -212,6 +242,54 @@ export const PageBehaviorProvider: React.FC = ({ goToSocialHomePage(); }, }, + AmityCommunityProfilePageBehavior: { + goToPostComposerPage(context: { + mode: Mode.CREATE; + targetId: string | null; + targetType: 'community' | 'user'; + community?: Amity.Community; + post?: Amity.Post; + }) { + if (pageBehavior?.AmityCommunityProfilePageBehavior?.goToPostComposerPage) { + return pageBehavior.AmityCommunityProfilePageBehavior.goToPostComposerPage(context); + } + goToPostComposerPage(context); + }, + goToPostDetailPage(context: { + postId: string; + hideTarget?: boolean; + category?: AmityPostCategory; + }) { + if (pageBehavior?.AmityCommunityProfilePageBehavior?.goToPostDetailPage) { + return pageBehavior.AmityCommunityProfilePageBehavior.goToPostDetailPage(context); + } + goToPostDetailPage(context.postId, context.hideTarget, context.category); + }, + // goToPendingPostPage(context) { + // if (pageBehavior?.AmityCommunityProfilePageBehavior?.goToPendingPostPage) { + // return pageBehavior.AmityCommunityProfilePageBehavior.goToPendingPostPage(context); + // } + // goToPostDetailPage(context); + // }, + // goToCommunitySettingPage(context) { + // if (pageBehavior?.AmityCommunityProfilePageBehavior?.goToCommunitySettingPage) { + // return pageBehavior.AmityCommunityProfilePageBehavior.goToCommunitySettingPage(context); + // } + // goToPostDetailPage(context); + // }, + // goToCreateStoryPage(context) { + // if (pageBehavior?.AmityCommunityProfilePageBehavior?.goToCreateStoryPage) { + // return pageBehavior.AmityCommunityProfilePageBehavior.goToCreateStoryPage(context); + // } + // goToPostDetailPage(context); + // }, + // goToMemberListPage(context) { + // if (pageBehavior?.AmityCommunityProfilePageBehavior?.goToMemberListPage) { + // return pageBehavior.AmityCommunityProfilePageBehavior.goToMemberListPage(context); + // } + // goToPostDetailPage(context); + // }, + }, }; return ( diff --git a/src/v4/core/providers/SDKConnectorProvider/SDKConnectorLiveCollectionProvider.tsx b/src/v4/core/providers/SDKConnectorProvider/SDKConnectorLiveCollectionProvider.tsx index 9fee6b430..30dadfbdc 100644 --- a/src/v4/core/providers/SDKConnectorProvider/SDKConnectorLiveCollectionProvider.tsx +++ b/src/v4/core/providers/SDKConnectorProvider/SDKConnectorLiveCollectionProvider.tsx @@ -6,6 +6,7 @@ const SDKConnectorLiveCollectionContext = createContext({ params, callback, config, + refresh = false, }: { fetcher: ( params: Amity.LiveCollectionParams, @@ -15,6 +16,7 @@ const SDKConnectorLiveCollectionContext = createContext({ params: Amity.LiveCollectionParams; callback: Amity.LiveCollectionCallback; config?: Amity.LiveCollectionConfig; + refresh?: boolean; }) => { return { unsubscribe: () => {} }; }, @@ -47,6 +49,7 @@ export default function SDKConnectorLiveCollectionProvider({ params, callback, config, + refresh = false, }: { fetcher: ( params: Amity.LiveCollectionParams, @@ -56,10 +59,16 @@ export default function SDKConnectorLiveCollectionProvider({ params: Amity.LiveCollectionParams; callback: Amity.LiveCollectionCallback; config?: Amity.LiveCollectionConfig; + refresh?: boolean; }) => { if (currentUserId == null) return { unsubscribe() {} }; const key = getSubscriberKey(fetcher.name, params); + if (refresh) { + delete responseMap.current[key]; + delete subscriberMap.current[key]; + } + if (subscriberMap.current[key] && responseMap.current[key]) { callback?.(responseMap.current[key] as Amity.LiveCollection); subscriberMap.current[key].push(callback as Amity.LiveCollectionCallback); diff --git a/src/v4/icons/CommunityCreatePost.tsx b/src/v4/icons/CommunityCreatePost.tsx new file mode 100644 index 000000000..0729b7846 --- /dev/null +++ b/src/v4/icons/CommunityCreatePost.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +export const CommunityCreatePostButtonIcon = (props: React.SVGProps) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/v4/icons/Ellipsis.tsx b/src/v4/icons/Ellipsis.tsx new file mode 100644 index 000000000..1c6405a98 --- /dev/null +++ b/src/v4/icons/Ellipsis.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export const EllipsisH = ({ ...props }: React.SVGProps) => ( + + + +); diff --git a/src/v4/icons/EmptyPost.tsx b/src/v4/icons/EmptyPost.tsx new file mode 100644 index 000000000..b9a9e6b8f --- /dev/null +++ b/src/v4/icons/EmptyPost.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const EmptyPost = ({ ...props }: React.SVGProps) => ( + + + +); + +export default EmptyPost; diff --git a/src/v4/icons/Featured/Featured.module.css b/src/v4/icons/Featured/Featured.module.css new file mode 100644 index 000000000..45dbf02a8 --- /dev/null +++ b/src/v4/icons/Featured/Featured.module.css @@ -0,0 +1,7 @@ +.featuredIcon__background { + fill: var(--asc-color-base-shade4); +} + +.featuredIcon__textColor { + fill: var(--asc-color-base-default); +} diff --git a/src/v4/icons/Featured/Featured.tsx b/src/v4/icons/Featured/Featured.tsx new file mode 100644 index 000000000..4ac38e9b4 --- /dev/null +++ b/src/v4/icons/Featured/Featured.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styles from './Featured.module.css'; + +interface FeaturedIconProps extends React.SVGProps { + backgroundColor?: string; + textColor?: string; +} + +export const FeaturedIcon = ({ backgroundColor, textColor, ...props }: FeaturedIconProps) => { + return ( + + + + + ); +}; diff --git a/src/v4/icons/Feed.tsx b/src/v4/icons/Feed.tsx new file mode 100644 index 000000000..cabe072fc --- /dev/null +++ b/src/v4/icons/Feed.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const Feed = () => { + return ( + + + + ); +}; diff --git a/src/v4/icons/Lock.tsx b/src/v4/icons/Lock.tsx new file mode 100644 index 000000000..d597b5928 --- /dev/null +++ b/src/v4/icons/Lock.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const Lock = ({ ...props }: React.SVGProps) => ( + + + +); + +export default Lock; diff --git a/src/v4/icons/Pin.tsx b/src/v4/icons/Pin.tsx new file mode 100644 index 000000000..18d3cd815 --- /dev/null +++ b/src/v4/icons/Pin.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const Pin = () => { + return ( + + + + ); +}; diff --git a/src/v4/icons/PinBadge.tsx b/src/v4/icons/PinBadge.tsx new file mode 100644 index 000000000..836bb3346 --- /dev/null +++ b/src/v4/icons/PinBadge.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export const PinBadgeIcon = (props: React.SVGProps) => { + return ( + + + + ); +}; diff --git a/src/v4/icons/Plus.tsx b/src/v4/icons/Plus.tsx new file mode 100644 index 000000000..f3cd3c3db --- /dev/null +++ b/src/v4/icons/Plus.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export const Plus = (props: React.SVGProps) => ( + + + +); diff --git a/src/v4/icons/RefreshSpinner.tsx b/src/v4/icons/RefreshSpinner.tsx new file mode 100644 index 000000000..32b194914 --- /dev/null +++ b/src/v4/icons/RefreshSpinner.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +type RefreshSpinnerProps = { + props?: React.SVGProps; + className?: string; +}; + +export const RefreshSpinner = ({ props, className }: RefreshSpinnerProps) => { + return ( +
    + + + + + + + + + +
    + ); +}; diff --git a/src/v4/icons/VerifyBadge.tsx b/src/v4/icons/VerifyBadge.tsx new file mode 100644 index 000000000..888379a75 --- /dev/null +++ b/src/v4/icons/VerifyBadge.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export const VerifyBadgeIcon = () => { + return ( + + + + + ); +}; diff --git a/src/v4/social/components/CommentComposer/CommentComposer.tsx b/src/v4/social/components/CommentComposer/CommentComposer.tsx index 2a64a2990..182d150c4 100644 --- a/src/v4/social/components/CommentComposer/CommentComposer.tsx +++ b/src/v4/social/components/CommentComposer/CommentComposer.tsx @@ -38,6 +38,7 @@ interface CommentComposerProps { replyTo?: Amity.Comment; onCancelReply: () => void; shouldAllowCreation?: boolean; + community?: Amity.Community | null; } export const CommentComposer = ({ @@ -46,6 +47,7 @@ export const CommentComposer = ({ replyTo, onCancelReply, shouldAllowCreation = true, + community, }: CommentComposerProps) => { const userId = useSDK().currentUserId; const { user } = useUser(userId); @@ -136,6 +138,7 @@ export const CommentComposer = ({ mentionOffsetBottom={-mentionOffsetBottom} value={textValue} placehoder="Say something nice..." + community={community} /> + ))} + {posts?.length === 0 && !isLoading && ( +
    + +

    No post yet

    +
    + )} +
    + + ); + }; + + const renderAnnouncementPost = () => { + return isLoadingAnnouncementPosts ? ( + + ) : ( + announcementPosts && + announcementPosts.map(({ post }: Amity.Post) => { + return ( + + ); + }) + ); + }; + + return ( +
    + {isMemberPrivateCommunity || community?.isPublic ? ( + <> + {renderAnnouncementPost()} + {renderPublicCommunityFeed()} + + ) : ( + + )} +
    + ); +}; diff --git a/src/v4/social/components/CommunityFeed/index.ts b/src/v4/social/components/CommunityFeed/index.ts new file mode 100644 index 000000000..e88887fef --- /dev/null +++ b/src/v4/social/components/CommunityFeed/index.ts @@ -0,0 +1 @@ +export { CommunityFeed } from './CommunityFeed'; diff --git a/src/v4/social/components/CommunityHeader/CommunityHeader.module.css b/src/v4/social/components/CommunityHeader/CommunityHeader.module.css new file mode 100644 index 000000000..13066334e --- /dev/null +++ b/src/v4/social/components/CommunityHeader/CommunityHeader.module.css @@ -0,0 +1,141 @@ +.container { + width: 100%; + margin: 0 auto; + background: var(--asc-color-background-default); + overflow: hidden; +} + +.headerImageContainer { + position: relative; + height: 17.0625rem; + overflow: hidden; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + +.headerImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.backButton, +.moreButton { + position: absolute; + top: 0.625rem; + background: rgb(0 0 0 / 50%); + color: var(--asc-color-white); + border: none; + border-radius: 50%; + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.backButton { + left: 0.625rem; +} + +.moreButton { + right: 0.625rem; +} + +.content { + width: 100%; + display: flex; + flex-direction: column; + padding: 1rem 1rem 0; + align-items: flex-start; + gap: 0.5rem; + overflow: hidden; +} + +.name { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--asc-color-base-default); +} + +.description { + overflow: hidden; + color: var(--asc-color-base-default); + margin-bottom: var(--asc-spacing-s2); +} + +.communityProfile__communityInfo__container { + display: flex; + gap: var(--asc-spacing-s1); +} + +.divider { + width: 1px; + background-color: var(--asc-color-base-shade3); + margin-left: var(--asc-spacing-s1); + margin-right: var(--asc-spacing-s1); +} + +.statCount { + color: var(--asc-color-base-default); +} + +.statTitle { + color: var(--asc-color-base-shade2); +} + +.menuButton { + display: flex; + padding: 0.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + border-radius: 0.5rem; + border: 1px solid var(--asc-color-base-shade3); +} + +.communityProfile__joinButton__container { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + color: var(--asc-color-primary-shade4); +} + +.communityProfile__joinButton { + width: 100%; + background-color: var(--asc-color-primary-default); + color: var(--asc-color-secondary-default); + padding: 0.625rem 1rem 0.625rem 0.75rem; + border-radius: 0.5rem; + cursor: pointer; + text-align: center; +} + +.communityProfile__pendingPost__container { + width: 100%; +} + +.communityProfile__menuItem { + display: flex; + padding: 0.75rem 1rem; + align-items: flex-start; + gap: 0.75rem; + align-self: stretch; + background: var(--asc-color-background-shade1); +} + +.communityProfile__privateIcon { + width: 1.25rem; + height: 1rem; + fill: var(--asc-color-base-default); +} + +.communityProfile__divider { + width: 100%; + height: 1px; + background-color: var(--asc-color-base-shade4); + margin-bottom: 0.25rem; +} diff --git a/src/v4/social/components/CommunityHeader/CommunityHeader.tsx b/src/v4/social/components/CommunityHeader/CommunityHeader.tsx new file mode 100644 index 000000000..ae034c8bf --- /dev/null +++ b/src/v4/social/components/CommunityHeader/CommunityHeader.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import styles from './CommunityHeader.module.css'; +import { StoryTab } from '~/v4/social/components/StoryTab'; +import { CommunityPendingPost } from '~/v4/social/elements/CommunityPendingPost'; +import { CommunityProfileTab } from '~/v4/social/elements/CommunityProfileTab'; +import { CommunityCover } from '~/v4/social/elements/CommunityCover'; +import { CommunityJoinButton } from '~/v4/social/elements/CommunityJoinButton'; +import { useCommunityInfo } from '~/v4/social/hooks/useCommunityInfo'; +import { useCommunityTabContext } from '~/v4/core/providers/CommunityTabProvider'; +import { useNavigation } from '~/v4/core/providers/NavigationProvider'; +import { CommunityVerifyBadge } from '~/v4/social/elements/CommunityVerifyBadge'; +import { CommunityDescription } from '~/v4/social/elements/CommunityDescription'; +import { CommunityName } from '~/v4/social/elements/CommunityName'; +import { CommunityCategory } from '~/v4/social/elements/CommunityCategory'; +import { CommunityInfo } from '~/v4/social/elements/CommunityInfo'; +import Lock from '~/v4/icons/Lock'; + +interface CommunityProfileHeaderProps { + pageId?: string; + community: Amity.Community; +} + +export const CommunityHeader: React.FC = ({ + pageId = '*', + community, +}) => { + const { onBack, onEditCommunity } = useNavigation(); + const { activeTab, setActiveTab } = useCommunityTabContext(); + + const { + communityCategories, + avatarFileUrl, + joinCommunity, + pendingPostsCount, + canReviewCommunityPosts, + } = useCommunityInfo(community.communityId); + + const handleTabChange = (tab: 'community_feed' | 'community_pin') => { + setActiveTab(tab); + }; + + const isShowPendingPost = + community.isJoined && community.postSetting && canReviewCommunityPosts && pendingPostsCount > 0; + + return ( +
    + onEditCommunity(community.communityId)} + /> +
    +
    + {!community.isPublic && } + + {community.isOfficial && } +
    + + + + + +
    + +
    + onEditCommunity(community.communityId, 'MEMBERS')} + /> +
    + + {!community.isJoined && community.isPublic && ( +
    + +
    + )} +
    + +
    + {/* {isShowPendingPost && ( +
    + +
    + )} */} + +
    +
    +
    + ); +}; diff --git a/src/v4/social/components/CommunityHeader/index.ts b/src/v4/social/components/CommunityHeader/index.ts new file mode 100644 index 000000000..b275a1aa5 --- /dev/null +++ b/src/v4/social/components/CommunityHeader/index.ts @@ -0,0 +1 @@ +export { CommunityHeader } from './CommunityHeader'; diff --git a/src/v4/social/components/CommunityPin/CommunityPin.module.css b/src/v4/social/components/CommunityPin/CommunityPin.module.css new file mode 100644 index 000000000..e3dc19f10 --- /dev/null +++ b/src/v4/social/components/CommunityPin/CommunityPin.module.css @@ -0,0 +1,5 @@ +.communityPin__container { + display: flex; + flex-direction: column; + gap: 0.5rem; +} diff --git a/src/v4/social/components/CommunityPin/CommunityPin.tsx b/src/v4/social/components/CommunityPin/CommunityPin.tsx new file mode 100644 index 000000000..56fe2b157 --- /dev/null +++ b/src/v4/social/components/CommunityPin/CommunityPin.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { useAmityComponent } from '~/v4/core/hooks/uikit'; +import { EmptyPinnedPost } from '~/v4/social/components/EmptyPinnedPost'; +import styles from './CommunityPin.module.css'; +import useCommunity from '~/v4/core/hooks/collections/useCommunity'; +import LockPrivateContent from '~/v4/social/internal-components/LockPrivateContent'; + +interface CommunityPinProps { + pageId?: string; + communityId: string; +} + +export const CommunityPin = ({ pageId = '*', communityId }: CommunityPinProps) => { + const componentId = 'community_pin'; + const { accessibilityId, themeStyles, isExcluded } = useAmityComponent({ + pageId, + componentId, + }); + + if (isExcluded) return null; + + const { community } = useCommunity({ communityId, shouldCall: !!communityId }); + + const isMemberPrivateCommunity = community?.isJoined && !community?.isPublic; + + //TODO : Integrate with the new pinned post API + //TODO : Fix condition to show empty pinned post and lock private content + + return ( +
    + {isMemberPrivateCommunity || community?.isPublic ? ( + + ) : ( + + )} +
    + ); +}; diff --git a/src/v4/social/components/CommunityPin/index.ts b/src/v4/social/components/CommunityPin/index.ts new file mode 100644 index 000000000..b15e23891 --- /dev/null +++ b/src/v4/social/components/CommunityPin/index.ts @@ -0,0 +1 @@ +export { CommunityPin } from './CommunityPin'; diff --git a/src/v4/social/components/CommunityPinnedPost/CommunityPinnedPost.tsx b/src/v4/social/components/CommunityPinnedPost/CommunityPinnedPost.tsx new file mode 100644 index 000000000..861343082 --- /dev/null +++ b/src/v4/social/components/CommunityPinnedPost/CommunityPinnedPost.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +interface CommunityPinnedPostProps { + communityId?: string; +} + +export const CommunityPinnedPost = ({ communityId }: CommunityPinnedPostProps) => { + return
    CommunityPinnedPost
    ; +}; diff --git a/src/v4/social/components/CommunityPinnedPost/index.ts b/src/v4/social/components/CommunityPinnedPost/index.ts new file mode 100644 index 000000000..c89cf7961 --- /dev/null +++ b/src/v4/social/components/CommunityPinnedPost/index.ts @@ -0,0 +1 @@ +export { CommunityPinnedPost } from './CommunityPinnedPost'; diff --git a/src/v4/social/components/CommunitySearchResult/CommunitySearchResult.tsx b/src/v4/social/components/CommunitySearchResult/CommunitySearchResult.tsx index 7a85c146e..04936b802 100644 --- a/src/v4/social/components/CommunitySearchResult/CommunitySearchResult.tsx +++ b/src/v4/social/components/CommunitySearchResult/CommunitySearchResult.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import styles from './CommunitySearchResult.module.css'; import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; import { useAmityComponent } from '~/v4/core/hooks/uikit'; @@ -24,9 +24,9 @@ export const CommunitySearchResult = ({ componentId, }); - const intersectionRef = useRef(null); + const [intersectionNode, setIntersectionNode] = useState(null); - useIntersectionObserver({ onIntersect: () => onLoadMore(), ref: intersectionRef }); + useIntersectionObserver({ onIntersect: () => onLoadMore(), node: intersectionNode }); return (
    @@ -43,7 +43,7 @@ export const CommunitySearchResult = ({ )) : null} -
    +
    setIntersectionNode(node)} />
    ); }; diff --git a/src/v4/social/components/DetailedMediaAttachment/DetailedMediaAttachment.module.css b/src/v4/social/components/DetailedMediaAttachment/DetailedMediaAttachment.module.css index a93c9f236..40ed8ac49 100644 --- a/src/v4/social/components/DetailedMediaAttachment/DetailedMediaAttachment.module.css +++ b/src/v4/social/components/DetailedMediaAttachment/DetailedMediaAttachment.module.css @@ -2,14 +2,6 @@ display: block; width: 100%; background-color: var(--asc-color-background-default); - border-top-left-radius: 1.25rem; - border-top-right-radius: 1.25rem; - box-shadow: var(--asc-box-shadow-04); - padding-bottom: 2rem; - padding-top: 0.75rem; - margin-top: -1rem; - position: absolute; - bottom: -0.9rem; } .detailedMediaAttachment__swipeDown { diff --git a/src/v4/social/components/EmptyPinnedPost/EmptyPinnedPost.module.css b/src/v4/social/components/EmptyPinnedPost/EmptyPinnedPost.module.css new file mode 100644 index 000000000..320efe94a --- /dev/null +++ b/src/v4/social/components/EmptyPinnedPost/EmptyPinnedPost.module.css @@ -0,0 +1,18 @@ +.emptyPinnedPost__container { + margin: 5rem auto 3rem; + text-align: center; +} + +.emptyPinnedPost__text { + color: var(--asc-color-base-shade3); + font-size: 1rem; + font-weight: 600; + line-height: 1.375rem; + margin-top: 0.5rem; +} + +.emptyPinnedPost__icon { + fill: var(--asc-color-base-shade4); + width: 3.75rem; + height: 2.8125rem; +} diff --git a/src/v4/social/components/EmptyPinnedPost/EmptyPinnedPost.tsx b/src/v4/social/components/EmptyPinnedPost/EmptyPinnedPost.tsx new file mode 100644 index 000000000..4aee57b89 --- /dev/null +++ b/src/v4/social/components/EmptyPinnedPost/EmptyPinnedPost.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Typography } from '~/v4/core/components'; +import { useAmityComponent } from '~/v4/core/hooks/uikit'; +import styles from './EmptyPinnedPost.module.css'; +import EmptyPost from '~/v4/icons/EmptyPost'; + +interface EmptyPinnedPostProps { + pageId?: string; +} + +export const EmptyPinnedPost = ({ pageId = '*' }: EmptyPinnedPostProps) => { + const componentId = 'empty_pinned_post'; + const { config, accessibilityId, isExcluded, themeStyles } = useAmityComponent({ + pageId, + componentId, + }); + if (isExcluded) return null; + return ( +
    + + + No pinned post yet + +
    + ); +}; diff --git a/src/v4/social/components/EmptyPinnedPost/index.ts b/src/v4/social/components/EmptyPinnedPost/index.ts new file mode 100644 index 000000000..6088b7a11 --- /dev/null +++ b/src/v4/social/components/EmptyPinnedPost/index.ts @@ -0,0 +1 @@ +export { EmptyPinnedPost } from './EmptyPinnedPost'; diff --git a/src/v4/social/components/GlobalFeed/GlobalFeed.module.css b/src/v4/social/components/GlobalFeed/GlobalFeed.module.css index e5b6c7677..ddaa89ca6 100644 --- a/src/v4/social/components/GlobalFeed/GlobalFeed.module.css +++ b/src/v4/social/components/GlobalFeed/GlobalFeed.module.css @@ -4,8 +4,7 @@ } .global_feed__postContainer { - padding: 0.25rem 1rem 0.75rem; - width: 94%; + width: 100%; } .global_feed__postSkeletonContainer { diff --git a/src/v4/social/components/GlobalFeed/GlobalFeed.tsx b/src/v4/social/components/GlobalFeed/GlobalFeed.tsx index 2e15cc94a..c6ccaf817 100644 --- a/src/v4/social/components/GlobalFeed/GlobalFeed.tsx +++ b/src/v4/social/components/GlobalFeed/GlobalFeed.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, 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'; @@ -8,6 +8,10 @@ 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'; interface GlobalFeedProps { pageId?: string; @@ -35,12 +39,12 @@ export const GlobalFeed = ({ componentId, }); - const intersectionRef = useRef(null); + const [intersectionNode, setIntersectionNode] = useState(null); const { AmityGlobalFeedComponentBehavior } = usePageBehavior(); useIntersectionObserver({ - ref: intersectionRef, + node: intersectionNode, onIntersect: () => { onFeedReachBottom(); }, @@ -78,7 +82,8 @@ export const GlobalFeed = ({ { AmityGlobalFeedComponentBehavior?.goToPostDetailPage?.({ postId: item.postId }); }} @@ -98,7 +103,12 @@ export const GlobalFeed = ({
    )) : null} - {!isLoading &&
    } + {!isLoading && ( +
    setIntersectionNode(node)} + className={styles.global_feed__intersection} + /> + )}
    ); }; diff --git a/src/v4/social/components/MediaAttachment/MediaAttachment.module.css b/src/v4/social/components/MediaAttachment/MediaAttachment.module.css index a10cdf8c8..462944adf 100644 --- a/src/v4/social/components/MediaAttachment/MediaAttachment.module.css +++ b/src/v4/social/components/MediaAttachment/MediaAttachment.module.css @@ -3,14 +3,6 @@ align-items: center; justify-content: space-between; width: 100%; - padding: 0.5rem 1rem; - background-color: var(--asc-color-background-default); - border-top-left-radius: 1.25rem; - border-top-right-radius: 1.25rem; - box-shadow: var(--asc-box-shadow-04); - margin-top: 0.75rem; - position: absolute; - bottom: -0.4rem; } .mediaAttachment__swipeDown { diff --git a/src/v4/social/components/PostContent/PostContent.module.css b/src/v4/social/components/PostContent/PostContent.module.css index 5cabd922b..a2afeebcb 100644 --- a/src/v4/social/components/PostContent/PostContent.module.css +++ b/src/v4/social/components/PostContent/PostContent.module.css @@ -1,5 +1,6 @@ .postContent { background-color: var(--asc-color-background-default); + padding: 0.25rem 1rem 0.75rem; } .postContent__bar { @@ -35,8 +36,12 @@ } .postContent__bar__actionButton { + display: flex; + justify-content: center; + align-items: center; justify-self: flex-end; cursor: pointer; + gap: 0.5rem; } .postContent__content_and_reactions { @@ -165,6 +170,133 @@ } .postContent__bar__information__editedTag { - margin-left: 0.125rem; color: var(--asc-color-base-shade2); } + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + background-color: rgb(0 0 0 / 20%); + background-clip: padding-box; + border: 1px solid rgb(255 255 255 / 20%); + padding: 0.5rem 0.75rem; + color: white; + cursor: pointer; + outline: none; + transition: background-color 0.3s; +} + +.button:hover { + background-color: rgb(0 0 0 / 30%); +} + +.button:active { + background-color: rgb(0 0 0 / 40%); +} + +.button:focus-visible { + box-shadow: 0 0 0 2px rgb(255 255 255 / 75%); +} + +.popover { + padding: 0.25rem; + width: 14rem; + overflow: auto; + border-radius: 0.375rem; + background-color: white; + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 10%), + 0 4px 6px -2px rgb(0 0 0 / 5%); + box-shadow: 0 0 0 1px rgb(0 0 0 / 5%); +} + +.popover[data-entering] { + animation: + fade-in 0.2s ease-out, + zoom-in 0.2s ease-out; +} + +.popover[data-exiting] { + animation: + fade-out 0.2s ease-in, + zoom-out 0.2s ease-in; +} + +.menu { + outline: none; +} + +.menuItem { + display: flex; + width: 100%; + align-items: center; + border-radius: 0.375rem; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + outline: none; + cursor: pointer; + color: var(--asc-color-secondary-default); +} + +.menuItem:focus { + background-color: var(--asc-color-base-shade1); + color: white; +} + +.separator { + background-color: #d1d5db; + height: 1px; + margin: 0.25rem 0.75rem; +} + +.postContent__notMember { + display: flex; + justify-content: start; + align-items: center; + gap: 0.25rem; + padding-top: 0.25rem; + line-height: 1.25rem; + color: var(--asc-color-base-shade2); +} + +@keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fade-out { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes zoom-in { + from { + transform: scale(0.95); + } + + to { + transform: scale(1); + } +} + +@keyframes zoom-out { + from { + transform: scale(1); + } + + to { + transform: scale(0.95); + } +} diff --git a/src/v4/social/components/PostContent/PostContent.tsx b/src/v4/social/components/PostContent/PostContent.tsx index 3a5bcac3e..31618cb6c 100644 --- a/src/v4/social/components/PostContent/PostContent.tsx +++ b/src/v4/social/components/PostContent/PostContent.tsx @@ -38,13 +38,26 @@ import { Button } from '~/v4/core/natives/Button'; import { PageTypes, useNavigation } from '~/v4/core/providers/NavigationProvider'; import dayjs from 'dayjs'; import { useVisibilitySensor } from '~/v4/social/hooks/useVisibilitySensor'; +import { AnnouncementBadge } from '~/v4/social/elements/AnnouncementBadge'; + +export enum AmityPostContentComponentStyle { + FEED = 'feed', + DETAIL = 'detail', +} + +export enum AmityPostCategory { + GENERAL = 'general', + ANNOUNCEMENT = 'announcement', + PIN = 'pin', +} interface PostTitleProps { post: Amity.Post; pageId?: string; + hideTarget?: boolean; } -const PostTitle = ({ pageId, post }: PostTitleProps) => { +const PostTitle = ({ pageId, post, hideTarget }: PostTitleProps) => { const shouldCall = useMemo(() => post?.targetType === 'community', [post?.targetType]); const { community: targetCommunity } = useCommunity({ @@ -52,6 +65,8 @@ const PostTitle = ({ pageId, post }: PostTitleProps) => { shouldCall, }); + const { goToCommunityProfilePage } = useNavigation(); + const { user: postedUser } = useUser(post.postedUserId); const { onClickCommunity, onClickUser } = useNavigation(); @@ -65,19 +80,13 @@ const PostTitle = ({ pageId, post }: PostTitleProps) => { )} - {targetCommunity && ( - <> + {targetCommunity && !hideTarget && ( + - + + {targetCommunity.displayName} + {' '} + )}
    ); @@ -151,21 +160,25 @@ const ChildrenPostContent = ({ }; interface PostContentProps { - pageId?: string; post: Amity.Post; - type: 'feed' | 'detail'; - drawerRef?: React.RefObject; onClick?: () => void; onPostDeleted?: (post: Amity.Post) => void; + style: AmityPostContentComponentStyle; + category: AmityPostCategory; + hideMenu?: boolean; + hideTarget?: boolean; + pageId?: string; } export const PostContent = ({ pageId = '*', post: initialPost, - type, - drawerRef, onClick, onPostDeleted, + category, + hideMenu = false, + hideTarget = false, + style, }: PostContentProps) => { const componentId = 'post_content'; const { themeStyles } = useAmityComponent({ @@ -288,11 +301,17 @@ export const PostContent = ({ setClickedVideoIndex(null); }; - const hasLike = post?.reactions.like > 0; - const hasLove = post?.reactions.love > 0; - const hasFire = post?.reactions.fire > 0; - const hasHappy = post?.reactions.happy > 0; - const hasCrying = post?.reactions.crying > 0; + const handleUnpinPost = async () => {}; + + const handleEditPost = () => {}; + + const handleDeletePost = () => {}; + + const hasLike = post?.reactions?.like > 0; + const hasLove = post?.reactions?.love > 0; + const hasFire = post?.reactions?.fire > 0; + const hasHappy = post?.reactions?.happy > 0; + const hasCrying = post?.reactions?.crying > 0; const hasReaction = hasLike || hasLove || hasFire || hasHappy || hasCrying; @@ -310,13 +329,16 @@ export const PostContent = ({ return (
    -
    + {category === AmityPostCategory.ANNOUNCEMENT && ( + + )} +
    - +
    {isCommunityModerator ? ( @@ -333,27 +355,30 @@ export const PostContent = ({ )}
    -
    - {type === 'feed' ? ( - - setDrawerData({ - content: ( - removeDrawerData()} - pageId={pageId} - componentId={componentId} - onPostDeleted={onPostDeleted} - /> - ), - }) - } - /> - ) : null} -
    + + {style === AmityPostContentComponentStyle.FEED ? ( +
    + {!hideMenu && ( + + setDrawerData({ + content: ( + removeDrawerData()} + pageId={pageId} + componentId={componentId} + onPostDeleted={onPostDeleted} + /> + ), + }) + } + /> + )} +
    + ) : null}
    @@ -366,7 +391,7 @@ export const PostContent = ({ /> ) : null}
    - {type === 'detail' ? ( + {style === AmityPostContentComponentStyle.DETAIL ? (
    ) : null} -
    -
    -
    - - onClick?.()} - /> -
    -
    - -
    -
    + {!targetCommunity?.isJoined && page.type === PageTypes.CommunityProfilePage ? ( + <> + + Join community to interact with all posts + + + ) : !targetCommunity?.isJoined && page.type === PageTypes.PostDetailPage ? null : ( + <> +
    +
    +
    + + onClick?.()} + /> +
    +
    + +
    +
    + + )}
    {isImageViewerOpen && typeof clickedImageIndex === 'number' ? ( diff --git a/src/v4/social/components/StoryTab/StoryTabCommunity.tsx b/src/v4/social/components/StoryTab/StoryTabCommunity.tsx index 7d446a0be..bcde21eeb 100644 --- a/src/v4/social/components/StoryTab/StoryTabCommunity.tsx +++ b/src/v4/social/components/StoryTab/StoryTabCommunity.tsx @@ -6,7 +6,7 @@ import clsx from 'clsx'; import { useGetActiveStoriesByTarget } from '~/v4/social/hooks/useGetActiveStories'; import useSDK from '~/v4/core/hooks/useSDK'; import { useUser } from '~/v4/core/hooks/objects/useUser'; -import { isAdmin, isModerator } from '~/v4/utils/permissions'; +import { isAdmin } from '~/v4/utils/permissions'; import { checkStoryPermission } from '~/v4/social/utils'; import { useCommunityInfo } from '~/v4/social/hooks/useCommunityInfo'; import { CreateNewStoryButton } from '~/v4/social/elements/CreateNewStoryButton'; @@ -65,9 +65,7 @@ export const StoryTabCommunityFeed: React.FC = ({ const { currentUserId, client } = useSDK(); const { user } = useUser(currentUserId); const isGlobalAdmin = isAdmin(user?.roles); - const isCommunityModerator = isModerator(user?.roles); - const hasStoryPermission = - isGlobalAdmin || isCommunityModerator || checkStoryPermission(client, communityId); + const hasStoryPermission = isGlobalAdmin || checkStoryPermission(client, communityId); const hasStories = stories?.length > 0; const hasUnSeen = stories.some((story) => !story?.isSeen); const uploading = stories.some((story) => story?.syncState === 'syncing'); diff --git a/src/v4/social/components/UserSearchResult/UserSearchItem.module.css b/src/v4/social/components/UserSearchResult/UserSearchItem.module.css index 57a641301..7b55aa592 100644 --- a/src/v4/social/components/UserSearchResult/UserSearchItem.module.css +++ b/src/v4/social/components/UserSearchResult/UserSearchItem.module.css @@ -4,6 +4,7 @@ padding-top: 0.5rem; padding-bottom: 0.5rem; width: 100%; + cursor: pointer; } .userItem__leftPane { diff --git a/src/v4/social/components/UserSearchResult/UserSearchItem.tsx b/src/v4/social/components/UserSearchResult/UserSearchItem.tsx index 70572f40c..a67d5c0ea 100644 --- a/src/v4/social/components/UserSearchResult/UserSearchItem.tsx +++ b/src/v4/social/components/UserSearchResult/UserSearchItem.tsx @@ -2,14 +2,19 @@ import React from 'react'; import { UserAvatar } from '~/v4/social/internal-components/UserAvatar/UserAvatar'; 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'; interface UserSearchItemProps { user: Amity.User; + onClick?: () => void; } export const UserSearchItem = ({ user }: UserSearchItemProps) => { + const { onClickUser } = useNavigation(); + return ( -
    +
    -
    + ); }; diff --git a/src/v4/social/components/UserSearchResult/UserSearchResult.tsx b/src/v4/social/components/UserSearchResult/UserSearchResult.tsx index 3856b1074..f419ccb6e 100644 --- a/src/v4/social/components/UserSearchResult/UserSearchResult.tsx +++ b/src/v4/social/components/UserSearchResult/UserSearchResult.tsx @@ -1,9 +1,10 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import styles from './UserSearchResult.module.css'; import useIntersectionObserver from '~/v4/core/hooks/useIntersectionObserver'; import { useAmityComponent } from '~/v4/core/hooks/uikit'; import { UserSearchItem } from './UserSearchItem'; import { UserSearchItemSkeleton } from './UserSearchItemSkeleton'; +import { usePageBehavior } from '~/v4/core/providers/PageBehaviorProvider'; interface UserSearchResultProps { pageId?: string; @@ -25,9 +26,9 @@ export const UserSearchResult = ({ componentId, }); - const intersectionRef = useRef(null); + const [intersectionNode, setIntersectionNode] = useState(null); - useIntersectionObserver({ onIntersect: () => onLoadMore(), ref: intersectionRef }); + useIntersectionObserver({ onIntersect: () => onLoadMore(), node: intersectionNode }); return (
    @@ -39,7 +40,7 @@ export const UserSearchResult = ({ )) : null} -
    +
    setIntersectionNode(node)} />
    ); }; diff --git a/src/v4/social/components/index.ts b/src/v4/social/components/index.ts index 8cbcd8eb9..aff4b42c5 100644 --- a/src/v4/social/components/index.ts +++ b/src/v4/social/components/index.ts @@ -15,3 +15,6 @@ export * from './GlobalFeed'; export * from './EmptyNewsFeed'; export * from './Newsfeed'; export * from './TopNavigation'; +export * from './CommunityHeader'; +export * from './CommunityFeed'; +export * from './CommunityPinnedPost'; diff --git a/src/v4/social/elements/AnnouncementBadge/AnnouncementBadge.module.css b/src/v4/social/elements/AnnouncementBadge/AnnouncementBadge.module.css new file mode 100644 index 000000000..c2b62c9bb --- /dev/null +++ b/src/v4/social/elements/AnnouncementBadge/AnnouncementBadge.module.css @@ -0,0 +1,5 @@ +.announcementBadge { + position: relative; + left: -1rem; + margin-bottom: 0.5rem; +} diff --git a/src/v4/social/elements/AnnouncementBadge/AnnouncementBadge.tsx b/src/v4/social/elements/AnnouncementBadge/AnnouncementBadge.tsx new file mode 100644 index 000000000..a0d0b6830 --- /dev/null +++ b/src/v4/social/elements/AnnouncementBadge/AnnouncementBadge.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styles from './AnnouncementBadge.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { IconComponent } from '~/v4/core/IconComponent'; +import { FeaturedIcon } from '~/v4/icons/Featured/Featured'; + +interface AnnouncementBadge { + pageId?: string; + componentId?: string; +} + +export const AnnouncementBadge = ({ pageId = '*', componentId = '*' }: AnnouncementBadge) => { + const elementId = 'announcement_badge'; + const { config, isExcluded, accessibilityId, uiReference, defaultConfig } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + } + imgIcon={() => {uiReference}} + configIconName={config.icon} + defaultIconName={defaultConfig.icon} + /> + ); +}; diff --git a/src/v4/social/elements/AnnouncementBadge/index.ts b/src/v4/social/elements/AnnouncementBadge/index.ts new file mode 100644 index 000000000..a4eb96d09 --- /dev/null +++ b/src/v4/social/elements/AnnouncementBadge/index.ts @@ -0,0 +1 @@ +export { AnnouncementBadge } from './AnnouncementBadge'; diff --git a/src/v4/social/elements/BackButton/BackButton.tsx b/src/v4/social/elements/BackButton/BackButton.tsx index 977aca3a9..c02c05c81 100644 --- a/src/v4/social/elements/BackButton/BackButton.tsx +++ b/src/v4/social/elements/BackButton/BackButton.tsx @@ -9,7 +9,7 @@ const BackButtonSvg = (props: React.SVGProps) => ( width="10" height="17" viewBox="0 0 10 17" - fill="none" + fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props} > diff --git a/src/v4/social/elements/CameraButton/CameraButton.tsx b/src/v4/social/elements/CameraButton/CameraButton.tsx index 8784cc592..c9ad831c7 100644 --- a/src/v4/social/elements/CameraButton/CameraButton.tsx +++ b/src/v4/social/elements/CameraButton/CameraButton.tsx @@ -66,27 +66,22 @@ export function CameraButton({ fileInput.click(); }; - const onLoadMedia: React.ChangeEventHandler = useCallback((e) => { - e.preventDefault(); - e.stopPropagation(); - const targetFiles = e.target.files ? [...e.target.files] : []; - const isImage = targetFiles.some((file) => file.type.startsWith('image/')); - const isVideo = targetFiles.some((file) => file.type.startsWith('video/')); + const onLoadMedia: React.ChangeEventHandler = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + const targetFiles = e.target.files ? [...e.target.files] : []; + const isImage = targetFiles.some((file) => file.type.startsWith('image/')); + const isVideo = targetFiles.some((file) => file.type.startsWith('video/')); - if (isImage) { - onImageFileChange?.(e.target.files ? [...e.target.files] : []); - } else if (isVideo) { - onVideoFileChange?.(e.target.files ? [...e.target.files] : []); - } - }, []); - - const onImageChange: React.ChangeEventHandler = useCallback((e) => { - onImageFileChange?.(e.target.files ? [...e.target.files] : []); - }, []); - - const onVideoChange: React.ChangeEventHandler = useCallback((e) => { - onImageFileChange?.(e.target.files ? [...e.target.files] : []); - }, []); + if (isImage) { + onImageFileChange?.(targetFiles); + } else if (isVideo) { + onVideoFileChange?.(targetFiles); + } + }, + [onImageFileChange, onVideoFileChange], + ); return ( ); } diff --git a/src/v4/social/elements/CommunityCategory/CommunityCategory.module.css b/src/v4/social/elements/CommunityCategory/CommunityCategory.module.css new file mode 100644 index 000000000..fee67d974 --- /dev/null +++ b/src/v4/social/elements/CommunityCategory/CommunityCategory.module.css @@ -0,0 +1,11 @@ +.community__category { + display: flex; + align-items: center; + gap: 0.5rem; + overflow-x: scroll; + width: 100%; +} + +.community__category::-webkit-scrollbar { + display: none; +} diff --git a/src/v4/social/elements/CommunityCategory/CommunityCategory.tsx b/src/v4/social/elements/CommunityCategory/CommunityCategory.tsx new file mode 100644 index 000000000..debf081ec --- /dev/null +++ b/src/v4/social/elements/CommunityCategory/CommunityCategory.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import styles from './CommunityCategory.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { CommunityCategoryName } from '~/v4/social/elements/CommunityCategoryName'; + +interface CommunityCategoryProps { + categories: Amity.Category[]; + pageId?: string; + componentId?: string; +} + +export const CommunityCategory = ({ + pageId = '*', + componentId = '*', + categories, +}: CommunityCategoryProps) => { + const elementId = 'community_category'; + const { config, themeStyles, accessibilityId, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( +
    + {categories.map((category) => ( + + ))} +
    + ); +}; diff --git a/src/v4/social/elements/CommunityCategory/index.ts b/src/v4/social/elements/CommunityCategory/index.ts new file mode 100644 index 000000000..9012445ee --- /dev/null +++ b/src/v4/social/elements/CommunityCategory/index.ts @@ -0,0 +1 @@ +export { CommunityCategory } from './CommunityCategory'; diff --git a/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.module.css b/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.module.css index 496881b71..1986addeb 100644 --- a/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.module.css +++ b/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.module.css @@ -1,9 +1,12 @@ .communityCategoryName { display: flex; - padding: 0 0.5rem; + 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; } diff --git a/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.tsx b/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.tsx index b4d41b8bd..37b240d5f 100644 --- a/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.tsx +++ b/src/v4/social/elements/CommunityCategoryName/CommunityCategoryName.tsx @@ -1,9 +1,7 @@ import React from 'react'; +import styles from './CommunityCategoryName.module.css'; import { Typography } from '~/v4/core/components'; import { useAmityElement } from '~/v4/core/hooks/uikit'; -import { useCustomization } from '~/v4/core/providers/CustomizationProvider'; -import { useGenerateStylesShadeColors } from '~/v4/core/providers/ThemeProvider'; -import styles from './CommunityCategoryName.module.css'; export interface CommunityCategoryNameProps { pageId?: string; @@ -17,12 +15,11 @@ export function CommunityCategoryName({ categoryName, }: CommunityCategoryNameProps) { const elementId = 'community_category_name'; - const { accessibilityId, config, defaultConfig, isExcluded, uiReference, themeStyles } = - useAmityElement({ - pageId, - componentId, - elementId, - }); + const { accessibilityId, isExcluded, themeStyles } = useAmityElement({ + pageId, + componentId, + elementId, + }); if (isExcluded) return null; diff --git a/src/v4/social/elements/CommunityCover/CommunityCover.module.css b/src/v4/social/elements/CommunityCover/CommunityCover.module.css new file mode 100644 index 000000000..90bee2be0 --- /dev/null +++ b/src/v4/social/elements/CommunityCover/CommunityCover.module.css @@ -0,0 +1,45 @@ +.communityCover__container { + position: relative; + width: 100%; + padding-top: 56.25%; /* 16:9 Aspect Ratio for mobile */ + background-size: cover; + background-position: center; + background-repeat: no-repeat; + overflow: hidden; +} + +@media (width >= 768px) { + .communityCover__container { + padding-top: 33.33%; /* 3:1 Aspect Ratio for desktop */ + max-height: 11.75rem; /* Limit the maximum height on larger screens */ + } +} + +.communityCover__topBar { + position: absolute; + top: 3rem; + left: 1rem; + right: 1rem; + display: flex; + justify-content: space-between; +} + +.communityCover__backButton { + fill: var(--asc-color-primary-shade4); + background: rgb(0 0 0 / 50%); + border-radius: 50%; + padding: 0.5rem; + width: 2rem; + height: 2rem; + cursor: pointer; +} + +.communityCover__menuButton { + fill: var(--asc-color-primary-shade4); + background: rgb(0 0 0 / 50%); + border-radius: 50%; + padding: 0.5rem; + width: 1rem; + height: 1rem; + cursor: pointer; +} diff --git a/src/v4/social/elements/CommunityCover/CommunityCover.tsx b/src/v4/social/elements/CommunityCover/CommunityCover.tsx new file mode 100644 index 000000000..8c62c8a74 --- /dev/null +++ b/src/v4/social/elements/CommunityCover/CommunityCover.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import styles from './CommunityCover.module.css'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { BackButton } from '~/v4/social/elements/BackButton'; +import { CommunityProfileMenuButton } from '~/v4/social/elements/CommunityProfileMenuButton'; + +interface CommunityCoverProps { + pageId?: string; + componentId?: string; + image?: string; + onBack?: () => void; + onClickMenu?: () => void; +} + +export const CommunityCover: React.FC = ({ + pageId = '*', + componentId = '*', + image, + onBack, + onClickMenu, +}) => { + const elementId = 'community_cover'; + const { isExcluded, accessibilityId, themeStyles, config } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + const backgroundStyle = image + ? { backgroundImage: `url(${image})` } + : { background: 'linear-gradient(180deg, #a5a9b5 0%, #636878 100%)' }; + + if (isExcluded) return null; + + return ( +
    +
    +
    + + +
    +
    + ); +}; diff --git a/src/v4/social/elements/CommunityCover/index.ts b/src/v4/social/elements/CommunityCover/index.ts new file mode 100644 index 000000000..57a928db9 --- /dev/null +++ b/src/v4/social/elements/CommunityCover/index.ts @@ -0,0 +1 @@ +export { CommunityCover } from './CommunityCover'; diff --git a/src/v4/social/elements/CommunityCreatePostButton/CommunityCreatePostButton.tsx b/src/v4/social/elements/CommunityCreatePostButton/CommunityCreatePostButton.tsx new file mode 100644 index 000000000..2371786f8 --- /dev/null +++ b/src/v4/social/elements/CommunityCreatePostButton/CommunityCreatePostButton.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { IconComponent } from '~/v4/core/IconComponent'; +import { Button, ButtonProps } from '~/v4/core/natives/Button'; +import { CommunityCreatePostButtonIcon } from '~/v4/icons/CommunityCreatePost'; + +interface CommunityCreatePostButtonProps { + pageId?: string; + componentId?: string; + defaultClassName?: string; + imgClassName?: string; + onPress?: ButtonProps['onPress']; +} + +export const CommunityCreatePostButton = ({ + pageId = '*', + componentId = '*', + defaultClassName, + imgClassName, + onPress, +}: CommunityCreatePostButtonProps) => { + const elementId = 'community_create_post_button'; + const { config, accessibilityId, themeStyles, isExcluded, defaultConfig, uiReference } = + useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + + ); +}; diff --git a/src/v4/social/elements/CommunityCreatePostButton/index.ts b/src/v4/social/elements/CommunityCreatePostButton/index.ts new file mode 100644 index 000000000..d7bd69b88 --- /dev/null +++ b/src/v4/social/elements/CommunityCreatePostButton/index.ts @@ -0,0 +1 @@ +export { CommunityCreatePostButton } from './CommunityCreatePostButton'; diff --git a/src/v4/social/elements/CommunityDescription/CommunityDescription.module.css b/src/v4/social/elements/CommunityDescription/CommunityDescription.module.css new file mode 100644 index 000000000..ac12760f7 --- /dev/null +++ b/src/v4/social/elements/CommunityDescription/CommunityDescription.module.css @@ -0,0 +1,26 @@ +.descriptionWrapper { + width: 100%; + box-sizing: border-box; +} + +.description { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + line-height: 1.5rem; + word-wrap: break-word; + word-break: break-word; +} + +.description.expanded { + overflow: visible; +} + +.toggleButton { + background: none; + border: none; + color: var(--asc-color-primary-default); + cursor: pointer; + padding: 0; + font-size: inherit; +} diff --git a/src/v4/social/elements/CommunityDescription/CommunityDescription.tsx b/src/v4/social/elements/CommunityDescription/CommunityDescription.tsx new file mode 100644 index 000000000..c0cb1948b --- /dev/null +++ b/src/v4/social/elements/CommunityDescription/CommunityDescription.tsx @@ -0,0 +1,62 @@ +import React, { useState, useRef, useEffect } from 'react'; +import styles from './CommunityDescription.module.css'; +import { Typography } from '~/v4/core/components'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { Button } from '~/v4/core/natives/Button'; + +interface CommunityDescriptionProps { + description: string; + pageId?: string; + componentId?: string; +} + +export const CommunityDescription: React.FC = ({ + pageId = '*', + componentId = '*', + description = '', +}) => { + const [isExpanded, setIsExpanded] = useState(false); + const [showToggle, setShowToggle] = useState(false); + const descriptionRef = useRef(null); + + const elementId = 'community_description'; + const { themeStyles, accessibilityId, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + const maxLength = 180; + + const truncatedText = + description.length > maxLength ? description.slice(0, maxLength) : description; + + useEffect(() => { + if (descriptionRef.current) { + setShowToggle(description.length > maxLength); + } + }, [description]); + + if (isExcluded) return null; + + const expandText = () => setIsExpanded(true); + + return ( +
    +
    + + {isExpanded ? description : truncatedText}{' '} + {showToggle && !isExpanded && ( + + )} + +
    +
    + ); +}; diff --git a/src/v4/social/elements/CommunityDescription/index.ts b/src/v4/social/elements/CommunityDescription/index.ts new file mode 100644 index 000000000..114e4f949 --- /dev/null +++ b/src/v4/social/elements/CommunityDescription/index.ts @@ -0,0 +1 @@ +export { CommunityDescription } from './CommunityDescription'; diff --git a/src/v4/social/elements/CommunityFeedTabButton/CommunityFeedTabButton.tsx b/src/v4/social/elements/CommunityFeedTabButton/CommunityFeedTabButton.tsx new file mode 100644 index 000000000..0c98b7ef8 --- /dev/null +++ b/src/v4/social/elements/CommunityFeedTabButton/CommunityFeedTabButton.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; + +interface CommunityFeedTabButtonProps { + pageId?: string; + componentId?: string; +} + +export const CommunityFeedTabButton = ({ + pageId = '*', + componentId = '*', +}: CommunityFeedTabButtonProps) => { + const elementId = 'community_feed_tab_button'; + const { config, accessibilityId, themeStyles, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( +
    + CommunityFeedTabButton +
    + ); +}; diff --git a/src/v4/social/elements/CommunityFeedTabButton/index.ts b/src/v4/social/elements/CommunityFeedTabButton/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/v4/social/elements/CommunityInfo/CommunityInfo.module.css b/src/v4/social/elements/CommunityInfo/CommunityInfo.module.css new file mode 100644 index 000000000..d3569d498 --- /dev/null +++ b/src/v4/social/elements/CommunityInfo/CommunityInfo.module.css @@ -0,0 +1,20 @@ +.communityInfo__container { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; +} + +.communityInfo__wrapper { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.communityInfo__count { + color: var(--asc-color-base-default); +} + +.communityInfo__title { + color: var(--asc-color-base-shade2); +} diff --git a/src/v4/social/elements/CommunityInfo/CommunityInfo.tsx b/src/v4/social/elements/CommunityInfo/CommunityInfo.tsx new file mode 100644 index 000000000..456bf11ee --- /dev/null +++ b/src/v4/social/elements/CommunityInfo/CommunityInfo.tsx @@ -0,0 +1,40 @@ +import millify from 'millify'; +import React from 'react'; +import { Typography } from '~/v4/core/components'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import styles from './CommunityInfo.module.css'; +import { Button } from '~/v4/core/natives/Button'; + +interface CommunityInfoProps { + count: number; + text: string; + pageId?: string; + componentId?: string; + onClick?: () => void; +} + +export const CommunityInfo = ({ + pageId = '*', + componentId = '*', + count, + text, + onClick, +}: CommunityInfoProps) => { + const elementId = 'community_info'; + const { config, accessibilityId, themeStyles, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + if (isExcluded) return null; + return ( + + ); +}; diff --git a/src/v4/social/elements/CommunityInfo/index.ts b/src/v4/social/elements/CommunityInfo/index.ts new file mode 100644 index 000000000..b3f90956b --- /dev/null +++ b/src/v4/social/elements/CommunityInfo/index.ts @@ -0,0 +1 @@ +export { CommunityInfo } from './CommunityInfo'; diff --git a/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.module.css b/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.module.css new file mode 100644 index 000000000..da25c1d29 --- /dev/null +++ b/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.module.css @@ -0,0 +1,16 @@ +.communityJoinButton { + display: flex; + width: 100%; + background: var(--asc-color-primary-default); + padding: 0.625rem 1rem 0.625rem 0.75rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + border-radius: 0.5rem; + margin-top: 1rem; +} + +.joinButton { + width: 1.25rem; + height: 1rem; +} diff --git a/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.tsx b/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.tsx new file mode 100644 index 000000000..6e775613d --- /dev/null +++ b/src/v4/social/elements/CommunityJoinButton/CommunityJoinButton.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Button } from '~/v4/core/natives/Button'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import styles from './CommunityJoinButton.module.css'; +import { IconComponent } from '~/v4/core/IconComponent'; +import { Plus as PlusIcon } from '~/v4/icons/Plus'; +import clsx from 'clsx'; + +interface CommunityJoinButtonProps { + pageId?: string; + componentId?: string; + onClick?: () => void; + className?: string; + defaultClassName?: string; +} + +export const CommunityJoinButton = ({ + pageId = '*', + componentId = '*', + onClick, + className, + defaultClassName, +}: CommunityJoinButtonProps) => { + const elementId = 'community_join_button'; + const { config, themeStyles, accessibilityId, isExcluded, uiReference, defaultConfig } = + useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + + ); +}; diff --git a/src/v4/social/elements/CommunityJoinButton/index.ts b/src/v4/social/elements/CommunityJoinButton/index.ts new file mode 100644 index 000000000..68c4ad0f6 --- /dev/null +++ b/src/v4/social/elements/CommunityJoinButton/index.ts @@ -0,0 +1 @@ +export { CommunityJoinButton } from './CommunityJoinButton'; diff --git a/src/v4/social/elements/CommunityName/CommunityName.module.css b/src/v4/social/elements/CommunityName/CommunityName.module.css new file mode 100644 index 000000000..7b5b9e6a2 --- /dev/null +++ b/src/v4/social/elements/CommunityName/CommunityName.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/CommunityName/CommunityName.tsx b/src/v4/social/elements/CommunityName/CommunityName.tsx new file mode 100644 index 000000000..551e26707 --- /dev/null +++ b/src/v4/social/elements/CommunityName/CommunityName.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styles from './CommunityName.module.css'; +import { Typography } from '~/v4/core/components'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; + +interface CommunityNameProps { + name: string; + pageId?: string; + componentId?: string; +} + +export const CommunityName = ({ + pageId = '*', + componentId = '*', + name = '', +}: CommunityNameProps) => { + const elementId = 'community_name'; + const { config, themeStyles, accessibilityId, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + + {name} + + ); +}; diff --git a/src/v4/social/elements/CommunityName/index.ts b/src/v4/social/elements/CommunityName/index.ts new file mode 100644 index 000000000..444e64fdd --- /dev/null +++ b/src/v4/social/elements/CommunityName/index.ts @@ -0,0 +1 @@ +export { CommunityName } from './CommunityName'; diff --git a/src/v4/social/elements/CommunityPendingPost/CommunityPendingPost.module.css b/src/v4/social/elements/CommunityPendingPost/CommunityPendingPost.module.css new file mode 100644 index 000000000..1d7f827f8 --- /dev/null +++ b/src/v4/social/elements/CommunityPendingPost/CommunityPendingPost.module.css @@ -0,0 +1,41 @@ +.communityPendingPost__container { + background-color: var(--asc-color-base-shade4); + border-radius: 0.25rem; + padding: 1rem; +} + +.communityPendingPost__content { + display: flex; + justify-content: center; + align-items: center; +} + +.communityPendingPost__icon { + width: 0.375rem; + height: 0.375rem; + background-color: var(--asc-color-primary-default); + border-radius: 50%; +} + +.communityPendingPost__textContainer { + display: flex; + flex-direction: column; + text-align: center; +} + +.communityPendingPost__title__wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; +} + +.communityPendingPost__title { + color: var(--asc-color-base-default); + margin: 0; +} + +.communityPendingPost__subtext { + color: var(--asc-color-base-shade1); + margin: 0; +} diff --git a/src/v4/social/elements/CommunityPendingPost/CommunityPendingPost.tsx b/src/v4/social/elements/CommunityPendingPost/CommunityPendingPost.tsx new file mode 100644 index 000000000..af1210c80 --- /dev/null +++ b/src/v4/social/elements/CommunityPendingPost/CommunityPendingPost.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Typography } from '~/v4/core/components'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import styles from './CommunityPendingPost.module.css'; + +interface CommunityPendingPostProps { + pageId?: string; + componentId?: string; + pendingPostsCount?: number; +} + +export const CommunityPendingPost: React.FC = ({ + pageId = '*', + componentId = '*', + pendingPostsCount = 0, +}) => { + const elementId = 'community_pending_post'; + const { config, themeStyles, accessibilityId, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( +
    +
    +
    +
    +
    + Pending posts +
    + + {pendingPostsCount == 1 + ? '1 post need approval' + : `${pendingPostsCount} posts need approval`} + +
    +
    +
    + ); +}; diff --git a/src/v4/social/elements/CommunityPendingPost/index.ts b/src/v4/social/elements/CommunityPendingPost/index.ts new file mode 100644 index 000000000..43f6ac2cc --- /dev/null +++ b/src/v4/social/elements/CommunityPendingPost/index.ts @@ -0,0 +1 @@ +export { CommunityPendingPost } from './CommunityPendingPost'; diff --git a/src/v4/social/elements/CommunityPinTabButton/CommunityPinTabButton.tsx b/src/v4/social/elements/CommunityPinTabButton/CommunityPinTabButton.tsx new file mode 100644 index 000000000..3830dc44f --- /dev/null +++ b/src/v4/social/elements/CommunityPinTabButton/CommunityPinTabButton.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; + +interface CommunityPinTabButtonProps { + pageId?: string; + componentId?: string; +} + +export const CommunityPinTabButton = ({ + pageId = '*', + componentId = '*', +}: CommunityPinTabButtonProps) => { + const elementId = 'community_pin_tab_button'; + const { config, themeStyles, accessibilityId, isExcluded } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( +
    + CommunityPinTabButton +
    + ); +}; diff --git a/src/v4/social/elements/CommunityPinTabButton/index.ts b/src/v4/social/elements/CommunityPinTabButton/index.ts new file mode 100644 index 000000000..3f875611c --- /dev/null +++ b/src/v4/social/elements/CommunityPinTabButton/index.ts @@ -0,0 +1 @@ +export { CommunityPinTabButton } from './CommunityPinTabButton'; diff --git a/src/v4/social/elements/CommunityProfileMenuButton/CommunityProfileMenuButton.module.css b/src/v4/social/elements/CommunityProfileMenuButton/CommunityProfileMenuButton.module.css new file mode 100644 index 000000000..2d5e13162 --- /dev/null +++ b/src/v4/social/elements/CommunityProfileMenuButton/CommunityProfileMenuButton.module.css @@ -0,0 +1,6 @@ +.menuButton { + display: flex; + justify-content: center; + align-items: center; + fill: var(--asc-color-primary-shade4); +} diff --git a/src/v4/social/elements/CommunityProfileMenuButton/CommunityProfileMenuButton.tsx b/src/v4/social/elements/CommunityProfileMenuButton/CommunityProfileMenuButton.tsx new file mode 100644 index 000000000..4723e3ba7 --- /dev/null +++ b/src/v4/social/elements/CommunityProfileMenuButton/CommunityProfileMenuButton.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { IconComponent } from '~/v4/core/IconComponent'; +import { Button } from '~/v4/core/natives/Button'; +import styles from './CommunityProfileMenuButton.module.css'; +import clsx from 'clsx'; +import { EllipsisH } from '~/v4/icons/Ellipsis'; + +export interface CommunityProfileMenuButtonProps { + pageId?: string; + componentId?: string; + onClick?: () => void; + className?: string; + defaultClassName?: string; +} + +export function CommunityProfileMenuButton({ + pageId = '*', + componentId = '*', + onClick, + className, + defaultClassName, +}: CommunityProfileMenuButtonProps) { + const elementId = 'menu_button'; + const { isExcluded, accessibilityId, themeStyles, config, defaultConfig, uiReference } = + useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + + ); +} diff --git a/src/v4/social/elements/CommunityProfileMenuButton/index.tsx b/src/v4/social/elements/CommunityProfileMenuButton/index.tsx new file mode 100644 index 000000000..4ca489bd8 --- /dev/null +++ b/src/v4/social/elements/CommunityProfileMenuButton/index.tsx @@ -0,0 +1 @@ +export { CommunityProfileMenuButton } from './CommunityProfileMenuButton'; diff --git a/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.module.css b/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.module.css new file mode 100644 index 000000000..65c37cc0a --- /dev/null +++ b/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.module.css @@ -0,0 +1,27 @@ +.communityTabs__container { + background-color: var(--asc-color-base-background); + border-radius: 8px; + display: flex; + align-items: center; + width: 100%; +} + +.communityTabs__tab { + display: flex; + justify-content: center; + padding: 1rem; + flex-grow: 1; + width: 100%; + height: 1rem; + align-items: center; + + --asc-icon-color: var(--asc-color-base-shade3); + + color: var(--asc-icon-color); +} + +.communityTabs__tab[data-is-active='true'] { + border-bottom: 2px solid var(--asc-color-primary-default); + + --asc-icon-color: var(--asc-color-base-default); +} diff --git a/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.tsx b/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.tsx new file mode 100644 index 000000000..b673a1cb8 --- /dev/null +++ b/src/v4/social/elements/CommunityProfileTab/CommunityProfileTab.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import styles from './CommunityProfileTab.module.css'; +import clsx from 'clsx'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { Button } from '~/v4/core/natives/Button'; +import { Feed as FeedIcon } from '~/v4/icons/Feed'; +import { Pin as PinIcon } from '~/v4/icons/Pin'; + +interface CommunityTabsProps { + pageId: string; + componentId?: string; + activeTab: 'community_feed' | 'community_pin'; + onTabChange: (tab: 'community_feed' | 'community_pin') => void; +} + +export const CommunityProfileTab: React.FC = ({ + pageId, + componentId = '*', + activeTab, + onTabChange, +}) => { + const elementId = 'community_profile_tab'; + + const { isExcluded, accessibilityId, themeStyles } = useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( +
    + + +
    + ); +}; diff --git a/src/v4/social/elements/CommunityProfileTab/index.ts b/src/v4/social/elements/CommunityProfileTab/index.ts new file mode 100644 index 000000000..276c79d9b --- /dev/null +++ b/src/v4/social/elements/CommunityProfileTab/index.ts @@ -0,0 +1 @@ +export { CommunityProfileTab } from './CommunityProfileTab'; diff --git a/src/v4/social/elements/CommunityVerifyBadge/CommunityVerifyBadge.tsx b/src/v4/social/elements/CommunityVerifyBadge/CommunityVerifyBadge.tsx new file mode 100644 index 000000000..d2a1b9442 --- /dev/null +++ b/src/v4/social/elements/CommunityVerifyBadge/CommunityVerifyBadge.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useAmityElement } from '~/v4/core/hooks/uikit'; +import { IconComponent } from '~/v4/core/IconComponent'; +import { VerifyBadgeIcon } from '~/v4/icons/VerifyBadge'; + +interface CommunityVerifyBadgeProps { + pageId?: string; + componentId?: string; +} + +export const CommunityVerifyBadge = ({ + pageId = '*', + componentId = '*', +}: CommunityVerifyBadgeProps) => { + const elementId = 'community_verify_badge'; + const { config, themeStyles, accessibilityId, isExcluded, uiReference, defaultConfig } = + useAmityElement({ + pageId, + componentId, + elementId, + }); + + if (isExcluded) return null; + + return ( + } + imgIcon={() => {uiReference}} + defaultIconName={defaultConfig.icon} + configIconName={config.icon} + /> + ); +}; diff --git a/src/v4/social/elements/CommunityVerifyBadge/index.ts b/src/v4/social/elements/CommunityVerifyBadge/index.ts new file mode 100644 index 000000000..b9d605686 --- /dev/null +++ b/src/v4/social/elements/CommunityVerifyBadge/index.ts @@ -0,0 +1 @@ +export { CommunityVerifyBadge } from './CommunityVerifyBadge'; diff --git a/src/v4/social/elements/MenuButton/MenuButton.module.css b/src/v4/social/elements/MenuButton/MenuButton.module.css index 2aed04dc5..f1f084701 100644 --- a/src/v4/social/elements/MenuButton/MenuButton.module.css +++ b/src/v4/social/elements/MenuButton/MenuButton.module.css @@ -1,3 +1,14 @@ .menuButton { + display: flex; + justify-content: center; + align-items: center; fill: var(--asc-color-base-default); } + +.menuButton__button { + display: flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.125rem; +} diff --git a/src/v4/social/elements/MenuButton/MenuButton.tsx b/src/v4/social/elements/MenuButton/MenuButton.tsx index e2e8aa71b..904401111 100644 --- a/src/v4/social/elements/MenuButton/MenuButton.tsx +++ b/src/v4/social/elements/MenuButton/MenuButton.tsx @@ -3,19 +3,7 @@ import { useAmityElement } from '~/v4/core/hooks/uikit'; import { IconComponent } from '~/v4/core/IconComponent'; import { Button } from '~/v4/core/natives/Button'; import styles from './MenuButton.module.css'; - -const EllipsisH = ({ ...props }: React.SVGProps) => ( - - - -); +import { EllipsisH } from '~/v4/icons/Ellipsis'; export interface MenuButtonProps { pageId?: string; @@ -35,7 +23,11 @@ export function MenuButton({ pageId = '*', componentId = '*', onClick }: MenuBut if (isExcluded) return null; return ( - ) : null} {showEditPostButton ? ( @@ -191,13 +193,15 @@ export const PostMenu = ({ }} > - Edit post + Edit post ) : null} {showDeletePostButton ? ( ) : null}
    diff --git a/src/v4/social/internal-components/StoryAd/UIStoryAd.tsx b/src/v4/social/internal-components/StoryAd/UIStoryAd.tsx index 86b194024..ed841bf02 100644 --- a/src/v4/social/internal-components/StoryAd/UIStoryAd.tsx +++ b/src/v4/social/internal-components/StoryAd/UIStoryAd.tsx @@ -143,7 +143,6 @@ export const UIStoryAd = ({