diff --git a/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.module.css b/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.module.css
index 5e5e68d50..de86cacdf 100644
--- a/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.module.css
+++ b/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.module.css
@@ -1,5 +1,5 @@
.hyperlinkFormContainer {
- border-radius: var(--asc-border-radius-md);
+ background-color: transparent;
}
.form {
@@ -8,6 +8,44 @@
gap: var(--asc-spacing-l1);
}
+.bottomSheet {
+}
+
+.bottomSheet .react-modal-sheet-backdrop {
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.bottomSheet .react-modal-sheet-container {
+ background-color: white;
+ border-top-left-radius: 16px;
+ border-top-right-radius: 16px;
+ max-height: 80%;
+ overflow: auto;
+}
+
+.bottomSheet .react-modal-sheet-content {
+ background-color: var(--asc-color-base-background);
+}
+
+.bottomSheet .react-modal-sheet-header {
+ padding: 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.bottomSheet .react-modal-sheet-drag-indicator {
+ width: 40px;
+ height: 4px;
+ background-color: #ccc;
+ border-radius: 2px;
+ margin-bottom: 8px;
+}
+
+.bottomSheet .react-modal-sheet-content {
+ padding: 16px;
+}
+
.inputContainer {
display: flex;
flex-direction: column;
@@ -73,10 +111,18 @@
color: var(--asc-color-base-default);
}
-.styledSecondaryButton {
+.cancelButton {
border: none;
color: var(--asc-color-base-default);
- background: var(--asc-color-base-background);
+}
+
+.doneButton {
+ border: none;
+}
+
+.doneButton:disabled {
+ cursor: not-allowed;
+ color: var(--asc-color-primary-shade2);
}
.removeIcon {
diff --git a/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.tsx b/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.tsx
index 26b7f13d2..e806c0d35 100644
--- a/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.tsx
+++ b/src/v4/social/components/HyperLinkConfig/HyperLinkConfig.tsx
@@ -14,7 +14,7 @@ import { useConfirmContext } from '~/v4/core/providers/ConfirmProvider';
import { Button } from '~/v4/core/components/Button';
interface HyperLinkConfigProps {
- pageId: '*';
+ pageId: string;
isHaveHyperLink: boolean;
isOpen: boolean;
onClose: () => void;
@@ -33,10 +33,12 @@ export const HyperLinkConfig = ({
onRemove,
}: HyperLinkConfigProps) => {
const { confirm } = useConfirmContext();
+
const componentId = 'hyper_link_config_component';
- const { getConfig } = useCustomization();
+ const { getConfig, isExcluded } = useCustomization();
+
const componentConfig = getConfig(`${pageId}/${componentId}/*`);
- const componentTheme = componentConfig?.theme.light || {};
+ const componentTheme = componentConfig?.theme?.light || {};
const cancelButtonConfig = getConfig(`*/hyper_link_config_component/cancel_button`);
const doneButtonConfig = getConfig(`*/hyper_link_config_component/done_button`);
@@ -45,11 +47,43 @@ export const HyperLinkConfig = ({
const { client } = useSDK();
const schema = z.object({
- url: z.string().refine(async (value) => {
- if (!value) return true;
- const hasWhitelistedUrls = await client?.validateUrls([value]).catch(() => false);
- return hasWhitelistedUrls;
- }, formatMessage({ id: 'storyCreation.hyperlink.validation.error.whitelisted' })),
+ url: z
+ .string()
+ .refine(
+ (value) => {
+ if (!value) return true;
+ try {
+ const urlObj = new URL(value);
+ return ['http:', 'https:'].includes(urlObj.protocol);
+ } catch (error) {
+ // Check if the value starts with "www."
+ if (value.startsWith('www.')) {
+ try {
+ const urlObj = new URL(`https://${value}`);
+ return ['http:', 'https:'].includes(urlObj.protocol);
+ } catch (error) {
+ return false;
+ }
+ }
+ return false;
+ }
+ },
+ {
+ message: formatMessage({ id: 'storyCreation.hyperlink.validation.error.invalidUrl' }),
+ },
+ )
+ .refine(
+ async (value) => {
+ if (!value) return true;
+ // Prepend "https://" to the value if it starts with "www."
+ const urlToValidate = value.startsWith('www.') ? `https://${value}` : value;
+ const hasWhitelistedUrls = await client?.validateUrls([urlToValidate]).catch(() => false);
+ return hasWhitelistedUrls;
+ },
+ {
+ message: formatMessage({ id: 'storyCreation.hyperlink.validation.error.whitelisted' }),
+ },
+ ),
customText: z
.string()
.optional()
@@ -63,10 +97,12 @@ export const HyperLinkConfig = ({
type HyperLinkFormInputs = z.infer
;
const {
+ trigger,
watch,
register,
handleSubmit,
formState: { errors },
+ reset,
} = useForm({
resolver: zodResolver(schema),
});
@@ -77,6 +113,7 @@ export const HyperLinkConfig = ({
};
const confirmDiscardHyperlink = () => {
+ reset();
onRemove();
onClose();
};
@@ -98,31 +135,34 @@ export const HyperLinkConfig = ({
rootId="asc-uikit-create-story"
isOpen={isOpen}
onClose={onClose}
+ className={styles.bottomSheet}
>
diff --git a/src/v4/social/elements/ActionButton/ActionButton.module.css b/src/v4/social/elements/ActionButton/ActionButton.module.css
new file mode 100644
index 000000000..5cca3c61b
--- /dev/null
+++ b/src/v4/social/elements/ActionButton/ActionButton.module.css
@@ -0,0 +1,15 @@
+.actionButton {
+ width: 2rem;
+ height: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: var(--base-inverse-default);
+ cursor: pointer;
+ padding: 0.1875rem 0rem;
+ border-radius: 50%;
+ transition: background-color 0.3s;
+ flex-shrink: 0;
+}
diff --git a/src/v4/social/elements/ActionButton/ActionButton.tsx b/src/v4/social/elements/ActionButton/ActionButton.tsx
index 12b2446ff..f32980862 100644
--- a/src/v4/social/elements/ActionButton/ActionButton.tsx
+++ b/src/v4/social/elements/ActionButton/ActionButton.tsx
@@ -1,26 +1,15 @@
import React from 'react';
-import styled from 'styled-components';
+import clsx from 'clsx';
+import styles from './ActionButton.module.css';
interface ActionButtonProps extends React.ButtonHTMLAttributes {
icon: React.ReactNode;
}
-const StyledActionButton = styled.button`
- width: 2rem;
- height: 2rem;
- display: flex;
- align-items: center;
- justify-content: center;
- border: none;
- background-color: ${({ theme }) => theme.v4.colors.actionButton.default};
- color: ${({ theme }) => theme.v4.colors.baseInverse.default};
- cursor: pointer;
- padding: 0.1875rem 0rem;
- border-radius: 50%;
- transition: background-color 0.3s;
- flex-shrink: 0;
-`;
-
-export const ActionButton: React.FC = ({ icon, ...rest }) => {
- return {icon};
+export const ActionButton: React.FC = ({ icon, className, ...rest }) => {
+ return (
+
+ );
};
diff --git a/src/v4/social/elements/HyperLink/HyperLink.module.css b/src/v4/social/elements/HyperLink/HyperLink.module.css
index e16c56d0e..0fca5ea50 100644
--- a/src/v4/social/elements/HyperLink/HyperLink.module.css
+++ b/src/v4/social/elements/HyperLink/HyperLink.module.css
@@ -15,6 +15,7 @@
max-width: 200px;
border: var(--asc-color-base-shade4);
text-decoration: none;
+ cursor: pointer;
}
.hyperlinkIcon {
@@ -29,7 +30,7 @@
align-items: center;
max-width: calc(100% - 2rem);
overflow: hidden;
- color: var(--asc-color-secondary-shade4);
+ color: var(--asc-color-secondary-default);
}
.text {
diff --git a/src/v4/social/elements/HyperLink/HyperLink.tsx b/src/v4/social/elements/HyperLink/HyperLink.tsx
index e4dff3e56..f3252c1b0 100644
--- a/src/v4/social/elements/HyperLink/HyperLink.tsx
+++ b/src/v4/social/elements/HyperLink/HyperLink.tsx
@@ -3,7 +3,7 @@ import { LinkIcon } from '~/v4/social/icons';
import styles from './HyperLink.module.css';
interface LinkButtonProps extends React.AnchorHTMLAttributes {
- href: string;
+ href?: string;
}
export const HyperLink: React.FC = ({ href, children, ...rest }) => {
diff --git a/src/v4/social/elements/HyperLinkButton/HyperLinkButton.tsx b/src/v4/social/elements/HyperLinkButton/HyperLinkButton.tsx
index 51d784007..c7b005af1 100644
--- a/src/v4/social/elements/HyperLinkButton/HyperLinkButton.tsx
+++ b/src/v4/social/elements/HyperLinkButton/HyperLinkButton.tsx
@@ -1,50 +1,34 @@
import React from 'react';
-import { useTheme } from 'styled-components';
import { ActionButton } from '../ActionButton';
import { LinkIcon } from '~/icons';
-import { useCustomization } from '~/v4/core/providers/CustomizationProvider';
+import { useAmityElement } from '~/v4/core/hooks/uikit/index';
-interface BackButtonProps {
- pageId: 'create_story_page';
- componentId: '*';
+interface HyperLinkButtonProps {
+ pageId?: string;
+ componentId?: string;
onClick: (e: React.MouseEvent) => void;
}
export const HyperLinkButton = ({
- pageId = 'create_story_page',
+ pageId = '*',
componentId = '*',
onClick = () => {},
-}: BackButtonProps) => {
- const theme = useTheme();
+}: HyperLinkButtonProps) => {
const elementId = 'story_hyperlink_button';
- const { getConfig, isExcluded } = useCustomization();
- const elementConfig = getConfig(`${pageId}/${componentId}/${elementId}`);
- const backgroundColor = elementConfig?.background_color;
- const customIcon = elementConfig?.hyperlink_button_icon;
- const isElementExcluded = isExcluded(`${pageId}/${componentId}/${elementId}`);
+ const { accessibilityId, config, defaultConfig, isExcluded, uiReference, themeStyles } =
+ useAmityElement({
+ pageId,
+ componentId,
+ elementId,
+ });
- if (isElementExcluded) return null;
-
- const renderIcon = () => {
- if (customIcon) {
- if (customIcon.startsWith('http://') || customIcon.startsWith('https://')) {
- return (
-
- );
- }
- }
-
- return ;
- };
+ if (isExcluded) return null;
return (
}
onClick={onClick}
- style={{
- backgroundColor: backgroundColor || theme.v4.colors.actionButton.default,
- }}
/>
);
};
diff --git a/src/v4/social/elements/ShareStoryButton/ShareStoryButton.tsx b/src/v4/social/elements/ShareStoryButton/ShareStoryButton.tsx
index 3b66ac7b8..297d0dacc 100644
--- a/src/v4/social/elements/ShareStoryButton/ShareStoryButton.tsx
+++ b/src/v4/social/elements/ShareStoryButton/ShareStoryButton.tsx
@@ -4,7 +4,7 @@ import { useIntl } from 'react-intl';
import { isValidHttpUrl } from '~/utils';
import { useCustomization } from '~/v4/core/providers/CustomizationProvider';
import { Icon } from '~/v4/core/components/Icon';
-import { backgroundImage as communityBackgroundImage } from '~/icons/Community';
+import { backgroundImage as communityBackgroundImage } from '~/v4/icons/Community';
import styles from './ShareStoryButton.module.css';
import { Avatar } from '~/v4/core/components';
diff --git a/src/v4/social/hooks/collections/useCommentsCollection.ts b/src/v4/social/hooks/collections/useCommentsCollection.ts
new file mode 100644
index 000000000..b642f9be5
--- /dev/null
+++ b/src/v4/social/hooks/collections/useCommentsCollection.ts
@@ -0,0 +1,38 @@
+import { CommentRepository } from '@amityco/ts-sdk';
+
+import useLiveCollection from '~/core/hooks/useLiveCollection';
+
+type useCommentsParams = {
+ parentId?: string | null;
+ referenceId?: string | null;
+ referenceType: Amity.CommentReferenceType;
+ limit?: number;
+ shouldCall?: () => boolean;
+ includeDeleted?: boolean;
+};
+
+export default function useCommentsCollection({
+ parentId,
+ referenceId,
+ referenceType,
+ limit = 10,
+ shouldCall = () => true,
+ includeDeleted = false,
+}: useCommentsParams) {
+ const { items, ...rest } = useLiveCollection({
+ fetcher: CommentRepository.getComments,
+ params: {
+ parentId,
+ referenceId: referenceId as string,
+ referenceType,
+ limit,
+ includeDeleted,
+ },
+ shouldCall: () => shouldCall() && !!referenceId && !!referenceType,
+ });
+
+ return {
+ comments: items,
+ ...rest,
+ };
+}
diff --git a/src/v4/social/hooks/collections/useCommunityMembersCollection.tsx b/src/v4/social/hooks/collections/useCommunityMembersCollection.tsx
new file mode 100644
index 000000000..be5668978
--- /dev/null
+++ b/src/v4/social/hooks/collections/useCommunityMembersCollection.tsx
@@ -0,0 +1,15 @@
+import { CommunityRepository } from '@amityco/ts-sdk';
+import useLiveCollection from '~/core/hooks/useLiveCollection';
+
+export default function useCommunityMembersCollection(communityId?: string, limit: number = 5) {
+ const { items, ...rest } = useLiveCollection({
+ fetcher: CommunityRepository.Membership.getMembers,
+ params: { communityId: communityId as string, limit, memberships: ['member'] },
+ shouldCall: () => !!communityId,
+ });
+
+ return {
+ members: items,
+ ...rest,
+ };
+}
diff --git a/src/v4/social/hooks/index.ts b/src/v4/social/hooks/index.ts
index 38695fed4..e19849fde 100644
--- a/src/v4/social/hooks/index.ts
+++ b/src/v4/social/hooks/index.ts
@@ -1,2 +1,3 @@
export * from './collections/useReactionsCollection';
export { useGetActiveStoriesByTarget } from './useGetActiveStories';
+export { useCommentFlaggedByMe } from './useCommentFlaggedByMe';
diff --git a/src/v4/social/hooks/useCommentFlaggedByMe.tsx b/src/v4/social/hooks/useCommentFlaggedByMe.tsx
new file mode 100644
index 000000000..f318ebd33
--- /dev/null
+++ b/src/v4/social/hooks/useCommentFlaggedByMe.tsx
@@ -0,0 +1,71 @@
+import { CommentRepository } from '@amityco/ts-sdk';
+import { useQuery } from '@tanstack/react-query';
+import React, { useEffect, useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { useNotifications } from '~/v4/core/providers/NotificationProvider';
+
+export const useCommentFlaggedByMe = (commentId?: string) => {
+ const notification = useNotifications();
+ const [isFlaggedByMe, setIsFlaggedByMe] = useState(false);
+
+ const { data, isLoading, refetch } = useQuery({
+ queryKey: ['asc-uikit', 'CommentRepository', 'isCommentFlaggedByMe', commentId],
+ queryFn: () => {
+ return CommentRepository.isCommentFlaggedByMe(commentId as string);
+ },
+ enabled: commentId != null,
+ });
+
+ useEffect(() => {
+ if (data != null) {
+ setIsFlaggedByMe(data);
+ }
+ }, [data]);
+
+ const flagComment = async () => {
+ if (commentId == null) return;
+ try {
+ setIsFlaggedByMe(true);
+ await CommentRepository.flagComment(commentId);
+ } catch (_error) {
+ setIsFlaggedByMe(false);
+ } finally {
+ refetch();
+ }
+ };
+
+ const unflagComment = async () => {
+ if (commentId == null) return;
+ try {
+ setIsFlaggedByMe(false);
+ await CommentRepository.unflagComment(commentId);
+ } catch (_error) {
+ setIsFlaggedByMe(true);
+ } finally {
+ refetch();
+ }
+ };
+
+ const toggleFlagComment = async () => {
+ if (commentId == null) return;
+ if (isFlaggedByMe) {
+ await unflagComment();
+ notification.success({
+ content: ,
+ });
+ } else {
+ await flagComment();
+ notification.success({
+ content: ,
+ });
+ }
+ };
+
+ return {
+ isLoading,
+ isFlaggedByMe,
+ flagComment,
+ unflagComment,
+ toggleFlagComment,
+ };
+};
diff --git a/src/v4/social/hooks/useGetStoryByStoryId.tsx b/src/v4/social/hooks/useGetStoryByStoryId.tsx
new file mode 100644
index 000000000..403a6b911
--- /dev/null
+++ b/src/v4/social/hooks/useGetStoryByStoryId.tsx
@@ -0,0 +1,15 @@
+import { StoryRepository } from '@amityco/ts-sdk';
+
+import useLiveObject from '~/core/hooks/useLiveObject';
+
+const useGetStoryByStoryId = (storyId: string | undefined) => {
+ const story = useLiveObject({
+ fetcher: StoryRepository.getStoryByStoryId,
+ params: storyId,
+ shouldCall: () => !!storyId,
+ });
+
+ return story;
+};
+
+export default useGetStoryByStoryId;
diff --git a/src/v4/social/icons/index.ts b/src/v4/social/icons/index.ts
index ee745d9a9..bbc1d791b 100644
--- a/src/v4/social/icons/index.ts
+++ b/src/v4/social/icons/index.ts
@@ -6,3 +6,4 @@ export { default as TrashIcon } from './trash';
export { default as FlagIcon } from './flag';
export { default as ChevronDownIcon } from './chevron_down';
export { default as LinkIcon } from './link';
+export { default as MinusCircleIcon } from './minus_circle';
diff --git a/src/v4/social/icons/minus_circle.tsx b/src/v4/social/icons/minus_circle.tsx
new file mode 100644
index 000000000..852e2252d
--- /dev/null
+++ b/src/v4/social/icons/minus_circle.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+function Icon(props: React.SVGProps) {
+ return (
+
+ );
+}
+
+export default Icon;
diff --git a/src/v4/social/internal-components/Comment/Comment.module.css b/src/v4/social/internal-components/Comment/Comment.module.css
index 327387304..e7bf63915 100644
--- a/src/v4/social/internal-components/Comment/Comment.module.css
+++ b/src/v4/social/internal-components/Comment/Comment.module.css
@@ -19,3 +19,21 @@
.replyContainer {
margin-left: 3.25rem;
}
+
+.deletedCommentBlock {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ gap: var(--asc-spacing-s2);
+ padding: var(--asc-spacing-s2) var(--asc-spacing-m1);
+ border-radius: var(--asc-border-radius-sm);
+ text-align: center;
+ color: var(--asc-color-base-shade2);
+ background-color: var(--asc-color-base-background);
+ border-top: 1px solid var(--asc-color-base-shade4);
+}
+
+.deletedCommentBlock:first-child {
+ padding: 0 var(--asc-spacing-m1) var(--asc-spacing-s2) var(--asc-spacing-m1);
+ border-top: none;
+}
diff --git a/src/v4/social/internal-components/Comment/UIComment.module.css b/src/v4/social/internal-components/Comment/UIComment.module.css
index 879e00e65..fe9e36cd3 100644
--- a/src/v4/social/internal-components/Comment/UIComment.module.css
+++ b/src/v4/social/internal-components/Comment/UIComment.module.css
@@ -127,6 +127,7 @@
.reactionListButton {
border: none;
color: var(--asc-color-primary-default);
+ padding: 0;
}
.content {
@@ -143,3 +144,11 @@
gap: 0.125rem;
color: var(--asc-color-base-default);
}
+
+.deletedCommentBlock {
+ padding: var(--asc-spacing-m1);
+ background-color: var(--asc-color-base-background);
+ border-radius: var(--asc-border-radius-sm);
+ text-align: center;
+ color: var(--asc-color-base-shade2);
+}
diff --git a/src/v4/social/internal-components/Comment/UIComment.tsx b/src/v4/social/internal-components/Comment/UIComment.tsx
index 8ded27d94..927ff22d1 100644
--- a/src/v4/social/internal-components/Comment/UIComment.tsx
+++ b/src/v4/social/internal-components/Comment/UIComment.tsx
@@ -59,6 +59,7 @@ interface StyledCommentProps {
}[];
}) => void;
queryMentionees: QueryMentioneesFnType;
+ isMember?: boolean;
isLiked?: boolean;
isReported?: boolean;
isReplyComment?: boolean;
@@ -94,6 +95,7 @@ const UIComment = ({
isEditing,
onChange,
queryMentionees,
+ isMember = false,
isBanned,
isLiked,
mentionees,
@@ -145,8 +147,8 @@ const UIComment = ({
)}
- {!isEditing && (canLike || canReply || options.length > 0) && (
-