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.