+ {/* Typeahead avatar URLs can use hosts outside next/image remotePatterns, so this stays unoptimized. */}
diff --git a/components/drops/create/lexical/transformers/GroupMentionTransformer.ts b/components/drops/create/lexical/transformers/GroupMentionTransformer.ts
new file mode 100644
index 0000000000..d1039e00f2
--- /dev/null
+++ b/components/drops/create/lexical/transformers/GroupMentionTransformer.ts
@@ -0,0 +1,41 @@
+import type { TextMatchTransformer } from "@lexical/markdown";
+
+import { ALL_GROUP_MENTION_TEXT } from "@/helpers/waves/drop-group-mentions";
+import {
+ $createGroupMentionNode,
+ $isGroupMentionNode,
+ GroupMentionNode,
+} from "../nodes/GroupMentionNode";
+
+const GROUP_MENTION_IMPORT_REGEXP = /(^|[^A-Za-z0-9_@])(@all)(?![A-Za-z0-9_@])/;
+const GROUP_MENTION_SHORTCUT_REGEXP =
+ /(^|[^A-Za-z0-9_@])(@all)(?![A-Za-z0-9_@])$/;
+
+export const GROUP_MENTION_TRANSFORMER: TextMatchTransformer = {
+ dependencies: [GroupMentionNode],
+ export: (node) => {
+ if (!$isGroupMentionNode(node)) {
+ return null;
+ }
+
+ return node.getTextContent();
+ },
+ regExp: GROUP_MENTION_SHORTCUT_REGEXP,
+ importRegExp: GROUP_MENTION_IMPORT_REGEXP,
+ replace: (textNode, match) => {
+ const [, prefix = ""] = match;
+
+ const nodeToReplace = prefix.length
+ ? textNode.splitText(prefix.length)[1]
+ : textNode;
+
+ if (!nodeToReplace) {
+ return;
+ }
+
+ const groupMentionNode = $createGroupMentionNode(ALL_GROUP_MENTION_TEXT);
+ nodeToReplace.replace(groupMentionNode);
+ },
+ trigger: "l",
+ type: "text-match",
+};
diff --git a/components/drops/create/lexical/utils/groupMentionDetection.ts b/components/drops/create/lexical/utils/groupMentionDetection.ts
new file mode 100644
index 0000000000..7aa16da206
--- /dev/null
+++ b/components/drops/create/lexical/utils/groupMentionDetection.ts
@@ -0,0 +1,26 @@
+import { $getRoot, type EditorState } from "lexical";
+
+import { $isGroupMentionNode } from "@/components/drops/create/lexical/nodes/GroupMentionNode";
+import { ALL_GROUP_MENTION_TEXT } from "@/helpers/waves/drop-group-mentions";
+import { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
+
+export const getMentionedGroupsFromEditorState = (
+ editorState: EditorState,
+ canMentionAll: boolean
+): ApiDropGroupMention[] => {
+ if (!canMentionAll) {
+ return [];
+ }
+
+ return editorState.read(() => {
+ const hasAllGroupMentionNode = $getRoot()
+ .getAllTextNodes()
+ .some(
+ (node) =>
+ $isGroupMentionNode(node) &&
+ node.getTextContent() === ALL_GROUP_MENTION_TEXT
+ );
+
+ return hasAllGroupMentionNode ? [ApiDropGroupMention.All] : [];
+ });
+};
diff --git a/components/drops/create/utils/storm/CreateDropStormView.tsx b/components/drops/create/utils/storm/CreateDropStormView.tsx
index a283ecb590..2eb13d1d77 100644
--- a/components/drops/create/utils/storm/CreateDropStormView.tsx
+++ b/components/drops/create/utils/storm/CreateDropStormView.tsx
@@ -31,6 +31,7 @@ const CreateDropStormView = memo(
part={part}
referencedNfts={drop.referenced_nfts}
mentionedUsers={drop.mentioned_users}
+ mentionedGroups={drop.mentioned_groups ?? []}
mentionedWaves={drop.mentioned_waves ?? []}
createdAt={now}
partIndex={index}
diff --git a/components/drops/create/utils/storm/CreateDropStormViewPart.tsx b/components/drops/create/utils/storm/CreateDropStormViewPart.tsx
index 6bb7a62e12..9cca1a4230 100644
--- a/components/drops/create/utils/storm/CreateDropStormViewPart.tsx
+++ b/components/drops/create/utils/storm/CreateDropStormViewPart.tsx
@@ -5,6 +5,7 @@ import type {
MentionedWave,
ReferencedNft,
} from "@/entities/IDrop";
+import type { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
import DropPart from "@/components/drops/view/part/DropPart";
import CreateDropStormViewPartQuote from "./CreateDropStormViewPartQuote";
import type { ProfileMinWithoutSubs } from "@/helpers/ProfileTypes";
@@ -19,6 +20,7 @@ interface CreateDropStormViewPartProps {
readonly profile: ProfileMinWithoutSubs;
readonly part: CreateDropPart;
readonly mentionedUsers: Array
>;
+ readonly mentionedGroups: ApiDropGroupMention[];
readonly mentionedWaves: MentionedWave[];
readonly referencedNfts: Array;
readonly createdAt: number;
@@ -33,6 +35,7 @@ const CreateDropStormViewPart = memo(
profile,
part,
mentionedUsers,
+ mentionedGroups,
mentionedWaves,
referencedNfts,
createdAt,
@@ -56,6 +59,7 @@ const CreateDropStormViewPart = memo(
;
+ readonly mentionedGroups: Array;
readonly mentionedWaves: Array;
readonly referencedNfts: Array;
readonly createdAt: number;
@@ -62,6 +64,7 @@ export default function CreateDropStormViewPartQuote({
dropId: drop.id,
part,
mentionedUsers: drop.mentioned_users,
+ mentionedGroups: drop.mentioned_groups,
mentionedWaves: drop.mentioned_waves,
referencedNfts: drop.referenced_nfts,
createdAt: drop.created_at,
@@ -85,6 +88,7 @@ export default function CreateDropStormViewPartQuote({
dropId={partConfig.dropId}
profile={profile}
mentionedUsers={partConfig.mentionedUsers}
+ mentionedGroups={partConfig.mentionedGroups}
mentionedWaves={partConfig.mentionedWaves}
referencedNfts={partConfig.referencedNfts}
smallMenuIsShown={false}
diff --git a/components/drops/view/item/content/DropListItemContentGroupMention.tsx b/components/drops/view/item/content/DropListItemContentGroupMention.tsx
new file mode 100644
index 0000000000..28e16c6c58
--- /dev/null
+++ b/components/drops/view/item/content/DropListItemContentGroupMention.tsx
@@ -0,0 +1,7 @@
+export default function DropListItemContentGroupMention() {
+ return (
+
+ @all
+
+ );
+}
diff --git a/components/drops/view/item/content/DropListItemContentPart.tsx b/components/drops/view/item/content/DropListItemContentPart.tsx
index 2ba0309bf5..391d72a3fb 100644
--- a/components/drops/view/item/content/DropListItemContentPart.tsx
+++ b/components/drops/view/item/content/DropListItemContentPart.tsx
@@ -2,6 +2,8 @@ import type { MentionedUser, ReferencedNft } from "@/entities/IDrop";
import type { ApiMentionedWave } from "@/generated/models/ApiMentionedWave";
import { assertUnreachable } from "@/helpers/AllowlistToolHelpers";
import DropListItemContentNft from "./nft-tag/DropListItemContentNft";
+import type { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
+import DropListItemContentGroupMention from "./DropListItemContentGroupMention";
import DropListItemContentMention from "./DropListItemContentMention";
import DropListItemContentWaveMention from "./DropListItemContentWaveMention";
import { DropContentPartType } from "@/components/drops/view/part/DropPartMarkdown";
@@ -18,6 +20,12 @@ interface DropListItemContentHashtagProps {
readonly match: string;
}
+interface DropListItemContentGroupMentionProps {
+ readonly type: DropContentPartType.GROUP_MENTION;
+ readonly value: ApiDropGroupMention;
+ readonly match: string;
+}
+
interface DropListItemContentWaveMentionProps {
readonly type: DropContentPartType.WAVE_MENTION;
readonly value: ApiMentionedWave;
@@ -26,6 +34,7 @@ interface DropListItemContentWaveMentionProps {
export type DropListItemContentPartProps =
| DropListItemContentMentionProps
+ | DropListItemContentGroupMentionProps
| DropListItemContentHashtagProps
| DropListItemContentWaveMentionProps;
@@ -38,6 +47,8 @@ export default function DropListItemContentPart({
switch (type) {
case DropContentPartType.MENTION:
return ;
+ case DropContentPartType.GROUP_MENTION:
+ return ;
case DropContentPartType.HASHTAG:
return ;
case DropContentPartType.WAVE_MENTION:
diff --git a/components/drops/view/part/DropPart.tsx b/components/drops/view/part/DropPart.tsx
index e8d41d6d02..64b30159c3 100644
--- a/components/drops/view/part/DropPart.tsx
+++ b/components/drops/view/part/DropPart.tsx
@@ -3,6 +3,7 @@
import type { ReactNode } from "react";
import { memo, useRef } from "react";
import type { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser";
+import type { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
import type { ApiMentionedWave } from "@/generated/models/ApiMentionedWave";
import type { ApiDropReferencedNFT } from "@/generated/models/ApiDropReferencedNFT";
import DropPfp from "@/components/drops/create/utils/DropPfp";
@@ -31,6 +32,7 @@ interface DropPartProps {
readonly profile: ProfileMinWithoutSubs;
readonly dropTitle: string | null;
readonly mentionedUsers: Array;
+ readonly mentionedGroups?: Array | undefined;
readonly mentionedWaves: Array;
readonly referencedNfts: Array;
readonly partContent: string | null;
@@ -56,6 +58,7 @@ const DropPart = memo(
dropId,
profile,
mentionedUsers,
+ mentionedGroups = [],
mentionedWaves,
referencedNfts,
partContent,
@@ -221,6 +224,7 @@ const DropPart = memo(
)}
= ({
mentionedUsers,
+ mentionedGroups = [],
mentionedWaves,
referencedNfts,
partContent,
@@ -34,6 +37,7 @@ const DropPartContent: React.FC = ({
;
+ readonly mentionedGroups?: Array | undefined;
readonly mentionedWaves: Array;
readonly referencedNfts: Array;
readonly nftLinks?: readonly ApiDropNftLink[] | undefined;
@@ -266,6 +268,7 @@ export interface DropPartMarkdownProps {
function DropPartMarkdown({
mentionedUsers,
+ mentionedGroups = [],
mentionedWaves,
referencedNfts,
nftLinks,
@@ -350,6 +353,7 @@ function DropPartMarkdown({
createMarkdownContentRenderers({
textSizeClass,
mentionedUsers,
+ mentionedGroups,
mentionedWaves,
referencedNfts,
emojiMap,
@@ -359,6 +363,7 @@ function DropPartMarkdown({
[
textSizeClass,
mentionedUsers,
+ mentionedGroups,
mentionedWaves,
referencedNfts,
emojiMap,
diff --git a/components/drops/view/part/DropPartMarkdownWithPropLogger.tsx b/components/drops/view/part/DropPartMarkdownWithPropLogger.tsx
index 39b5196cd6..3fd5d5569c 100644
--- a/components/drops/view/part/DropPartMarkdownWithPropLogger.tsx
+++ b/components/drops/view/part/DropPartMarkdownWithPropLogger.tsx
@@ -41,6 +41,7 @@ function areEqual(
) {
const propsToCheck: (keyof DropPartMarkdownProps)[] = [
"mentionedUsers",
+ "mentionedGroups",
"mentionedWaves",
"referencedNfts",
"nftLinks",
diff --git a/components/drops/view/part/dropPartMarkdown/content.tsx b/components/drops/view/part/dropPartMarkdown/content.tsx
index 0000cf9bd7..466b2735c0 100644
--- a/components/drops/view/part/dropPartMarkdown/content.tsx
+++ b/components/drops/view/part/dropPartMarkdown/content.tsx
@@ -5,12 +5,20 @@ import type { ExtraProps } from "react-markdown";
import { getRandomObjectId } from "@/helpers/AllowlistToolHelpers";
import type { DropListItemContentPartProps } from "@/components/drops/view/item/content/DropListItemContentPart";
import type { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser";
+import type { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
+import { ApiDropGroupMention as ApiDropGroupMentionValue } from "@/generated/models/ApiDropGroupMention";
import type { ApiMentionedWave } from "@/generated/models/ApiMentionedWave";
import type { ApiDropReferencedNFT } from "@/generated/models/ApiDropReferencedNFT";
import DropListItemContentPart from "@/components/drops/view/item/content/DropListItemContentPart";
+import {
+ ALL_GROUP_MENTION_TEXT,
+ hasMentionedGroup,
+ markAllGroupMentionTokens,
+} from "@/helpers/waves/drop-group-mentions";
export enum DropContentPartType {
MENTION = "MENTION",
+ GROUP_MENTION = "GROUP_MENTION",
HASHTAG = "HASHTAG",
WAVE_MENTION = "WAVE_MENTION",
}
@@ -28,6 +36,7 @@ type FindNativeEmoji = (emojiId: string) => { skins: NativeEmojiSkin[] } | null;
interface MarkdownContentConfig {
readonly textSizeClass: string;
readonly mentionedUsers: Array;
+ readonly mentionedGroups: Array;
readonly mentionedWaves: Array;
readonly referencedNfts: Array;
readonly emojiMap: EmojiCategory[];
@@ -50,6 +59,7 @@ const emojiRegex = /(:\w+:)/g;
export const createMarkdownContentRenderers = ({
textSizeClass,
mentionedUsers,
+ mentionedGroups,
mentionedWaves,
referencedNfts,
emojiMap,
@@ -102,6 +112,15 @@ export const createMarkdownContentRenderers = ({
}),
{}
),
+ ...(hasMentionedGroup(mentionedGroups, ApiDropGroupMentionValue.All)
+ ? {
+ [ALL_GROUP_MENTION_TEXT]: {
+ type: DropContentPartType.GROUP_MENTION,
+ value: ApiDropGroupMentionValue.All,
+ match: ALL_GROUP_MENTION_TEXT,
+ },
+ }
+ : {}),
...mentionedWaves.reduce(
(acc, wave) => ({
...acc,
@@ -140,12 +159,22 @@ export const createMarkdownContentRenderers = ({
let currentContent = content;
for (const token of Object.values(values)) {
+ if (token.type === DropContentPartType.GROUP_MENTION) {
+ continue;
+ }
currentContent = currentContent.replaceAll(
token.match,
`${splitter}${token.match}${splitter}`
);
}
+ if (hasMentionedGroup(mentionedGroups, ApiDropGroupMentionValue.All)) {
+ currentContent = markAllGroupMentionTokens({
+ content: currentContent,
+ marker: splitter,
+ });
+ }
+
const parts = currentContent
.split(splitter)
.filter((part) => part !== "")
diff --git a/components/waves/CreateDrop.tsx b/components/waves/CreateDrop.tsx
index 54a9af019c..d6743a7bbe 100644
--- a/components/waves/CreateDrop.tsx
+++ b/components/waves/CreateDrop.tsx
@@ -26,6 +26,7 @@ import {
resolveWaveSubmissionExperience,
WaveSubmissionExperience,
} from "@/helpers/waves/wave-submission-experience.helpers";
+import { getMentionedGroupsFromParts } from "@/helpers/waves/drop-group-mentions";
interface CreateDropProps {
readonly activeDrop: ActiveDropState | null;
@@ -113,6 +114,8 @@ export default function CreateDrop({
const isCurationDropMode =
submissionExperience === WaveSubmissionExperience.CURATION_LEGACY &&
isDropMode;
+ const canMentionAll =
+ wave.wave.authenticated_user_eligible_for_admin === true;
const canSwitchDropMode = useCallback(
(newIsDropMode: boolean) => {
@@ -173,20 +176,27 @@ export default function CreateDrop({
[canSwitchDropMode, modeScopeToken]
);
- const onRemovePart = useCallback((partIndex: number) => {
- setDrop((prevDrop) => {
- if (!prevDrop) return null;
- const newParts = prevDrop.parts.filter((_, i) => i !== partIndex);
- return {
- ...prevDrop,
- parts: newParts,
- referenced_nfts: prevDrop.referenced_nfts,
- mentioned_users: prevDrop.mentioned_users,
- mentioned_waves: prevDrop.mentioned_waves ?? [],
- metadata: prevDrop.metadata,
- };
- });
- }, []);
+ const onRemovePart = useCallback(
+ (partIndex: number) => {
+ setDrop((prevDrop) => {
+ if (!prevDrop) return null;
+ const newParts = prevDrop.parts.filter((_, i) => i !== partIndex);
+ return {
+ ...prevDrop,
+ parts: newParts,
+ referenced_nfts: prevDrop.referenced_nfts,
+ mentioned_users: prevDrop.mentioned_users,
+ mentioned_groups: getMentionedGroupsFromParts(
+ newParts,
+ canMentionAll
+ ),
+ mentioned_waves: prevDrop.mentioned_waves ?? [],
+ metadata: prevDrop.metadata,
+ };
+ });
+ },
+ [canMentionAll]
+ );
const addDropMutation = useMutation({
mutationFn: async (body: DropMutationBody) => {
@@ -344,6 +354,7 @@ export default function CreateDrop({
generateMediaForPart(media, setUploadingFiles))
);
return {
- ...part,
+ content: part.content,
+ quoted_drop: part.quoted_drop,
media,
};
};
@@ -545,6 +550,7 @@ const CreateDropContent: React.FC = ({
const hasMetadataValidationErrors = Object.keys(metadataErrorById).length > 0;
const hasMetadata = useMemo(() => hasMetadataContent(metadata), [metadata]);
+ const canMentionAll = wave.wave.authenticated_user_eligible_for_admin;
const getMarkdown = useMemo(
() =>
@@ -552,13 +558,21 @@ const CreateDropContent: React.FC = ({
? exportDropMarkdown(editorState, [
...SAFE_MARKDOWN_TRANSFORMERS,
MENTION_TRANSFORMER,
+ ...(canMentionAll ? [GROUP_MENTION_TRANSFORMER] : []),
HASHTAG_TRANSFORMER,
WAVE_MENTION_TRANSFORMER,
IMAGE_TRANSFORMER,
EMOJI_TRANSFORMER,
])
: null,
- [editorState]
+ [canMentionAll, editorState]
+ );
+ const currentPartMentionedGroups = useMemo(
+ () =>
+ editorState
+ ? getMentionedGroupsFromEditorState(editorState, canMentionAll)
+ : [],
+ [canMentionAll, editorState]
);
const isStormModeActive = isStormMode;
@@ -729,6 +743,7 @@ const CreateDropContent: React.FC = ({
...replyToObj,
parts: ensurePartsWithFallback(baseParts, hasMetadata),
mentioned_users: drop?.mentioned_users ?? [],
+ mentioned_groups: getMentionedGroupsFromParts(baseParts, canMentionAll),
mentioned_waves: drop?.mentioned_waves ?? [],
referenced_nfts: drop?.referenced_nfts ?? [],
metadata: getSubmissionMetadata(),
@@ -745,25 +760,29 @@ const CreateDropContent: React.FC = ({
const replyToObj = replyTo ? { reply_to: replyTo } : {};
const createGifDrop = (gif: string): CreateDropConfig => {
+ const parts: CreateDropPart[] = [
+ ...(drop?.parts ?? []),
+ {
+ content: gif,
+ quoted_drop:
+ activeDrop?.action === ActiveDropAction.QUOTE
+ ? {
+ drop_id: activeDrop.drop.id,
+ drop_part_id: activeDrop.partId,
+ }
+ : null,
+ media: files,
+ mentioned_groups: [],
+ },
+ ];
+
return {
title: null,
drop_type: isDropMode ? ApiDropType.Participatory : ApiDropType.Chat,
...replyToObj,
- parts: [
- ...(drop?.parts ?? []),
- {
- content: gif,
- quoted_drop:
- activeDrop?.action === ActiveDropAction.QUOTE
- ? {
- drop_id: activeDrop.drop.id,
- drop_part_id: activeDrop.partId,
- }
- : null,
- media: files,
- },
- ],
+ parts,
mentioned_users: [],
+ mentioned_groups: getMentionedGroupsFromParts(parts, canMentionAll),
mentioned_waves: [],
referenced_nfts: [],
metadata: getSubmissionMetadata(),
@@ -777,7 +796,8 @@ const CreateDropContent: React.FC = ({
markdown: string | null,
allMentions: ApiDropMentionedUser[],
allNfts: ReferencedNft[],
- allWaves: ApiMentionedWave[]
+ allWaves: ApiMentionedWave[],
+ currentMentionedGroups: ApiDropGroupMention[]
): CreateDropConfig => {
const availableFiles = files;
const hasPartsInDrop = (drop?.parts.length ?? 0) > 0;
@@ -802,6 +822,7 @@ const CreateDropContent: React.FC = ({
content: markdown?.length ? markdown : null,
quoted_drop: quotedDrop,
media: availableFiles,
+ mentioned_groups: currentMentionedGroups,
},
];
@@ -814,6 +835,7 @@ const CreateDropContent: React.FC = ({
...replyToObj,
parts,
mentioned_users: allMentions,
+ mentioned_groups: getMentionedGroupsFromParts(parts, canMentionAll),
mentioned_waves: allWaves,
referenced_nfts: allNfts,
metadata: getSubmissionMetadata(),
@@ -848,7 +870,8 @@ const CreateDropContent: React.FC = ({
updatedMarkdown,
updatedMentions,
updatedNfts,
- updatedWaves
+ updatedWaves,
+ currentPartMentionedGroups
);
};
@@ -890,6 +913,13 @@ const CreateDropContent: React.FC = ({
)
);
+ const filterMentionedGroups = ({
+ parts,
+ }: {
+ readonly parts: CreateDropPart[];
+ }): ApiDropGroupMention[] =>
+ getMentionedGroupsFromParts(parts, canMentionAll);
+
const [dropEditorRefreshKey, setDropEditorRefreshKey] = useState(0);
const refreshState = () => {
@@ -1001,6 +1031,9 @@ const CreateDropContent: React.FC = ({
mentionedWaves: dropRequest.mentioned_waves ?? [],
parts: dropRequest.parts,
}),
+ mentioned_groups: filterMentionedGroups({
+ parts: dropRequest.parts,
+ }),
metadata: dropRequest.metadata,
wave_id: wave.id,
parts,
@@ -1602,6 +1635,7 @@ const CreateDropContent: React.FC = ({
submitting={submitting}
isStormMode={isStormModeActive}
isDropMode={isDropMode}
+ canMentionAll={canMentionAll}
canSubmit={canSubmit}
onEditorState={handleEditorStateChange}
onEditorBlur={handleEditorBlur}
diff --git a/components/waves/CreateDropInput.tsx b/components/waves/CreateDropInput.tsx
index f9c6f541aa..608fa6a677 100644
--- a/components/waves/CreateDropInput.tsx
+++ b/components/waves/CreateDropInput.tsx
@@ -36,6 +36,7 @@ import type {
} from "@/entities/IDrop";
import { ActiveDropAction } from "@/types/dropInteractionTypes";
import { MentionNode } from "../drops/create/lexical/nodes/MentionNode";
+import { GroupMentionNode } from "../drops/create/lexical/nodes/GroupMentionNode";
import { HashtagNode } from "../drops/create/lexical/nodes/HashtagNode";
import { WaveMentionNode } from "../drops/create/lexical/nodes/WaveMentionNode";
import { ImageNode } from "../drops/create/lexical/nodes/ImageNode";
@@ -98,6 +99,7 @@ const CreateDropInput = forwardRef<
readonly isStormMode: boolean;
readonly submitting: boolean;
readonly isDropMode: boolean;
+ readonly canMentionAll?: boolean | undefined;
readonly onDrop?: (() => void) | undefined;
readonly onEditorState: (editorState: EditorState) => void;
readonly onEditorBlur?: (event: FocusEvent) => void;
@@ -116,6 +118,7 @@ const CreateDropInput = forwardRef<
canSubmit,
isStormMode,
isDropMode,
+ canMentionAll = false,
submitting,
onEditorState,
onEditorBlur,
@@ -131,6 +134,7 @@ const CreateDropInput = forwardRef<
namespace: "User Drop",
nodes: [
MentionNode,
+ GroupMentionNode,
HashtagNode,
WaveMentionNode,
RootNode,
@@ -295,6 +299,7 @@ const CreateDropInput = forwardRef<
void;
@@ -17,6 +19,7 @@ const CreateDropStormPart: React.FC = ({
partIndex,
part,
mentionedUsers,
+ mentionedGroups,
mentionedWaves,
referencedNfts,
onRemovePart,
@@ -29,6 +32,7 @@ const CreateDropStormPart: React.FC = ({
void;
@@ -24,6 +26,7 @@ interface CreateDropStormPartsProps {
const CreateDropStormParts: FC = ({
parts,
mentionedUsers,
+ mentionedGroups,
mentionedWaves,
referencedNfts,
onRemovePart,
@@ -115,6 +118,7 @@ const CreateDropStormParts: FC = ({
partIndex={partIndex}
part={part}
mentionedUsers={mentionedUsers}
+ mentionedGroups={mentionedGroups}
mentionedWaves={mentionedWaves}
referencedNfts={referencedNfts}
onRemovePart={onRemovePart}
diff --git a/components/waves/drops/EditDropLexical.tsx b/components/waves/drops/EditDropLexical.tsx
index 0e41d30cf3..6af69f5be6 100644
--- a/components/waves/drops/EditDropLexical.tsx
+++ b/components/waves/drops/EditDropLexical.tsx
@@ -1,6 +1,9 @@
"use client";
-import { $convertFromMarkdownString } from "@lexical/markdown";
+import {
+ $convertFromMarkdownString,
+ type Transformer,
+} from "@lexical/markdown";
import type { InitialConfigType } from "@lexical/react/LexicalComposer";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
@@ -50,6 +53,7 @@ import {
$createMentionNode,
MentionNode,
} from "@/components/drops/create/lexical/nodes/MentionNode";
+import { GroupMentionNode } from "@/components/drops/create/lexical/nodes/GroupMentionNode";
import EmojiPlugin from "@/components/drops/create/lexical/plugins/emoji/EmojiPlugin";
import type { NewMentionsPluginHandles } from "@/components/drops/create/lexical/plugins/mentions/MentionsPlugin";
import NewMentionsPlugin from "@/components/drops/create/lexical/plugins/mentions/MentionsPlugin";
@@ -59,8 +63,11 @@ import PlainTextPastePlugin from "@/components/drops/create/lexical/plugins/Plai
import { HASHTAG_TRANSFORMER } from "@/components/drops/create/lexical/transformers/HastagTransformer";
import { SAFE_MARKDOWN_TRANSFORMERS_WITHOUT_CODE } from "@/components/drops/create/lexical/transformers/markdownTransformers";
import { MENTION_TRANSFORMER } from "@/components/drops/create/lexical/transformers/MentionTransformer";
+import { GROUP_MENTION_TRANSFORMER } from "@/components/drops/create/lexical/transformers/GroupMentionTransformer";
+import { getMentionedGroupsFromEditorState } from "@/components/drops/create/lexical/utils/groupMentionDetection";
import { WAVE_MENTION_TRANSFORMER } from "@/components/drops/create/lexical/transformers/WaveMentionTransformer";
import type { MentionedUser, MentionedWave } from "@/entities/IDrop";
+import { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
import type { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser";
import type { ApiMentionedWave } from "@/generated/models/ApiMentionedWave";
import useDeviceInfo from "@/hooks/useDeviceInfo";
@@ -73,16 +80,20 @@ import {
exportDropMarkdown,
normalizeDropMarkdown,
} from "./normalizeDropMarkdown";
+import { areMentionedGroupsEqual } from "@/helpers/waves/drop-group-mentions";
interface EditDropLexicalProps {
readonly initialContent: string;
readonly initialMentions: ApiDropMentionedUser[];
+ readonly initialGroupMentions: ApiDropGroupMention[];
readonly initialWaveMentions: ApiMentionedWave[];
+ readonly canMentionAll: boolean;
readonly waveId: string | null;
readonly isSaving: boolean;
readonly onSave: (
content: string,
mentions: ApiDropMentionedUser[],
+ mentionedGroups: ApiDropGroupMention[],
mentionedWaves: ApiMentionedWave[]
) => void;
readonly onCancel: () => void;
@@ -90,7 +101,7 @@ interface EditDropLexicalProps {
const MAX_MENTION_RECONSTRUCTION_PASSES = 20;
-const EDIT_MARKDOWN_TRANSFORMERS = [
+const BASE_EDIT_MARKDOWN_TRANSFORMERS = [
...SAFE_MARKDOWN_TRANSFORMERS_WITHOUT_CODE,
MENTION_TRANSFORMER,
HASHTAG_TRANSFORMER,
@@ -266,13 +277,19 @@ function processSplitMentions(textNodes: Array): boolean {
return false;
}
-function InitialContentPlugin({ initialContent }: { initialContent: string }) {
+function InitialContentPlugin({
+ initialContent,
+ transformers,
+}: {
+ initialContent: string;
+ transformers: Transformer[];
+}) {
const [editor] = useLexicalComposerContext();
useEffect(() => {
editor.update(() => {
const normalizedContent = normalizeDropMarkdown(initialContent);
- $convertFromMarkdownString(normalizedContent, EDIT_MARKDOWN_TRANSFORMERS);
+ $convertFromMarkdownString(normalizedContent, transformers);
const root = $getRoot();
convertCodeNodesToFences(root);
@@ -306,7 +323,7 @@ function InitialContentPlugin({ initialContent }: { initialContent: string }) {
root.selectEnd();
});
- }, [editor, initialContent]);
+ }, [editor, initialContent, transformers]);
return null;
}
@@ -351,6 +368,9 @@ function KeyboardPlugin({
isSaving,
isMobileOrApp,
initialContent,
+ initialGroupMentions,
+ canResolveAllGroupMention,
+ transformers,
mentionsRef,
waveMentionsRef,
}: {
@@ -359,6 +379,9 @@ function KeyboardPlugin({
isSaving: boolean;
isMobileOrApp: boolean;
initialContent: string;
+ initialGroupMentions: ApiDropGroupMention[];
+ canResolveAllGroupMention: boolean;
+ transformers: Transformer[];
mentionsRef: React.RefObject;
waveMentionsRef: React.RefObject;
}) {
@@ -396,12 +419,21 @@ function KeyboardPlugin({
if (!isSaving) {
const currentMarkdown = exportDropMarkdown(
editor.getEditorState(),
- EDIT_MARKDOWN_TRANSFORMERS
+ transformers
);
const sanitizedCurrentMarkdown =
removeBlankLinePlaceholders(currentMarkdown);
+ const currentMentionedGroups = getMentionedGroupsFromEditorState(
+ editor.getEditorState(),
+ canResolveAllGroupMention
+ );
if (
- sanitizedCurrentMarkdown.trim() === sanitizedInitialContent.trim()
+ sanitizedCurrentMarkdown.trim() ===
+ sanitizedInitialContent.trim() &&
+ areMentionedGroupsEqual(
+ currentMentionedGroups,
+ initialGroupMentions
+ )
) {
onCancel();
} else {
@@ -424,6 +456,9 @@ function KeyboardPlugin({
isSaving,
isMobileOrApp,
initialContent,
+ initialGroupMentions,
+ canResolveAllGroupMention,
+ transformers,
mentionsRef,
waveMentionsRef,
sanitizedInitialContent,
@@ -435,7 +470,9 @@ function KeyboardPlugin({
const EditDropLexical: React.FC = ({
initialContent,
initialMentions,
+ initialGroupMentions,
initialWaveMentions,
+ canMentionAll,
waveId,
isSaving,
onSave,
@@ -459,7 +496,24 @@ const EditDropLexical: React.FC = ({
() => addBlankLinePlaceholders(normalizedInitialContent),
[normalizedInitialContent]
);
-
+ const hasInitialAllGroupMention = initialGroupMentions.includes(
+ ApiDropGroupMention.All
+ );
+ const canResolveAllGroupMention = canMentionAll || hasInitialAllGroupMention;
+ const importMarkdownTransformers = useMemo(
+ () =>
+ hasInitialAllGroupMention
+ ? [...BASE_EDIT_MARKDOWN_TRANSFORMERS, GROUP_MENTION_TRANSFORMER]
+ : BASE_EDIT_MARKDOWN_TRANSFORMERS,
+ [hasInitialAllGroupMention]
+ );
+ const exportMarkdownTransformers = useMemo(
+ () =>
+ canResolveAllGroupMention
+ ? [...BASE_EDIT_MARKDOWN_TRANSFORMERS, GROUP_MENTION_TRANSFORMER]
+ : BASE_EDIT_MARKDOWN_TRANSFORMERS,
+ [canResolveAllGroupMention]
+ );
const initialConfig: InitialConfigType = {
namespace: "EditDropLexical",
theme: ExampleTheme,
@@ -477,6 +531,7 @@ const EditDropLexical: React.FC = ({
AutoLinkNode,
LinkNode,
MentionNode,
+ GroupMentionNode,
HashtagNode,
WaveMentionNode,
EmojiNode,
@@ -527,21 +582,36 @@ const EditDropLexical: React.FC = ({
const markdown = exportDropMarkdown(
editorState,
- EDIT_MARKDOWN_TRANSFORMERS
+ exportMarkdownTransformers
);
const sanitizedMarkdown = removeBlankLinePlaceholders(markdown);
+ const sanitizedMentionedGroups = getMentionedGroupsFromEditorState(
+ editorState,
+ canResolveAllGroupMention
+ );
- if (sanitizedMarkdown.trim() === normalizedInitialContent.trim()) {
+ if (
+ sanitizedMarkdown.trim() === normalizedInitialContent.trim() &&
+ areMentionedGroupsEqual(sanitizedMentionedGroups, initialGroupMentions)
+ ) {
onCancel();
return;
}
- onSave(sanitizedMarkdown, mentionedUsers, mentionedWaves);
+ onSave(
+ sanitizedMarkdown,
+ mentionedUsers,
+ sanitizedMentionedGroups,
+ mentionedWaves
+ );
}, [
editorState,
+ exportMarkdownTransformers,
mentionedUsers,
mentionedWaves,
+ canResolveAllGroupMention,
+ initialGroupMentions,
onSave,
normalizedInitialContent,
onCancel,
@@ -575,7 +645,7 @@ const EditDropLexical: React.FC = ({
@@ -583,13 +653,17 @@ const EditDropLexical: React.FC = ({
ref={mentionsRef}
waveId={waveId}
onSelect={handleMentionSelect}
+ canMentionAll={canMentionAll}
/>
-
+
= ({
isSaving={isSaving}
isMobileOrApp={isMobileOrApp}
initialContent={normalizedInitialContent}
+ initialGroupMentions={initialGroupMentions}
+ canResolveAllGroupMention={canResolveAllGroupMention}
+ transformers={exportMarkdownTransformers}
mentionsRef={mentionsRef}
waveMentionsRef={waveMentionsRef}
/>
diff --git a/components/waves/drops/WaveDrop.tsx b/components/waves/drops/WaveDrop.tsx
index bf8ee02f73..e0ea7ce8d8 100644
--- a/components/waves/drops/WaveDrop.tsx
+++ b/components/waves/drops/WaveDrop.tsx
@@ -575,6 +575,7 @@ const WaveDrop = ({
(
newContent: string,
mentions?: ApiDropMentionedUser[],
+ _mentionedGroups?: unknown,
mentionedWaves?: ApiMentionedWave[]
) => {
// Clean mentioned users to only include allowed fields for API
@@ -591,13 +592,14 @@ const WaveDrop = ({
wave_name_in_content: wave.wave_name_in_content,
})
);
+ const updatedParts = drop.parts.map((part, index) => ({
+ content: index === activePartIndex ? newContent : part.content,
+ quoted_drop: part.quoted_drop ?? null,
+ media: part.media,
+ }));
const updateRequest: ApiUpdateDropRequest = {
- parts: drop.parts.map((part, index) => ({
- content: index === activePartIndex ? newContent : part.content,
- quoted_drop: part.quoted_drop ?? null,
- media: part.media,
- })),
+ parts: updatedParts,
title: drop.title,
metadata: drop.metadata,
referenced_nfts: drop.referenced_nfts,
diff --git a/components/waves/drops/WaveDropContent.tsx b/components/waves/drops/WaveDropContent.tsx
index 38b2b6f7bf..4d7ec47803 100644
--- a/components/waves/drops/WaveDropContent.tsx
+++ b/components/waves/drops/WaveDropContent.tsx
@@ -1,6 +1,7 @@
import React from "react";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import type { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser";
+import type { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
import type { ApiMentionedWave } from "@/generated/models/ApiMentionedWave";
import WaveDropPart from "./WaveDropPart";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
@@ -21,6 +22,7 @@ interface WaveDropContentProps {
| ((
newContent: string,
mentions?: ApiDropMentionedUser[],
+ mentionedGroups?: ApiDropGroupMention[],
mentionedWaves?: ApiMentionedWave[]
) => void)
| undefined;
diff --git a/components/waves/drops/WaveDropPart.tsx b/components/waves/drops/WaveDropPart.tsx
index e094ae3aa5..0b2f1cbe1f 100644
--- a/components/waves/drops/WaveDropPart.tsx
+++ b/components/waves/drops/WaveDropPart.tsx
@@ -3,6 +3,7 @@
import React, { memo, useRef } from "react";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import type { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser";
+import type { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
import type { ApiMentionedWave } from "@/generated/models/ApiMentionedWave";
import WaveDropPartDrop from "./WaveDropPartDrop";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
@@ -22,6 +23,7 @@ interface WaveDropPartProps {
| ((
newContent: string,
mentions?: ApiDropMentionedUser[],
+ mentionedGroups?: ApiDropGroupMention[],
mentionedWaves?: ApiMentionedWave[]
) => void)
| undefined;
diff --git a/components/waves/drops/WaveDropPartContent.tsx b/components/waves/drops/WaveDropPartContent.tsx
index c11e8abfe0..fa48837894 100644
--- a/components/waves/drops/WaveDropPartContent.tsx
+++ b/components/waves/drops/WaveDropPartContent.tsx
@@ -4,6 +4,7 @@ import React, { useMemo } from "react";
import type { ApiDropPart } from "@/generated/models/ApiDropPart";
import WaveDropPartContentMedias from "./WaveDropPartContentMedias";
import type { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser";
+import type { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
import type { ApiMentionedWave } from "@/generated/models/ApiMentionedWave";
import type { ReferencedNft } from "@/entities/IDrop";
import type { ApiWaveMin } from "@/generated/models/ApiWaveMin";
@@ -13,6 +14,7 @@ import { ImageScale } from "@/helpers/image.helpers";
interface WaveDropPartContentProps {
readonly mentionedUsers: ApiDropMentionedUser[];
+ readonly mentionedGroups?: ApiDropGroupMention[] | undefined;
readonly mentionedWaves: ApiMentionedWave[];
readonly referencedNfts: ReferencedNft[];
readonly wave: ApiWaveMin;
@@ -29,6 +31,7 @@ interface WaveDropPartContentProps {
| ((
newContent: string,
mentions?: ApiDropMentionedUser[],
+ mentionedGroups?: ApiDropGroupMention[],
mentionedWaves?: ApiMentionedWave[]
) => void)
| undefined;
@@ -44,6 +47,7 @@ interface WaveDropPartContentProps {
const WaveDropPartContent: React.FC = ({
mentionedUsers,
+ mentionedGroups = [],
mentionedWaves,
referencedNfts,
wave,
@@ -74,6 +78,10 @@ const WaveDropPartContent: React.FC = ({
() => mentionedWaves,
[mentionedWaves]
);
+ const memoizedMentionedGroups = useMemo(
+ () => mentionedGroups,
+ [mentionedGroups]
+ );
const memoizedReferencedNfts = useMemo(
() => referencedNfts,
[referencedNfts]
@@ -142,6 +150,7 @@ const WaveDropPartContent: React.FC = ({
;
+ readonly mentionedGroups?: Array | undefined;
readonly mentionedWaves: Array;
readonly referencedNfts: Array;
readonly part: ApiDropPart;
@@ -23,6 +25,7 @@ interface WaveDropPartContentMarkdownProps {
| ((
newContent: string,
mentions?: ApiDropMentionedUser[],
+ mentionedGroups?: ApiDropGroupMention[],
mentionedWaves?: ApiMentionedWave[]
) => void)
| undefined;
@@ -37,6 +40,7 @@ const WaveDropPartContentMarkdown: React.FC<
WaveDropPartContentMarkdownProps
> = ({
mentionedUsers,
+ mentionedGroups = [],
mentionedWaves,
referencedNfts,
part,
@@ -58,16 +62,19 @@ const WaveDropPartContentMarkdown: React.FC<
{
if (onSave) {
- onSave(content, mentions, waves);
+ onSave(content, mentions, groups, waves);
}
}}
onCancel={() => {
@@ -84,6 +91,7 @@ const WaveDropPartContentMarkdown: React.FC<
void)
| undefined;
@@ -59,6 +61,7 @@ const WaveDropPartDrop: React.FC = ({
= ({
-
+
{!!connectedProfile?.handle && !activeProfileProxy && (
-
diff --git a/components/waves/header/WaveHeaderFollow.tsx b/components/waves/header/WaveHeaderFollow.tsx
index 78f52218f0..b2bda8f836 100644
--- a/components/waves/header/WaveHeaderFollow.tsx
+++ b/components/waves/header/WaveHeaderFollow.tsx
@@ -35,15 +35,19 @@ const LOADER_SIZES: Record
= {
[WaveFollowBtnSize.MEDIUM]: CircleLoaderSize.MEDIUM,
};
+interface WaveHeaderFollowProps {
+ readonly wave: ApiWave;
+ readonly subscribeToAllDrops?: boolean | undefined;
+ readonly size?: WaveFollowBtnSize | undefined;
+ readonly fullWidth?: boolean | undefined;
+}
+
export default function WaveHeaderFollow({
wave,
subscribeToAllDrops = false,
size = WaveFollowBtnSize.MEDIUM,
-}: {
- readonly wave: ApiWave;
- readonly subscribeToAllDrops?: boolean | undefined;
- readonly size?: WaveFollowBtnSize | undefined;
-}) {
+ fullWidth = false,
+}: WaveHeaderFollowProps) {
const { setToast, requestAuth } = useContext(AuthContext);
const { onWaveFollowChange } = useContext(ReactQueryWrapperContext);
const following = !!wave.subscribed_actions.length;
@@ -131,14 +135,16 @@ export default function WaveHeaderFollow({
const printIcon = () => {
if (mutating) {
return ;
- } else if (following) {
+ }
+ if (following) {
return (
);
- } else {
- return (
-
- );
}
+ return (
+
+ );
};
return (
-
+
diff --git a/components/waves/leaderboard/grid/WaveLeaderboardGridItem.tsx b/components/waves/leaderboard/grid/WaveLeaderboardGridItem.tsx
index bccd0d60b5..0e96bb70e2 100644
--- a/components/waves/leaderboard/grid/WaveLeaderboardGridItem.tsx
+++ b/components/waves/leaderboard/grid/WaveLeaderboardGridItem.tsx
@@ -333,6 +333,7 @@ export const WaveLeaderboardGridItem: React.FC<
=
- seizeSettings.all_drops_notifications_subscribers_limit;
+ const settings = useWaveNotificationSettings(wave);
- const [following, setFollowing] = useState(false);
- const [isAllEnabled, setIsAllEnabled] = useState();
- const [isMuted, setIsMuted] = useState(wave.metrics.muted);
- const [muteLoading, setMuteLoading] = useState(false);
-
- const { data, refetch } = useWaveNotificationSubscription(wave);
-
- const { setToast } = useAuth();
-
- const [loading, setLoading] = useState(false);
- const [loadingTarget, setLoadingTarget] = useState<"mentions" | "all" | null>(
- null
- );
-
- useEffect(() => {
- setIsMuted(wave.metrics.muted);
- }, [wave.metrics.muted]);
-
- const toggleMute = useCallback(async () => {
- setMuteLoading(true);
- try {
- if (isMuted) {
- await commonApiDelete({ endpoint: `waves/${wave.id}/mute` });
- } else {
- await commonApiPost({ endpoint: `waves/${wave.id}/mute`, body: {} });
- }
- setIsMuted(!isMuted);
- queryClient.invalidateQueries({
- queryKey: [QueryKey.WAVE, { wave_id: wave.id }],
- });
- queryClient.invalidateQueries({
- queryKey: [QueryKey.WAVES_OVERVIEW],
- });
- } catch (error) {
- const defaultMessage = isMuted
- ? "Unable to unmute wave"
- : "Unable to mute wave";
- const errorMessage = typeof error === "string" ? error : defaultMessage;
- setToast({
- message: errorMessage,
- type: "error",
- });
- } finally {
- setMuteLoading(false);
- }
- }, [isMuted, wave.id, queryClient, setToast]);
-
- useEffect(() => {
- setIsAllEnabled(data?.subscribed && !disableSelection);
- }, [data, disableSelection]);
-
- const toggleNotifications = useCallback(
- async (enableAll: boolean) => {
- if (enableAll === isAllEnabled) return;
-
- setLoadingTarget(enableAll ? "all" : "mentions");
- setLoading(true);
-
- if (enableAll) {
- try {
- await commonApiPost({
- endpoint: `notifications/wave-subscription/${wave.id}`,
- body: {},
- });
- await refetch();
- setLoading(false);
- setLoadingTarget(null);
- } catch (error) {
- setLoading(false);
- setLoadingTarget(null);
- setToast({
- message:
- typeof error === "string"
- ? error
- : "Unable to subscribe to all drops",
- type: "error",
- });
- }
- } else {
- try {
- await commonApiDelete({
- endpoint: `notifications/wave-subscription/${wave.id}`,
- });
- await refetch();
- setLoading(false);
- setLoadingTarget(null);
- } catch (error) {
- setLoading(false);
- setLoadingTarget(null);
- setToast({
- message:
- typeof error === "string"
- ? error
- : "Unable to subscribe to mentions",
- type: "error",
- });
- }
- }
- },
- [isAllEnabled, wave.id, refetch, setToast]
- );
-
- useEffect(() => {
- setFollowing(!!wave.subscribed_actions.length);
- refetch();
- }, [wave.subscribed_actions.length, refetch]);
-
- const getMentionsTooltip = () => {
- return isAllEnabled ? "Click to switch to mentions-only notifications" : "";
- };
-
- const getAllTooltip = () => {
- if (disableSelection) {
- return `'All' notifications unavailable for waves with ${seizeSettings.all_drops_notifications_subscribers_limit.toLocaleString()}+ followers.`;
- }
- return !isAllEnabled ? "Click to enable notifications for all drops" : "";
- };
-
- const getActiveButtonStyle = () => {
- return "tw-bg-iron-800 tw-text-primary-400 tw-font-medium";
- };
-
- const getInactiveButtonStyle = () => {
- return "tw-text-iron-400 desktop-hover:hover:tw-text-iron-300 tw-bg-transparent";
- };
-
- const getDisabledButtonStyle = () => {
- return "tw-text-iron-500 tw-bg-transparent tw-cursor-not-allowed";
- };
-
- if (!following) {
+ if (!settings.following) {
return null;
}
- const getMuteTooltip = () => {
- return isMuted ? "Click to unmute this wave" : "Click to mute this wave";
- };
+ if (settings.isMuted) {
+ return ;
+ }
- if (isMuted) {
- return (
-
-
-
- {getMuteTooltip()}
-
- }>
-
-
-
-
- );
+ if (settings.preferencesUnavailable) {
+ return ;
}
return (
-
-
-
- {isAllEnabled ? (
-
- {getMentionsTooltip()}
-
- }>
-
-
- ) : (
-
- )}
-
- {disableSelection ? (
-
- {getAllTooltip()}
-
- }>
-
-
- ) : isAllEnabled ? (
-
- ) : (
-
- {getAllTooltip()}
-
- }>
-
-
- )}
-
-
-
+
);
}
diff --git a/components/waves/specs/wave-notification-settings/WaveMutedNotificationButton.tsx b/components/waves/specs/wave-notification-settings/WaveMutedNotificationButton.tsx
new file mode 100644
index 0000000000..eb0c115835
--- /dev/null
+++ b/components/waves/specs/wave-notification-settings/WaveMutedNotificationButton.tsx
@@ -0,0 +1,47 @@
+import { Spinner } from "@/components/dotLoader/DotLoader";
+import { faBellSlash } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import type { WaveNotificationSettingsState } from "./useWaveNotificationSettings";
+
+interface WaveMutedNotificationButtonProps {
+ readonly waveId: string;
+ readonly settings: WaveNotificationSettingsState;
+}
+
+export default function WaveMutedNotificationButton({
+ waveId,
+ settings,
+}: WaveMutedNotificationButtonProps) {
+ return (
+
+
+ {settings.muteTooltip}
+
+ }
+ >
+
+
+
+ );
+}
diff --git a/components/waves/specs/wave-notification-settings/WaveNotificationPreferenceButtons.tsx b/components/waves/specs/wave-notification-settings/WaveNotificationPreferenceButtons.tsx
new file mode 100644
index 0000000000..1760f3fe64
--- /dev/null
+++ b/components/waves/specs/wave-notification-settings/WaveNotificationPreferenceButtons.tsx
@@ -0,0 +1,113 @@
+import { Spinner } from "@/components/dotLoader/DotLoader";
+import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import type { WaveNotificationSettingsState } from "./useWaveNotificationSettings";
+
+interface WaveNotificationPreferenceButtonsProps {
+ readonly waveId: string;
+ readonly settings: WaveNotificationSettingsState;
+}
+
+const getButtonStyle = (active: boolean) => {
+ return active
+ ? "tw-bg-iron-800 tw-text-primary-400 tw-font-medium"
+ : "tw-text-iron-400 desktop-hover:hover:tw-text-iron-300 tw-bg-transparent";
+};
+
+function getAllDropsButtonStyle(settings: WaveNotificationSettingsState) {
+ const buttonStyle = getButtonStyle(settings.allDropsEnabled);
+ return settings.disableAllDropsSelection && !settings.allDropsEnabled
+ ? `${buttonStyle} tw-cursor-not-allowed`
+ : buttonStyle;
+}
+
+function AllDropsIcon({ className }: { readonly className: string }) {
+ return (
+
+ );
+}
+
+export default function WaveNotificationPreferenceButtons({
+ waveId,
+ settings,
+}: WaveNotificationPreferenceButtonsProps) {
+ const allDropsSelectionDisabled =
+ settings.disableAllDropsSelection && !settings.allDropsEnabled;
+ const allDropsTooltipId = `all-drops-tooltip-${waveId}`;
+ const allDropsDisabledDescriptionId = `${allDropsTooltipId}-disabled-description`;
+ const allDropsButton = (
+
+ );
+
+ return (
+
+
+ {settings.allGroupTooltip}
+
+ }
+ >
+
+
+
+
+ {settings.allDropsTooltip}
+
+ }
+ >
+ {allDropsButton}
+
+
+ );
+}
diff --git a/components/waves/specs/wave-notification-settings/WaveNotificationRetryButton.tsx b/components/waves/specs/wave-notification-settings/WaveNotificationRetryButton.tsx
new file mode 100644
index 0000000000..84c148cac1
--- /dev/null
+++ b/components/waves/specs/wave-notification-settings/WaveNotificationRetryButton.tsx
@@ -0,0 +1,28 @@
+import { Spinner } from "@/components/dotLoader/DotLoader";
+import type { WaveNotificationSettingsState } from "./useWaveNotificationSettings";
+
+interface WaveNotificationRetryButtonProps {
+ readonly settings: WaveNotificationSettingsState;
+}
+
+export default function WaveNotificationRetryButton({
+ settings,
+}: WaveNotificationRetryButtonProps) {
+ return (
+
+
+
+ );
+}
diff --git a/components/waves/specs/wave-notification-settings/useWaveMuteSettings.ts b/components/waves/specs/wave-notification-settings/useWaveMuteSettings.ts
new file mode 100644
index 0000000000..5c9c01adea
--- /dev/null
+++ b/components/waves/specs/wave-notification-settings/useWaveMuteSettings.ts
@@ -0,0 +1,58 @@
+import { useAuth } from "@/components/auth/Auth";
+import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import { commonApiDelete, commonApiPost } from "@/services/api/common-api";
+import { useQueryClient } from "@tanstack/react-query";
+import { useCallback, useState } from "react";
+import { getErrorMessage } from "./waveNotificationSettings.helpers";
+
+export function useWaveMuteSettings(wave: ApiWave) {
+ const queryClient = useQueryClient();
+ const { setToast } = useAuth();
+ const isMuted = wave.metrics.muted;
+ const [muteLoading, setMuteLoading] = useState(false);
+
+ const toggleMute = useCallback(async () => {
+ setMuteLoading(true);
+ try {
+ if (isMuted) {
+ await commonApiDelete({ endpoint: `waves/${wave.id}/mute` });
+ } else {
+ await commonApiPost({ endpoint: `waves/${wave.id}/mute`, body: {} });
+ }
+ await Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: [QueryKey.WAVE, { wave_id: wave.id }],
+ }),
+ queryClient.invalidateQueries({
+ queryKey: [QueryKey.WAVES_OVERVIEW],
+ }),
+ ]);
+ } catch (error) {
+ const defaultMessage = isMuted
+ ? "Unable to unmute wave"
+ : "Unable to mute wave";
+ setToast({
+ message: getErrorMessage(error, defaultMessage),
+ type: "error",
+ });
+ } finally {
+ setMuteLoading(false);
+ }
+ }, [isMuted, wave.id, queryClient, setToast]);
+
+ const onMuteClick = useCallback(() => {
+ void toggleMute();
+ }, [toggleMute]);
+
+ const muteTooltip = isMuted
+ ? "Click to unmute this wave"
+ : "Click to mute this wave";
+
+ return {
+ isMuted,
+ muteLoading,
+ muteTooltip,
+ onMuteClick,
+ };
+}
diff --git a/components/waves/specs/wave-notification-settings/useWaveNotificationSettings.ts b/components/waves/specs/wave-notification-settings/useWaveNotificationSettings.ts
new file mode 100644
index 0000000000..0aad4c5053
--- /dev/null
+++ b/components/waves/specs/wave-notification-settings/useWaveNotificationSettings.ts
@@ -0,0 +1,19 @@
+import type { ApiWave } from "@/generated/models/ApiWave";
+import { useWaveMuteSettings } from "./useWaveMuteSettings";
+import { useWavePreferenceSettings } from "./useWavePreferenceSettings";
+
+export function useWaveNotificationSettings(wave: ApiWave) {
+ const mute = useWaveMuteSettings(wave);
+ const preferences = useWavePreferenceSettings(wave);
+ const following = wave.subscribed_actions.length > 0;
+
+ return {
+ following,
+ ...mute,
+ ...preferences,
+ };
+}
+
+export type WaveNotificationSettingsState = ReturnType<
+ typeof useWaveNotificationSettings
+>;
diff --git a/components/waves/specs/wave-notification-settings/useWavePreferenceSettings.ts b/components/waves/specs/wave-notification-settings/useWavePreferenceSettings.ts
new file mode 100644
index 0000000000..90f2cd4c95
--- /dev/null
+++ b/components/waves/specs/wave-notification-settings/useWavePreferenceSettings.ts
@@ -0,0 +1,155 @@
+import { useAuth } from "@/components/auth/Auth";
+import { useSeizeSettings } from "@/contexts/SeizeSettingsContext";
+import type { ApiUpdateWaveNotificationPreferencesRequest } from "@/generated/models/ApiUpdateWaveNotificationPreferencesRequest";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import type { ApiWaveNotificationPreferences } from "@/generated/models/ApiWaveNotificationPreferences";
+import { useWaveNotificationSubscription } from "@/hooks/useWaveNotificationSubscription";
+import { commonApiPost } from "@/services/api/common-api";
+import { useCallback, useMemo, useState } from "react";
+import {
+ ALL_GROUP_MENTION,
+ getAllDropsTooltip,
+ getErrorMessage,
+ type NotificationLoadingTarget,
+} from "./waveNotificationSettings.helpers";
+
+export function useWavePreferenceSettings(wave: ApiWave) {
+ const { seizeSettings } = useSeizeSettings();
+ const { setToast } = useAuth();
+ const [loadingTarget, setLoadingTarget] =
+ useState(null);
+
+ const {
+ data,
+ refetch,
+ isFetching: preferencesFetching = false,
+ isPending: preferencesPending = false,
+ } = useWaveNotificationSubscription(wave);
+
+ const allDropsNotificationsSubscribersLimit =
+ seizeSettings.all_drops_notifications_subscribers_limit;
+ const disableAllDropsSelection =
+ wave.metrics.subscribers_count >= allDropsNotificationsSubscribersLimit;
+
+ const enabledGroupNotifications = useMemo(
+ () => data?.enabled_group_notifications ?? [],
+ [data?.enabled_group_notifications]
+ );
+
+ const subscribedToAllDrops = !!data?.subscribed;
+ const allDropsEnabled = subscribedToAllDrops;
+ const allGroupNotificationsEnabled =
+ enabledGroupNotifications.includes(ALL_GROUP_MENTION);
+ const loading =
+ loadingTarget !== null || preferencesPending || preferencesFetching;
+ const preferencesUnavailable = !data && !preferencesPending;
+
+ const updateNotificationPreferences = useCallback(
+ async ({
+ body,
+ target,
+ errorMessage,
+ }: {
+ readonly body: ApiUpdateWaveNotificationPreferencesRequest;
+ readonly target: NotificationLoadingTarget;
+ readonly errorMessage: string;
+ }) => {
+ setLoadingTarget(target);
+ try {
+ await commonApiPost<
+ ApiUpdateWaveNotificationPreferencesRequest,
+ ApiWaveNotificationPreferences
+ >({
+ endpoint: `notifications/wave-subscription/${wave.id}`,
+ body,
+ });
+ await refetch();
+ } catch (error) {
+ setToast({
+ message: getErrorMessage(error, errorMessage),
+ type: "error",
+ });
+ } finally {
+ setLoadingTarget(null);
+ }
+ },
+ [wave.id, refetch, setToast]
+ );
+
+ const toggleAllGroupNotifications = useCallback(async () => {
+ await updateNotificationPreferences({
+ target: "all-group",
+ body: {
+ subscribed: subscribedToAllDrops,
+ enabled_group_notifications: allGroupNotificationsEnabled
+ ? []
+ : [ALL_GROUP_MENTION],
+ },
+ errorMessage: allGroupNotificationsEnabled
+ ? "Unable to disable @ALL notifications"
+ : "Unable to enable @ALL notifications",
+ });
+ }, [
+ allGroupNotificationsEnabled,
+ subscribedToAllDrops,
+ updateNotificationPreferences,
+ ]);
+
+ const toggleAllDropsNotifications = useCallback(async () => {
+ if (!subscribedToAllDrops && disableAllDropsSelection) {
+ return;
+ }
+
+ await updateNotificationPreferences({
+ target: "all-drops",
+ body: {
+ subscribed: !subscribedToAllDrops,
+ enabled_group_notifications: enabledGroupNotifications,
+ },
+ errorMessage: subscribedToAllDrops
+ ? "Unable to disable all drop notifications"
+ : "Unable to enable all drop notifications",
+ });
+ }, [
+ disableAllDropsSelection,
+ enabledGroupNotifications,
+ subscribedToAllDrops,
+ updateNotificationPreferences,
+ ]);
+
+ const onAllGroupNotificationsClick = useCallback(() => {
+ void toggleAllGroupNotifications();
+ }, [toggleAllGroupNotifications]);
+
+ const onAllDropsNotificationsClick = useCallback(() => {
+ void toggleAllDropsNotifications();
+ }, [toggleAllDropsNotifications]);
+
+ const onRetryClick = useCallback(() => {
+ void refetch();
+ }, [refetch]);
+
+ const allGroupTooltip = allGroupNotificationsEnabled
+ ? "Click to disable @ALL notifications"
+ : "Click to enable @ALL notifications";
+ const allDropsTooltip = getAllDropsTooltip({
+ disableAllDropsSelection,
+ subscribedToAllDrops,
+ subscribersLimit: allDropsNotificationsSubscribersLimit,
+ });
+
+ return {
+ allDropsEnabled,
+ allGroupNotificationsEnabled,
+ allDropsTooltip,
+ allGroupTooltip,
+ disableAllDropsSelection,
+ loading,
+ loadingTarget,
+ onAllDropsNotificationsClick,
+ onAllGroupNotificationsClick,
+ onRetryClick,
+ preferencesFetching,
+ preferencesUnavailable,
+ };
+}
diff --git a/components/waves/specs/wave-notification-settings/waveNotificationSettings.helpers.ts b/components/waves/specs/wave-notification-settings/waveNotificationSettings.helpers.ts
new file mode 100644
index 0000000000..31a9f4cc8f
--- /dev/null
+++ b/components/waves/specs/wave-notification-settings/waveNotificationSettings.helpers.ts
@@ -0,0 +1,44 @@
+import { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
+
+export type NotificationLoadingTarget = "all-group" | "all-drops";
+
+export const ALL_GROUP_MENTION = ApiDropGroupMention.All;
+
+export const getErrorMessage = (error: unknown, defaultMessage: string) => {
+ if (error instanceof Error) {
+ return error.message;
+ }
+
+ if (typeof error === "object" && error !== null) {
+ const message = (error as { message?: unknown }).message;
+ if (typeof message === "string") {
+ return message;
+ }
+ }
+
+ if (typeof error === "string") {
+ return error;
+ }
+
+ return defaultMessage;
+};
+
+export function getAllDropsTooltip({
+ disableAllDropsSelection,
+ subscribedToAllDrops,
+ subscribersLimit,
+}: {
+ readonly disableAllDropsSelection: boolean;
+ readonly subscribedToAllDrops: boolean;
+ readonly subscribersLimit: number;
+}) {
+ if (disableAllDropsSelection && !subscribedToAllDrops) {
+ return `'All' notifications unavailable for waves with ${subscribersLimit.toLocaleString()}+ followers.`;
+ }
+
+ if (subscribedToAllDrops) {
+ return "Click to disable notifications for all drops";
+ }
+
+ return "Click to enable notifications for all drops";
+}
diff --git a/components/waves/utils/getOptimisticDrop.ts b/components/waves/utils/getOptimisticDrop.ts
index 16365ff6c1..49ac96fc59 100644
--- a/components/waves/utils/getOptimisticDrop.ts
+++ b/components/waves/utils/getOptimisticDrop.ts
@@ -128,5 +128,6 @@ export const getOptimisticDrop = (
reactions: [],
boosts: 0,
hide_link_preview: false,
+ mentioned_groups: dropRequest.mentioned_groups ?? [],
};
};
diff --git a/entities/IDrop.ts b/entities/IDrop.ts
index aca3f73677..0b05ab8b29 100644
--- a/entities/IDrop.ts
+++ b/entities/IDrop.ts
@@ -1,4 +1,5 @@
import type { ApiCreateDropRequest } from "@/generated/models/ApiCreateDropRequest";
+import type { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
export interface ReferencedNft {
readonly contract: string;
@@ -45,6 +46,7 @@ export interface CreateDropRequestPart {
export interface CreateDropPart extends Omit {
readonly media: Array;
+ readonly mentioned_groups?: Array;
}
export interface CreateDropConfig extends Omit<
diff --git a/generated/models/ApiCreateDropRequest.ts b/generated/models/ApiCreateDropRequest.ts
index 7ac4a4a324..deb4a3a756 100644
--- a/generated/models/ApiCreateDropRequest.ts
+++ b/generated/models/ApiCreateDropRequest.ts
@@ -13,6 +13,7 @@
import { ApiCreateDropPart } from '../models/ApiCreateDropPart';
import { ApiCreateMentionedWave } from '../models/ApiCreateMentionedWave';
+import { ApiDropGroupMention } from '../models/ApiDropGroupMention';
import { ApiDropMentionedUser } from '../models/ApiDropMentionedUser';
import { ApiDropMetadata } from '../models/ApiDropMetadata';
import { ApiDropReferencedNFT } from '../models/ApiDropReferencedNFT';
@@ -24,7 +25,7 @@ export class ApiCreateDropRequest {
'wave_id': string;
'reply_to'?: ApiReplyToDrop;
'drop_type'?: ApiDropType;
- 'mentions_all'?: boolean;
+ 'mentioned_groups'?: Array;
'title'?: string | null;
'parts': Array;
'referenced_nfts': Array;
@@ -62,9 +63,9 @@ export class ApiCreateDropRequest {
"format": ""
},
{
- "name": "mentions_all",
- "baseName": "mentions_all",
- "type": "boolean",
+ "name": "mentioned_groups",
+ "baseName": "mentioned_groups",
+ "type": "Array",
"format": ""
},
{
diff --git a/generated/models/ApiDrop.ts b/generated/models/ApiDrop.ts
index f8edf00621..3db2722187 100644
--- a/generated/models/ApiDrop.ts
+++ b/generated/models/ApiDrop.ts
@@ -12,6 +12,7 @@
*/
import { ApiDropContextProfileContext } from '../models/ApiDropContextProfileContext';
+import { ApiDropGroupMention } from '../models/ApiDropGroupMention';
import { ApiDropMentionedUser } from '../models/ApiDropMentionedUser';
import { ApiDropMetadataResponse } from '../models/ApiDropMetadataResponse';
import { ApiDropNftLink } from '../models/ApiDropNftLink';
@@ -56,6 +57,7 @@ export class ApiDrop {
'parts_count': number;
'referenced_nfts': Array;
'mentioned_users': Array;
+ 'mentioned_groups': Array;
'mentioned_waves': Array;
'metadata': Array;
'rating': number;
@@ -166,6 +168,12 @@ export class ApiDrop {
"type": "Array",
"format": ""
},
+ {
+ "name": "mentioned_groups",
+ "baseName": "mentioned_groups",
+ "type": "Array",
+ "format": ""
+ },
{
"name": "mentioned_waves",
"baseName": "mentioned_waves",
diff --git a/generated/models/ApiDropGroupMention.ts b/generated/models/ApiDropGroupMention.ts
new file mode 100644
index 0000000000..d5d7504ab2
--- /dev/null
+++ b/generated/models/ApiDropGroupMention.ts
@@ -0,0 +1,18 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export enum ApiDropGroupMention {
+ All = 'ALL'
+}
diff --git a/generated/models/ApiDropWithoutWave.ts b/generated/models/ApiDropWithoutWave.ts
index 098947cea1..18e4c94e96 100644
--- a/generated/models/ApiDropWithoutWave.ts
+++ b/generated/models/ApiDropWithoutWave.ts
@@ -12,6 +12,7 @@
*/
import { ApiDropContextProfileContext } from '../models/ApiDropContextProfileContext';
+import { ApiDropGroupMention } from '../models/ApiDropGroupMention';
import { ApiDropMentionedUser } from '../models/ApiDropMentionedUser';
import { ApiDropMetadataResponse } from '../models/ApiDropMetadataResponse';
import { ApiDropNftLink } from '../models/ApiDropNftLink';
@@ -54,6 +55,7 @@ export class ApiDropWithoutWave {
'parts_count': number;
'referenced_nfts': Array;
'mentioned_users': Array;
+ 'mentioned_groups': Array;
'mentioned_waves': Array;
'metadata': Array;
'rating': number;
@@ -158,6 +160,12 @@ export class ApiDropWithoutWave {
"type": "Array",
"format": ""
},
+ {
+ "name": "mentioned_groups",
+ "baseName": "mentioned_groups",
+ "type": "Array",
+ "format": ""
+ },
{
"name": "mentioned_waves",
"baseName": "mentioned_waves",
diff --git a/generated/models/ApiLightDrop.ts b/generated/models/ApiLightDrop.ts
index 2a18cb9f9f..fc884d6a1f 100644
--- a/generated/models/ApiLightDrop.ts
+++ b/generated/models/ApiLightDrop.ts
@@ -17,6 +17,10 @@ import { HttpFile } from '../http/http';
export class ApiLightDrop {
'id': string;
+ 'wave_id': string;
+ 'wave_name': string;
+ 'author': string;
+ 'created_at': number;
/**
* Sequence number of the drop in Seize
*/
@@ -39,6 +43,30 @@ export class ApiLightDrop {
"type": "string",
"format": ""
},
+ {
+ "name": "wave_id",
+ "baseName": "wave_id",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "wave_name",
+ "baseName": "wave_name",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "author",
+ "baseName": "author",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "created_at",
+ "baseName": "created_at",
+ "type": "number",
+ "format": "int64"
+ },
{
"name": "serial_no",
"baseName": "serial_no",
diff --git a/generated/models/ApiUpdateDropRequest.ts b/generated/models/ApiUpdateDropRequest.ts
index 2725eca4bb..f57bd44449 100644
--- a/generated/models/ApiUpdateDropRequest.ts
+++ b/generated/models/ApiUpdateDropRequest.ts
@@ -19,7 +19,6 @@ import { ApiDropReferencedNFT } from '../models/ApiDropReferencedNFT';
import { HttpFile } from '../http/http';
export class ApiUpdateDropRequest {
- 'mentions_all'?: boolean;
'title'?: string | null;
'parts': Array;
'referenced_nfts': Array;
@@ -38,12 +37,6 @@ export class ApiUpdateDropRequest {
static readonly mapping: {[index: string]: string} | undefined = undefined;
static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
- {
- "name": "mentions_all",
- "baseName": "mentions_all",
- "type": "boolean",
- "format": ""
- },
{
"name": "title",
"baseName": "title",
diff --git a/generated/models/GetWaveSubscription200Response.ts b/generated/models/ApiUpdateWaveNotificationPreferencesRequest.ts
similarity index 67%
rename from generated/models/GetWaveSubscription200Response.ts
rename to generated/models/ApiUpdateWaveNotificationPreferencesRequest.ts
index 642f030342..93807b4071 100644
--- a/generated/models/GetWaveSubscription200Response.ts
+++ b/generated/models/ApiUpdateWaveNotificationPreferencesRequest.ts
@@ -11,10 +11,12 @@
* Do not edit the class manually.
*/
+import { ApiDropGroupMention } from '../models/ApiDropGroupMention';
import { HttpFile } from '../http/http';
-export class GetWaveSubscription200Response {
+export class ApiUpdateWaveNotificationPreferencesRequest {
'subscribed'?: boolean;
+ 'enabled_group_notifications'?: Array;
static readonly discriminator: string | undefined = undefined;
@@ -26,10 +28,16 @@ export class GetWaveSubscription200Response {
"baseName": "subscribed",
"type": "boolean",
"format": ""
+ },
+ {
+ "name": "enabled_group_notifications",
+ "baseName": "enabled_group_notifications",
+ "type": "Array",
+ "format": ""
} ];
static getAttributeTypeMap() {
- return GetWaveSubscription200Response.attributeTypeMap;
+ return ApiUpdateWaveNotificationPreferencesRequest.attributeTypeMap;
}
public constructor() {
diff --git a/generated/models/ApiWaveNotificationPreferences.ts b/generated/models/ApiWaveNotificationPreferences.ts
new file mode 100644
index 0000000000..c240b86414
--- /dev/null
+++ b/generated/models/ApiWaveNotificationPreferences.ts
@@ -0,0 +1,45 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { ApiDropGroupMention } from '../models/ApiDropGroupMention';
+import { HttpFile } from '../http/http';
+
+export class ApiWaveNotificationPreferences {
+ 'subscribed': boolean;
+ 'enabled_group_notifications': Array;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "subscribed",
+ "baseName": "subscribed",
+ "type": "boolean",
+ "format": ""
+ },
+ {
+ "name": "enabled_group_notifications",
+ "baseName": "enabled_group_notifications",
+ "type": "Array",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiWaveNotificationPreferences.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ObjectSerializer.ts b/generated/models/ObjectSerializer.ts
index b14dbf774c..b2c68bb7fe 100644
--- a/generated/models/ObjectSerializer.ts
+++ b/generated/models/ObjectSerializer.ts
@@ -71,6 +71,7 @@ export * from '../models/ApiDropBoostsPage';
export * from '../models/ApiDropContextProfileContext';
export * from '../models/ApiDropCuration';
export * from '../models/ApiDropCurationRequest';
+export * from '../models/ApiDropGroupMention';
export * from '../models/ApiDropId';
export * from '../models/ApiDropMedia';
export * from '../models/ApiDropMentionedUser';
@@ -193,6 +194,7 @@ export * from '../models/ApiUpcomingMemeSubscriptionStatus';
export * from '../models/ApiUpdateDropRequest';
export * from '../models/ApiUpdateProxyActionRequest';
export * from '../models/ApiUpdateWaveDecisionPause';
+export * from '../models/ApiUpdateWaveNotificationPreferencesRequest';
export * from '../models/ApiUpdateWaveParticipationConfig';
export * from '../models/ApiUpdateWaveRequest';
export * from '../models/ApiUploadItem';
@@ -219,6 +221,7 @@ export * from '../models/ApiWaveLog';
export * from '../models/ApiWaveMetadataType';
export * from '../models/ApiWaveMetrics';
export * from '../models/ApiWaveMin';
+export * from '../models/ApiWaveNotificationPreferences';
export * from '../models/ApiWaveOutcome';
export * from '../models/ApiWaveOutcomeCredit';
export * from '../models/ApiWaveOutcomeDistributionItem';
@@ -272,7 +275,6 @@ export * from '../models/DistributionPhotoCompleteRequest';
export * from '../models/DistributionPhotoCompleteRequestPhoto';
export * from '../models/DistributionPhotoCompleteResponse';
export * from '../models/DistributionPhotosPage';
-export * from '../models/GetWaveSubscription200Response';
export * from '../models/MintingClaim';
export * from '../models/MintingClaimAnimationDetails';
export * from '../models/MintingClaimAnimationDetailsGlb';
@@ -376,13 +378,14 @@ import { ApiCreateWaveOutcome } from '../models/ApiCreateWaveOutcome';
import { ApiCreateWaveOutcomeDistributionItem } from '../models/ApiCreateWaveOutcomeDistributionItem';
import { ApiDistributionAirdropsCsvUploadRequest } from '../models/ApiDistributionAirdropsCsvUploadRequest';
import { ApiDistributionAirdropsUploadResponse } from '../models/ApiDistributionAirdropsUploadResponse';
-import { ApiDrop } from '../models/ApiDrop';
+import { ApiDrop } from '../models/ApiDrop';
import { ApiDropAndDropVote } from '../models/ApiDropAndDropVote';
import { ApiDropBoost } from '../models/ApiDropBoost';
import { ApiDropBoostsPage } from '../models/ApiDropBoostsPage';
import { ApiDropContextProfileContext } from '../models/ApiDropContextProfileContext';
import { ApiDropCuration } from '../models/ApiDropCuration';
import { ApiDropCurationRequest } from '../models/ApiDropCurationRequest';
+import { ApiDropGroupMention } from '../models/ApiDropGroupMention';
import { ApiDropId } from '../models/ApiDropId';
import { ApiDropMedia } from '../models/ApiDropMedia';
import { ApiDropMentionedUser } from '../models/ApiDropMentionedUser';
@@ -402,7 +405,7 @@ import { ApiDropTraceItem } from '../models/ApiDropTraceItem';
import { ApiDropType } from '../models/ApiDropType';
import { ApiDropVote } from '../models/ApiDropVote';
import { ApiDropWinningContext } from '../models/ApiDropWinningContext';
-import { ApiDropWithoutWave } from '../models/ApiDropWithoutWave';
+import { ApiDropWithoutWave } from '../models/ApiDropWithoutWave';
import { ApiDropWithoutWavesPageWithoutCount } from '../models/ApiDropWithoutWavesPageWithoutCount';
import { ApiDropsLeaderboardPage } from '../models/ApiDropsLeaderboardPage';
import { ApiDropsPage } from '../models/ApiDropsPage';
@@ -426,7 +429,7 @@ import { ApiIdentitySubscriptionTargetAction } from '../models/ApiIdentitySubscr
import { ApiIdentitySubscriptionTargetType } from '../models/ApiIdentitySubscriptionTargetType';
import { ApiIncomingIdentitySubscriptionsPage } from '../models/ApiIncomingIdentitySubscriptionsPage';
import { ApiIntRange } from '../models/ApiIntRange';
-import { ApiLightDrop } from '../models/ApiLightDrop';
+import { ApiLightDrop } from '../models/ApiLightDrop';
import { ApiLoginRequest } from '../models/ApiLoginRequest';
import { ApiLoginResponse } from '../models/ApiLoginResponse';
import { ApiMarkDropUnreadResponse } from '../models/ApiMarkDropUnreadResponse';
@@ -505,6 +508,7 @@ import { ApiUpcomingMemeSubscriptionStatus , ApiUpcomingMemeSubscriptionStatus
import { ApiUpdateDropRequest } from '../models/ApiUpdateDropRequest';
import { ApiUpdateProxyActionRequest } from '../models/ApiUpdateProxyActionRequest';
import { ApiUpdateWaveDecisionPause } from '../models/ApiUpdateWaveDecisionPause';
+import { ApiUpdateWaveNotificationPreferencesRequest } from '../models/ApiUpdateWaveNotificationPreferencesRequest';
import { ApiUpdateWaveParticipationConfig } from '../models/ApiUpdateWaveParticipationConfig';
import { ApiUpdateWaveRequest } from '../models/ApiUpdateWaveRequest';
import { ApiUploadItem } from '../models/ApiUploadItem';
@@ -531,6 +535,7 @@ import { ApiWaveLog } from '../models/ApiWaveLog';
import { ApiWaveMetadataType } from '../models/ApiWaveMetadataType';
import { ApiWaveMetrics } from '../models/ApiWaveMetrics';
import { ApiWaveMin } from '../models/ApiWaveMin';
+import { ApiWaveNotificationPreferences } from '../models/ApiWaveNotificationPreferences';
import { ApiWaveOutcome } from '../models/ApiWaveOutcome';
import { ApiWaveOutcomeCredit } from '../models/ApiWaveOutcomeCredit';
import { ApiWaveOutcomeDistributionItem } from '../models/ApiWaveOutcomeDistributionItem';
@@ -584,7 +589,6 @@ import { DistributionPhotoCompleteRequest } from '../models/DistributionPhotoCom
import { DistributionPhotoCompleteRequestPhoto } from '../models/DistributionPhotoCompleteRequestPhoto';
import { DistributionPhotoCompleteResponse } from '../models/DistributionPhotoCompleteResponse';
import { DistributionPhotosPage } from '../models/DistributionPhotosPage';
-import { GetWaveSubscription200Response } from '../models/GetWaveSubscription200Response';
import { MintingClaim } from '../models/MintingClaim';
import { MintingClaimAnimationDetailsClass } from '../models/MintingClaimAnimationDetails';
import { MintingClaimAnimationDetailsGlb , MintingClaimAnimationDetailsGlbFormatEnum } from '../models/MintingClaimAnimationDetailsGlb';
@@ -638,6 +642,7 @@ let primitives = [
let enumsMap: Set = new Set([
"AcceptActionRequestActionEnum",
"ApiCommunityMembersSortOption",
+ "ApiDropGroupMention",
"ApiDropSearchStrategy",
"ApiDropSubscriptionTargetAction",
"ApiDropType",
@@ -857,6 +862,7 @@ let typeMap: {[index: string]: any} = {
"ApiUpdateDropRequest": ApiUpdateDropRequest,
"ApiUpdateProxyActionRequest": ApiUpdateProxyActionRequest,
"ApiUpdateWaveDecisionPause": ApiUpdateWaveDecisionPause,
+ "ApiUpdateWaveNotificationPreferencesRequest": ApiUpdateWaveNotificationPreferencesRequest,
"ApiUpdateWaveParticipationConfig": ApiUpdateWaveParticipationConfig,
"ApiUpdateWaveRequest": ApiUpdateWaveRequest,
"ApiUploadItem": ApiUploadItem,
@@ -880,6 +886,7 @@ let typeMap: {[index: string]: any} = {
"ApiWaveLog": ApiWaveLog,
"ApiWaveMetrics": ApiWaveMetrics,
"ApiWaveMin": ApiWaveMin,
+ "ApiWaveNotificationPreferences": ApiWaveNotificationPreferences,
"ApiWaveOutcome": ApiWaveOutcome,
"ApiWaveOutcomeDistributionItem": ApiWaveOutcomeDistributionItem,
"ApiWaveOutcomeDistributionItemsPage": ApiWaveOutcomeDistributionItemsPage,
@@ -920,7 +927,6 @@ let typeMap: {[index: string]: any} = {
"DistributionPhotoCompleteRequestPhoto": DistributionPhotoCompleteRequestPhoto,
"DistributionPhotoCompleteResponse": DistributionPhotoCompleteResponse,
"DistributionPhotosPage": DistributionPhotosPage,
- "GetWaveSubscription200Response": GetWaveSubscription200Response,
"MintingClaim": MintingClaim,
"MintingClaimAnimationDetails": MintingClaimAnimationDetailsClass,
"MintingClaimAnimationDetailsGlb": MintingClaimAnimationDetailsGlb,
diff --git a/helpers/waves/drop-group-mentions.ts b/helpers/waves/drop-group-mentions.ts
new file mode 100644
index 0000000000..1bd5693b31
--- /dev/null
+++ b/helpers/waves/drop-group-mentions.ts
@@ -0,0 +1,55 @@
+import { ApiDropGroupMention } from "@/generated/models/ApiDropGroupMention";
+
+export const ALL_GROUP_MENTION_TEXT = "@all";
+
+const createAllGroupMentionPattern = () =>
+ /(^|[^A-Za-z0-9_@])(@all)(?![A-Za-z0-9_@])/g;
+
+export const getMentionedGroupsFromParts = (
+ parts: readonly {
+ readonly mentioned_groups?:
+ | readonly ApiDropGroupMention[]
+ | null
+ | undefined;
+ }[],
+ canMentionAll: boolean
+): ApiDropGroupMention[] => {
+ if (!canMentionAll) {
+ return [];
+ }
+
+ return parts.some((part) =>
+ part.mentioned_groups?.includes(ApiDropGroupMention.All)
+ )
+ ? [ApiDropGroupMention.All]
+ : [];
+};
+
+export const hasMentionedGroup = (
+ mentionedGroups: readonly ApiDropGroupMention[] | null | undefined,
+ group: ApiDropGroupMention
+) => mentionedGroups?.includes(group) ?? false;
+
+export const areMentionedGroupsEqual = (
+ a: readonly ApiDropGroupMention[],
+ b: readonly ApiDropGroupMention[]
+) => {
+ if (a.length !== b.length) {
+ return false;
+ }
+
+ return a.every((group) => b.includes(group));
+};
+
+export const markAllGroupMentionTokens = ({
+ content,
+ marker,
+}: {
+ readonly content: string;
+ readonly marker: string;
+}) =>
+ content.replace(
+ createAllGroupMentionPattern(),
+ (_match, prefix: string, token: string) =>
+ `${prefix}${marker}${token}${marker}`
+ );
diff --git a/hooks/drops/useDropUpdateMutation.ts b/hooks/drops/useDropUpdateMutation.ts
index 2313513ea6..44df955de1 100644
--- a/hooks/drops/useDropUpdateMutation.ts
+++ b/hooks/drops/useDropUpdateMutation.ts
@@ -1,4 +1,4 @@
-"use client"
+"use client";
import { useMutation } from "@tanstack/react-query";
import { commonApiPost } from "@/services/api/common-api";
@@ -37,7 +37,7 @@ export const useDropUpdateMutation = () => {
// Update the drop in wave messages store using existing processIncomingDrop
if (myStreamContext?.processIncomingDrop) {
myStreamContext.processIncomingDrop(
- updatedDrop,
+ updatedDrop,
ProcessIncomingDropType.DROP_INSERT // This will merge/update the drop
);
}
@@ -47,12 +47,14 @@ export const useDropUpdateMutation = () => {
},
onError: (error) => {
console.error("Failed to update drop:", error);
-
+
// Check if it's a time limit error
- const errorMessage = error instanceof Error ? error.message : String(error);
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
if (errorMessage.includes("can't be edited after")) {
setToast({
- message: "This drop can no longer be edited. Drops can only be edited within 5 minutes of creation.",
+ message:
+ "This drop can no longer be edited. Drops can only be edited within 5 minutes of creation.",
type: "error",
});
} else {
diff --git a/hooks/useIdentitiesSearch.tsx b/hooks/useIdentitiesSearch.tsx
index eb95c71498..5a2a8e56d1 100644
--- a/hooks/useIdentitiesSearch.tsx
+++ b/hooks/useIdentitiesSearch.tsx
@@ -9,6 +9,8 @@ interface UseIdentitiesSearchProps {
readonly waveId: string | null;
}
+export const IDENTITY_SEARCH_MIN_HANDLE_LENGTH = 3;
+
export function useIdentitiesSearch({
handle,
waveId,
@@ -34,7 +36,7 @@ export function useIdentitiesSearch({
params,
});
},
- enabled: handle.length >= 3,
+ enabled: handle.length >= IDENTITY_SEARCH_MIN_HANDLE_LENGTH,
});
return { identities: identities ?? [] };
diff --git a/hooks/useWaveNotificationSubscription.ts b/hooks/useWaveNotificationSubscription.ts
index 1c91b99d9b..27bd3195e5 100644
--- a/hooks/useWaveNotificationSubscription.ts
+++ b/hooks/useWaveNotificationSubscription.ts
@@ -1,22 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import { commonApiFetch } from "@/services/api/common-api";
import type { ApiWave } from "@/generated/models/ApiWave";
-import { useSeizeSettings } from "@/contexts/SeizeSettingsContext";
-import type { GetWaveSubscription200Response } from "@/generated/models/GetWaveSubscription200Response";
+import type { ApiWaveNotificationPreferences } from "@/generated/models/ApiWaveNotificationPreferences";
export function useWaveNotificationSubscription(wave: ApiWave) {
- const { seizeSettings } = useSeizeSettings();
return useQuery({
queryKey: ["wave-notification-subscription", wave.id],
queryFn: () => {
- return commonApiFetch({
+ return commonApiFetch({
endpoint: `notifications/wave-subscription/${wave.id}`,
});
},
- enabled:
- !!wave.id &&
- wave.metrics.subscribers_count <=
- seizeSettings.all_drops_notifications_subscribers_limit,
+ enabled: !!wave.id && wave.subscribed_actions.length > 0,
retry: (failureCount) => {
if (failureCount >= 3) {
return false;
diff --git a/openapi.yaml b/openapi.yaml
index f7768af52a..2ecbda1f7d 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -960,10 +960,24 @@ paths:
type: integer
format: int64
minimum: 1
+ - name: min_serial_no
+ in: query
+ required: false
+ description: Oldest message if null
+ schema:
+ type: integer
+ format: int64
+ minimum: 1
+ - name: older_first
+ in: query
+ required: false
+ description: By default this endpoint orders things newer first, but if you set it to true then it starts from older drops.
+ schema:
+ type: boolean
- name: wave_id
in: query
description: Drops in wave with given ID
- required: true
+ required: false
schema:
type: string
responses:
@@ -1018,6 +1032,17 @@ paths:
schema:
type: string
format: uuid
+ - name: curation_name
+ in: query
+ description: >-
+ Only include drops with persisted membership in curations with this
+ ApiWaveCuration.name across all visible waves. Cannot be combined
+ with curation_id.
+ required: false
+ schema:
+ type: string
+ minLength: 1
+ maxLength: 50
- name: serial_no_less_than
in: query
description: Used to find older drops
@@ -1887,6 +1912,13 @@ paths:
schema:
type: number
format: int64
+ - name: include_profile_groups
+ description: Include pure profile groups in search results
+ in: query
+ required: false
+ schema:
+ type: boolean
+ default: false
responses:
"200":
description: successful operation
@@ -2993,10 +3025,7 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- subscribed:
- type: boolean
+ $ref: "#/components/schemas/ApiWaveNotificationPreferences"
post:
tags:
- Notifications
@@ -3008,16 +3037,19 @@ paths:
required: true
schema:
type: string
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiUpdateWaveNotificationPreferencesRequest"
responses:
"200":
description: successful operation
content:
application/json:
schema:
- type: object
- properties:
- subscribed:
- type: boolean
+ $ref: "#/components/schemas/ApiWaveNotificationPreferences"
delete:
tags:
- Notifications
@@ -3035,10 +3067,7 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- subscribed:
- type: boolean
+ $ref: "#/components/schemas/ApiWaveNotificationPreferences"
/blocks:
get:
tags:
@@ -4088,7 +4117,7 @@ paths:
tags:
- Ratings
summary: >-
- Change REP rating of multiple targets and categories in one go. Targets
+ Change REP rating of multiple targets and categories in one go. Targets
final REP value will be the value you supply here. If you supply
multiple addresses for one consolidation group then those amounts will
be summed together.
@@ -7783,8 +7812,10 @@ components:
$ref: "#/components/schemas/ApiReplyToDrop"
drop_type:
$ref: "#/components/schemas/ApiDropType"
- mentions_all:
- type: boolean
+ mentioned_groups:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiDropGroupMention"
ApiCreateGroup:
type: object
required:
@@ -8049,7 +8080,7 @@ components:
$ref: "#/components/schemas/ApiWaveParticipationRequirement"
required_metadata:
description: |
- The metadata that must be provided by the participant.
+ The metadata that must be provided by the participant.
Empty array if nothing is required.
type: array
items:
@@ -8305,6 +8336,7 @@ components:
- updated_at
- referenced_nfts
- mentioned_users
+ - mentioned_groups
- mentioned_waves
- metadata
- parts
@@ -8376,6 +8408,10 @@ components:
type: array
items:
$ref: "#/components/schemas/ApiDropMentionedUser"
+ mentioned_groups:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiDropGroupMention"
mentioned_waves:
type: array
items:
@@ -8507,6 +8543,10 @@ components:
properties:
curation_id:
type: string
+ ApiDropGroupMention:
+ type: string
+ enum:
+ - ALL
ApiDropId:
type: object
required:
@@ -8791,6 +8831,7 @@ components:
- updated_at
- referenced_nfts
- mentioned_users
+ - mentioned_groups
- mentioned_waves
- metadata
- parts
@@ -8860,6 +8901,10 @@ components:
type: array
items:
$ref: "#/components/schemas/ApiDropMentionedUser"
+ mentioned_groups:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiDropGroupMention"
mentioned_waves:
type: array
items:
@@ -9338,6 +9383,10 @@ components:
type: object
required:
- id
+ - wave_id
+ - wave_name
+ - author
+ - created_at
- serial_no
- drop_type
- part_1_text
@@ -9348,6 +9397,15 @@ components:
properties:
id:
type: string
+ wave_id:
+ type: string
+ wave_name:
+ type: string
+ author:
+ type: string
+ created_at:
+ type: number
+ format: int64
serial_no:
description: Sequence number of the drop in Seize
type: number
@@ -11126,9 +11184,6 @@ components:
type: object
allOf:
- $ref: "#/components/schemas/ApiCreateWaveDropRequest"
- properties:
- mentions_all:
- type: boolean
ApiUpdateProxyActionRequest:
type: object
properties:
@@ -11168,6 +11223,15 @@ components:
Decisions before this time will not be made. Should not overlap with
other pauses. Needs to be after start_time, after now and after
wave.next_decision_time
+ ApiUpdateWaveNotificationPreferencesRequest:
+ type: object
+ properties:
+ subscribed:
+ type: boolean
+ enabled_group_notifications:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiDropGroupMention"
ApiUpdateWaveParticipationConfig:
type: object
required:
@@ -11194,7 +11258,7 @@ components:
$ref: "#/components/schemas/ApiWaveParticipationRequirement"
required_metadata:
description: |
- The metadata that must be provided by the participant.
+ The metadata that must be provided by the participant.
Empty array if nothing is required.
type: array
items:
@@ -11463,7 +11527,7 @@ components:
type: string
ApiWaveCreditScope:
description: |
- The scope of the credit.
+ The scope of the credit.
* WAVE - Credit is spendable across all drops in wave.
enum:
- WAVE
@@ -11799,6 +11863,18 @@ components:
type: boolean
identity_wave:
type: boolean
+ ApiWaveNotificationPreferences:
+ type: object
+ required:
+ - subscribed
+ - enabled_group_notifications
+ properties:
+ subscribed:
+ type: boolean
+ enabled_group_notifications:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiDropGroupMention"
ApiWaveOutcome:
type: object
required:
@@ -11896,7 +11972,7 @@ components:
nullable: true
required_metadata:
description: |
- The metadata that must be provided by the participant.
+ The metadata that must be provided by the participant.
Empty array if nothing is required.
type: array
items: