PageSourceSearch

https://clawchats.app/assets/CreateChannelModal-DjMwHabC.js

js clawchats.app collected 2026-10-06 08:55:20 UTC 16,663 bytes, 405 lines download raw bytes

1!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"95747397c3502edd74658aeda96b061610458195"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="82d39651-0940-4d8b-9909-57c58560fb26",e._sentryDebugIdIdentifier="sentry-dbid-82d39651-0940-4d8b-9909-57c58560fb26");}catch(e){}}();import { o as __toESM } from "./chunk-DfjDPQKE.js";
2import { t as require_react } from "./react-DcvxNHSN.js";
3import { i as useTranslation, n as require_jsx_runtime, t as Icon } from "./Icon-CBBfWXwE.js";
4import { K as trackGroupCreate, W as shortHash, ct as trackTxSignSuccess, ft as txErrorCode, s as reportUserOperationError, st as trackTxSignFailure } from "./sentry-D4Q9PZiu.js";
5import { r as ROUTES, s as Button, v as useNavigate } from "./constants-8CpDSLAx.js";
6import { n as useUiStore } from "./ui.store-DGSthQIT.js";
7import { a as getAvatarUrl } from "./identity.store-DGcJ-jrx.js";
8import { X as setCachedGroupKey, l as useConversationsStore, r as useWalletStore } from "./wallet.store-DoPENzLL.js";
9import { D as postSignedPaidGroupKey, H as signPaidGroupKey, O as postSignedPrivateGroupNameAnchor, U as signPrivateGroupNameAnchor, Z as uploadMetaFile, b as getOneChannel, i as createPaidChannel, r as createChannel } from "./talk-CcO-44Eh.js";
10import { a as groupDecryptName, s as isEncrypted } from "./crypto-VkIuMjDu.js";
11import { t as Modal } from "./Modal-DuwC61oU.js";
12import { a as Input } from "./index-DVVuUs55.js";
13import { t as SelectableCard } from "./SelectableCard-B6SAV7IS.js";
14import { n as getGroupNameError, r as getPaidPriceErrorKey, t as GroupAvatarPicker } from "./GroupAvatarPicker-6h04xZ_G.js";
15
16//#region src/features/channel/CreateChannelModal.tsx
17var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
18var import_jsx_runtime = require_jsx_runtime();
19var TYPE_OPTIONS = [
20	{
21		value: "public",
22		icon: "tag",
23		labelKey: "channel.publicGroup",
24		descKey: "channel.publicGroupDesc"
25	},
26	{
27		value: "private",
28		icon: "lock",
29		labelKey: "channel.privateGroup",
30		descKey: "channel.privateGroupDesc"
31	},
32	{
33		value: "paid",
34		icon: "paid",
35		labelKey: "channel.paidGroup",
36		descKey: "channel.paidGroupDesc"
37	}
38];
39var PAID_KEY_UPLOAD_ATTEMPTS = 5;
40var PAID_KEY_UPLOAD_RETRY_MS = 2e3;
41function wait(ms) {
42	return new Promise((resolve) => setTimeout(resolve, ms));
43}
44async function uploadPaidGroupKeyWithRetry(groupId, groupKey) {
45	const signed = await signPaidGroupKey(groupId, groupKey);
46	let lastError;
47	for (let attempt = 0; attempt < PAID_KEY_UPLOAD_ATTEMPTS; attempt++) try {
48		await postSignedPaidGroupKey(groupId, groupKey, signed);
49		return;
50	} catch (err) {
51		lastError = err;
52		if (attempt < PAID_KEY_UPLOAD_ATTEMPTS - 1) await wait(PAID_KEY_UPLOAD_RETRY_MS);
53	}
54	throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("paid group key upload failed");
55}
56var NAME_ANCHOR_UPLOAD_ATTEMPTS = 5;
57var NAME_ANCHOR_UPLOAD_RETRY_MS = 2e3;
58async function uploadPrivateGroupNameAnchorWithRetry(groupId, plaintextName, encryptedName) {
59	const signed = await signPrivateGroupNameAnchor();
60	let lastError;
61	for (let attempt = 0; attempt < NAME_ANCHOR_UPLOAD_ATTEMPTS; attempt++) try {
62		await postSignedPrivateGroupNameAnchor(groupId, plaintextName, encryptedName, signed);
63		return;
64	} catch (err) {
65		lastError = err;
66		if (attempt < NAME_ANCHOR_UPLOAD_ATTEMPTS - 1) await wait(NAME_ANCHOR_UPLOAD_RETRY_MS);
67	}
68	throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("private group name anchor upload failed");
69}
70function resolveVerifiedAnchorPlaintext(encryptedName, passwordKey, expectedPlaintext) {
71	if (!encryptedName || !passwordKey || !expectedPlaintext) return void 0;
72	if (!isEncrypted(encryptedName)) return void 0;
73	const decrypted = groupDecryptName(encryptedName, passwordKey);
74	if (!decrypted || decrypted === encryptedName) return void 0;
75	if (decrypted !== expectedPlaintext) return void 0;
76	return decrypted;
77}
78function CreateChannelModal({ open, onClose }) {
79	const [type, setType] = (0, import_react.useState)("public");
80	const [name, setName] = (0, import_react.useState)("");
81	const [description, setDescription] = (0, import_react.useState)("");
82	const [price, setPrice] = (0, import_react.useState)("");
83	const [loading, setLoading] = (0, import_react.useState)(false);
84	const [nameErrorKey, setNameErrorKey] = (0, import_react.useState)(null);
85	const [avatarFile, setAvatarFile] = (0, import_react.useState)(null);
86	const [avatarPreview, setAvatarPreview] = (0, import_react.useState)(null);
87	const [avatarPickerOpen, setAvatarPickerOpen] = (0, import_react.useState)(false);
88	const navigate = useNavigate();
89	const { fetchConversations, addPendingChannel, removePendingChannel, markJoined } = useConversationsStore();
90	const { address, globalMetaId } = useWalletStore();
91	const { showToast } = useUiStore();
92	const { t } = useTranslation();
93	function handleClose() {
94		if (loading) return;
95		setName("");
96		setDescription("");
97		setPrice("");
98		setType("public");
99		setNameErrorKey(null);
100		setAvatarFile(null);
101		setAvatarPreview(null);
102		setAvatarPickerOpen(false);
103		onClose();
104	}
105	function handleNameChange(value) {
106		setName(value);
107		if (nameErrorKey && value.trim()) setNameErrorKey(null);
108	}
109	async function handleCreate() {
110		if (loading) return;
111		const errorKey = getGroupNameError(name);
112		if (errorKey) {
113			setNameErrorKey(errorKey);
114			return;
115		}
116		if (!address) return;
117		if (type === "paid") {
118			if (getPaidPriceErrorKey(price)) {
119				showToast(t("channel.paidPriceError", { min: 100 }), "error");
120				return;
121			}
122		}
123		setNameErrorKey(null);
124		setLoading(true);
125		try {
126			let groupIcon;
127			if (avatarFile) try {
128				const { txid } = await uploadMetaFile({
129					file: avatarFile,
130					walletAddress: address
131				});
132				groupIcon = `metafile://${txid}i0`;
133			} catch (err) {
134				console.error("[CreateChannel] avatar upload failed; creating without avatar:", err);
135				showToast(t("toast.groupAvatarFailed"), "warning", void 0, err);
136			}
137			let channelId;
138			let txid;
139			let paidGroupKey;
140			let privatePasswordKey;
141			if (type === "paid") {
142				const paidResult = await createPaidChannel({
143					groupName: name.trim(),
144					groupDescription: description.trim() || void 0,
145					groupIcon,
146					price: Number(price)
147				});
148				channelId = paidResult.channelId;
149				txid = paidResult.txid;
150				paidGroupKey = paidResult.groupKey;
151				setCachedGroupKey(channelId, paidGroupKey);
152			} else {
153				const result = await createChannel({
154					groupName: name.trim(),
155					groupNote: "",
156					groupDescription: description.trim() || void 0,
157					groupIcon,
158					walletAddress: address,
159					globalMetaId: globalMetaId || void 0,
160					isPrivate: type === "private"
161				});
162				channelId = result.channelId;
163				txid = result.txid;
164				privatePasswordKey = result.passwordKey;
165			}
166			markJoined(channelId);
167			trackGroupCreate({ visibility: type });
168			trackTxSignSuccess({
169				kind: "group_create",
170				tx_id_hash: shortHash(txid)
171			});
172			addPendingChannel(channelId, {
173				id: channelId,
174				type: type === "private" ? "private_group" : type === "paid" ? "paid_group" : "public_group",
175				name: name.trim(),
176				avatarUrl: groupIcon ? getAvatarUrl(groupIcon) : void 0,
177				unreadCount: 0,
178				hasMention: false,
179				isEncrypted: type === "private" || type === "paid",
180				memberCount: 1
181			});
182			handleClose();
183			navigate(ROUTES.chatGroup(channelId));
184			(async () => {
185				for (let i = 0; i < 15; i++) {
186					await new Promise((r) => setTimeout(r, 2e3));
187					try {
188						const channel = await getOneChannel(channelId);
189						if (channel) {
190							if (type === "paid" && paidGroupKey) try {
191								await uploadPaidGroupKeyWithRetry(channelId, paidGroupKey);
192							} catch (e) {
193								console.warn("[CreateChannel] paid key upload failed:", e);
194								showToast(t("toast.paidGroupKeyUploadFailed"), "error", void 0, e);
195								reportUserOperationError(e, {
196									area: "channel",
197									action: "set_paid_group_key",
198									tags: { channel_id: channelId }
199								});
200							}
201							if (type === "private" && channel.roomName) {
202								const verifiedPlaintext = resolveVerifiedAnchorPlaintext(channel.roomName, privatePasswordKey ?? "", name.trim());
203								if (verifiedPlaintext) try {
204									await uploadPrivateGroupNameAnchorWithRetry(channelId, verifiedPlaintext, channel.roomName);
205								} catch (e) {
206									console.warn("[CreateChannel] name anchor upload failed:", e);
207									showToast(t("toast.privateGroupNameAnchorUploadFailed"), "error", void 0, e);
208									reportUserOperationError(e, {
209										area: "channel",
210										action: "set_private_group_name_anchor",
211										tags: { channel_id: channelId }
212									});
213								}
214								else console.warn("[CreateChannel] skipping name anchor: creation name did not verify against on-chain ciphertext");
215							}
216							removePendingChannel(channelId);
217							if (globalMetaId) fetchConversations(globalMetaId).catch((err) => {
218								console.warn("[CreateChannel] post-creation conversations refresh failed:", err);
219							});
220							return;
221						}
222					} catch {}
223				}
224				console.warn("[CreateChannel] channel did not appear in indexer after polling:", channelId);
225			})().catch((err) => {
226				console.warn("[CreateChannel] confirmation polling loop crashed:", err);
227			});
228		} catch (err) {
229			trackTxSignFailure({
230				kind: "group_create",
231				error_code: txErrorCode(err)
232			});
233			showToast(t("toast.groupCreateFailed"), "error", void 0, err);
234			reportUserOperationError(err, {
235				area: "channel",
236				action: "create_channel",
237				tags: { channel_type: type }
238			});
239		} finally {
240			setLoading(false);
241		}
242	}
243	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Modal, {
244		open,
245		onClose: handleClose,
246		title: t("channel.createGroup"),
247		children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
248			className: "space-y-5",
249			children: [
250				/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
251					className: "grid grid-cols-3 gap-2",
252					children: TYPE_OPTIONS.map(({ value, icon, labelKey, descKey }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectableCard, {
253						icon,
254						label: t(labelKey),
255						description: t(descKey),
256						orientation: "column",
257						active: type === value,
258						onClick: () => setType(value)
259					}, value))
260				}),
261				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
262					className: "flex items-center gap-3",
263					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
264						type: "button",
265						onClick: () => setAvatarPickerOpen(true),
266						disabled: loading,
267						"aria-label": t("groupAvatar.pickAriaLabel"),
268						className: "relative inline-flex items-center justify-center w-[72px] h-[72px] rounded-full bg-surface-container-high border border-dashed border-outline-variant overflow-hidden hover:border-primary-text transition-colors duration-100 disabled:opacity-50",
269						children: [avatarPreview ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
270							src: avatarPreview,
271							alt: "",
272							className: "w-full h-full object-cover"
273						}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, {
274							name: "add_a_photo",
275							size: 28,
276							className: "text-on-surface-variant"
277						}), avatarPreview && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
278							"aria-hidden": "true",
279							className: "absolute bottom-0 right-0 inline-flex items-center justify-center w-[22px] h-[22px] rounded-full bg-primary text-on-primary ring-2 ring-surface-bright",
280							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, {
281								name: "edit",
282								size: 12
283							})
284						})]
285					}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
286						className: "flex-1 min-w-0",
287						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
288							className: "text-label-md font-medium text-on-surface leading-none mb-1",
289							children: t("groupAvatar.createLabel")
290						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
291							className: "text-label-sm text-on-surface-variant",
292							children: avatarPreview ? t("groupAvatar.createPickedHint") : t("groupAvatar.createOptionalHint")
293						})]
294					})]
295				}),
296				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
297					label: t("channel.groupName"),
298					placeholder: t("channel.groupNamePlaceholder"),
299					value: name,
300					onChange: (e) => handleNameChange(e.target.value),
301					disabled: loading,
302					autoFocus: true,
303					maxLength: 60,
304					error: nameErrorKey ? t(nameErrorKey) : void 0,
305					"aria-invalid": nameErrorKey ? true : void 0,
306					onKeyDown: (e) => {
307						if (e.key === "Enter" && !e.nativeEvent.isComposing) {
308							e.preventDefault();
309							handleCreate();
310						}
311					}
312				}),
313				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
314					className: "flex flex-col gap-1",
315					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
316						className: "text-label-md text-on-surface-variant",
317						children: t("channel.groupDescription")
318					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", {
319						value: description,
320						onChange: (e) => setDescription(e.target.value),
321						maxLength: 200,
322						rows: 2,
323						disabled: loading,
324						placeholder: t("channel.groupDescriptionPlaceholder"),
325						className: "bg-transparent border border-outline-variant focus:border-primary-text rounded-lg px-3 py-2 text-body-md text-on-surface resize-none outline-none placeholder:text-on-surface-variant/50 transition-colors duration-150"
326					})]
327				}),
328				type === "paid" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
329					className: "flex flex-col gap-1",
330					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
331						label: t("channel.paidPriceLabel"),
332						type: "number",
333						inputMode: "numeric",
334						min: 100,
335						step: 1,
336						value: price,
337						onChange: (e) => setPrice(e.target.value.replace(/[^\d]/g, "")),
338						disabled: loading
339					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
340						className: "text-label-sm text-on-surface-variant",
341						children: t("channel.paidPriceHint", { min: 100 })
342					})]
343				}),
344				type === "paid" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
345					className: "flex items-start gap-2.5 px-3.5 py-3 rounded-xl bg-primary/8 border border-primary-text/20",
346					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, {
347						name: "paid",
348						size: 15,
349						className: "text-primary-text flex-shrink-0 mt-0.5"
350					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
351						className: "text-label-md text-on-surface-variant leading-relaxed",
352						children: t("channel.paidGroupInfo")
353					})]
354				}),
355				type === "private" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
356					className: "flex items-start gap-2.5 px-3.5 py-3 rounded-xl bg-primary/8 border border-primary-text/20",
357					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, {
358						name: "lock",
359						size: 15,
360						className: "text-primary-text flex-shrink-0 mt-0.5"
361					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
362						className: "text-label-md text-on-surface-variant leading-relaxed",
363						children: t("channel.privateGroupInfo")
364					})]
365				}),
366				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
367					className: "flex gap-3 pt-1",
368					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
369						variant: "secondary",
370						onClick: handleClose,
371						disabled: loading,
372						className: "flex-1",
373						children: t("common.cancel")
374					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
375						variant: "primary",
376						onClick: handleCreate,
377						loading,
378						disabled: loading || !name.trim() || type === "paid" && getPaidPriceErrorKey(price) !== null,
379						className: "flex-1",
380						children: t("channel.createGroup")
381					})]
382				})
383			]
384		})
385	}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GroupAvatarPicker, {
386		open: avatarPickerOpen,
387		onClose: () => setAvatarPickerOpen(false),
388		onConfirm: async (file) => {
389			setAvatarFile(file);
390			const reader = new FileReader();
391			await new Promise((resolve) => {
392				reader.onload = () => {
393					setAvatarPreview(reader.result);
394					resolve();
395				};
396				reader.onerror = () => resolve();
397				reader.readAsDataURL(file);
398			});
399		}
400	})] });
401}
402
403//#endregion
404export { CreateChannelModal, uploadPaidGroupKeyWithRetry };
405//# sourceMappingURL=CreateChannelModal-DjMwHabC.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.