Skip to content

Commit 04a33b7

Browse files
committed
feat: thread_creation_menu_precreate_channel
adds a new: thread_creation_menu_precreate_channel to create the threads, even when nothing is selected yet.
1 parent ed59cf1 commit 04a33b7

File tree

4 files changed

+165
-7
lines changed

4 files changed

+165
-7
lines changed

core/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,13 @@ class ConfigManager:
151151
"thread_creation_menu_enabled": False,
152152
"thread_creation_menu_options": {}, # main menu options mapping key -> {label, description, emoji, type, callback}
153153
"thread_creation_menu_submenus": {}, # submenu name -> submenu options (same structure as options)
154-
"thread_creation_menu_timeout": 20,
154+
"thread_creation_menu_timeout": 30, # Default interaction timeout for the thread-creation menu (in seconds)
155155
"thread_creation_menu_close_on_timeout": False,
156156
"thread_creation_menu_anonymous_menu": False,
157157
"thread_creation_menu_embed_text": "Please select an option.",
158158
"thread_creation_menu_dropdown_placeholder": "Select an option to contact the staff team.",
159159
"thread_creation_menu_selection_log": True, # log selected option in newly created thread channel
160+
"thread_creation_menu_precreate_channel": False,
160161
}
161162

162163
private_keys = {
@@ -265,6 +266,7 @@ class ConfigManager:
265266
"thread_creation_menu_close_on_timeout",
266267
"thread_creation_menu_anonymous_menu",
267268
"thread_creation_menu_selection_log",
269+
"thread_creation_menu_precreate_channel",
268270
}
269271

270272
enums = {

core/config_help.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@
14221422
]
14231423
},
14241424
"thread_creation_menu_timeout": {
1425-
"default": "20 (seconds)",
1425+
"default": "30 (seconds)",
14261426
"description": "Number of seconds to wait for a user to pick a menu option before timing out.",
14271427
"examples": [
14281428
"`{prefix}threadmenu config timeout 30`"
@@ -1486,5 +1486,18 @@
14861486
"When disabled, the staff will not see an automatic message with the selection details though commands may still act.",
14871487
"See also: `thread_creation_menu_enabled`, `thread_creation_menu_options`."
14881488
]
1489+
},
1490+
"thread_creation_menu_precreate_channel": {
1491+
"default": "No",
1492+
"description": "When enabled, a thread channel is created immediately upon the user's first DM even if the thread creation menu is enabled. The menu is still shown but selection becomes optional and happens after channel creation.",
1493+
"examples": [
1494+
"`{prefix}config set thread_creation_menu_precreate_channel yes`"
1495+
],
1496+
"notes": [
1497+
"If a user never selects an option the thread remains open in the default/main category.",
1498+
"Category-specific option selections will move the channel afterward if a category_id is configured on the option.",
1499+
"Confirmation (`confirm_thread_creation`) is only applied in the deferred mode; enabling precreate bypasses the confirm step for menu flows.",
1500+
"See also: `thread_creation_menu_enabled`, `thread_creation_menu_options`, `confirm_thread_creation`."
1501+
]
14891502
}
14901503
}

core/thread.py

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,9 +2425,10 @@ async def create(
24252425
adv_enabled = self.bot.config.get("thread_creation_menu_enabled") and bool(
24262426
self.bot.config.get("thread_creation_menu_options")
24272427
)
2428-
# Only defer if user initiated (creator is recipient) and not staff contact command
2428+
# Always treat user-initiated DM threads as precreate: create channel immediately in main_category
24292429
user_initiated = (creator is None or creator == recipient) and manual_trigger
2430-
if adv_enabled and user_initiated:
2430+
precreate = adv_enabled and user_initiated # force precreate semantics for user DM origin
2431+
if adv_enabled and user_initiated and not precreate: # condition will never be true now
24312432
# Send menu prompt FIRST, wait for selection, then create channel.
24322433
# Build dummy message for menu DM
24332434
try:
@@ -2688,8 +2689,149 @@ async def on_timeout(self):
26882689
)
26892690
return thread
26902691

2691-
# Regular immediate creation
2692-
self.bot.loop.create_task(thread.setup(creator=creator, category=category, initial_message=message))
2692+
# If menu is enabled but precreate is requested, send the menu DM but do NOT defer creation.
2693+
# Selection becomes optional; thread channel will already be created below.
2694+
if adv_enabled and user_initiated and precreate:
2695+
try:
2696+
embed_text = self.bot.config.get("thread_creation_menu_embed_text")
2697+
placeholder = self.bot.config.get("thread_creation_menu_dropdown_placeholder")
2698+
timeout = int(self.bot.config.get("thread_creation_menu_timeout") or 20)
2699+
except Exception:
2700+
embed_text = "Please select an option."
2701+
placeholder = "Select an option to contact the staff team."
2702+
timeout = 20
2703+
2704+
options = self.bot.config.get("thread_creation_menu_options") or {}
2705+
2706+
class _PrecreateMenuSelect(discord.ui.Select):
2707+
def __init__(self, outer_thread: Thread):
2708+
self.outer_thread = outer_thread
2709+
opts = [
2710+
discord.SelectOption(label=o["label"], description=o["description"], emoji=o["emoji"])
2711+
for o in options.values()
2712+
]
2713+
super().__init__(placeholder=placeholder, min_values=1, max_values=1, options=opts)
2714+
2715+
async def callback(self, interaction: discord.Interaction):
2716+
await interaction.response.defer(ephemeral=False)
2717+
chosen_label = self.values[0]
2718+
key = chosen_label.lower().replace(" ", "_")
2719+
selected = options.get(key)
2720+
self.outer_thread._selected_thread_creation_menu_option = selected
2721+
# Remove the view
2722+
if self.view:
2723+
try:
2724+
await interaction.edit_original_response(view=None)
2725+
except Exception:
2726+
pass
2727+
try:
2728+
self.view.stop()
2729+
except Exception:
2730+
pass
2731+
# Log selection to thread channel if configured
2732+
try:
2733+
await self.outer_thread.wait_until_ready()
2734+
if self.outer_thread.bot.config.get("thread_creation_menu_selection_log"):
2735+
opt = selected or {}
2736+
log_txt = f"Selected menu option: {opt.get('label')} ({opt.get('type')})"
2737+
if opt.get("type") == "command":
2738+
log_txt += f" -> {opt.get('callback')}"
2739+
await self.outer_thread.channel.send(
2740+
embed=discord.Embed(
2741+
description=log_txt, color=self.outer_thread.bot.mod_color
2742+
)
2743+
)
2744+
# If a category_id is set on the option, move the channel accordingly
2745+
try:
2746+
cat_id = selected.get("category_id") if isinstance(selected, dict) else None
2747+
if cat_id:
2748+
guild = self.outer_thread.bot.modmail_guild
2749+
target = guild and guild.get_channel(int(cat_id))
2750+
if isinstance(target, discord.CategoryChannel):
2751+
await self.outer_thread.channel.edit(
2752+
category=target, reason="Menu selection: move to category"
2753+
)
2754+
except Exception:
2755+
pass
2756+
except Exception:
2757+
pass
2758+
# If the option type is command, invoke it now within the created thread
2759+
if selected and selected.get("type") == "command":
2760+
alias = selected.get("callback")
2761+
if alias:
2762+
from discord.ext.commands.view import StringView
2763+
from core.utils import normalize_alias
2764+
2765+
ctxs = []
2766+
for al in normalize_alias(alias):
2767+
view_ = StringView(self.outer_thread.bot.prefix + al)
2768+
synthetic = DummyMessage(copy.copy(message))
2769+
try:
2770+
synthetic.author = (
2771+
self.outer_thread.bot.modmail_guild.me or self.outer_thread.bot.user
2772+
)
2773+
except Exception:
2774+
synthetic.author = self.outer_thread.bot.user
2775+
setattr(synthetic, "_menu_invoked", True)
2776+
ctx_ = commands.Context(
2777+
prefix=self.outer_thread.bot.prefix,
2778+
view=view_,
2779+
bot=self.outer_thread.bot,
2780+
message=synthetic,
2781+
)
2782+
ctx_.thread = self.outer_thread
2783+
discord.utils.find(
2784+
view_.skip_string, await self.outer_thread.bot.get_prefix()
2785+
)
2786+
ctx_.invoked_with = view_.get_word().lower()
2787+
ctx_.command = self.outer_thread.bot.all_commands.get(ctx_.invoked_with)
2788+
setattr(ctx_, "_menu_invoked", True)
2789+
ctxs.append(ctx_)
2790+
for ctx_ in ctxs:
2791+
if ctx_.command:
2792+
old_checks = copy.copy(ctx_.command.checks)
2793+
ctx_.command.checks = [checks.has_permissions(PermissionLevel.INVALID)]
2794+
try:
2795+
await self.outer_thread.bot.invoke(ctx_)
2796+
finally:
2797+
ctx_.command.checks = old_checks
2798+
2799+
class _PrecreateMenuView(discord.ui.View):
2800+
def __init__(self, outer_thread: Thread):
2801+
super().__init__(timeout=timeout)
2802+
self.add_item(_PrecreateMenuSelect(outer_thread))
2803+
2804+
async def on_timeout(self):
2805+
try:
2806+
await menu_msg.edit(content="Menu timed out.", view=None)
2807+
except Exception:
2808+
pass
2809+
try:
2810+
self.stop()
2811+
except Exception:
2812+
pass
2813+
2814+
try:
2815+
embed = discord.Embed(description=embed_text, color=self.bot.mod_color)
2816+
menu_view = _PrecreateMenuView(thread)
2817+
# Send menu DM AFTER channel creation initiation (channel will be created below)
2818+
menu_msg = await recipient.send(embed=embed, view=menu_view)
2819+
try:
2820+
menu_view.message = menu_msg
2821+
except Exception:
2822+
pass
2823+
except Exception:
2824+
logger.debug("Failed to send precreate menu DM; proceeding without menu.")
2825+
2826+
# Regular immediate creation (force main_category for user-initiated menu flows)
2827+
forced_category = None
2828+
if adv_enabled and user_initiated:
2829+
# Always use main_category for initial creation regardless of passed category
2830+
forced_category = self.bot.main_category
2831+
chosen_category = forced_category or category
2832+
self.bot.loop.create_task(
2833+
thread.setup(creator=creator, category=chosen_category, initial_message=message)
2834+
)
26932835
return thread
26942836

26952837
async def find_or_create(self, recipient) -> Thread:

core/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,8 @@ async def callback(self, interaction: discord.Interaction):
632632

633633
class ConfirmThreadCreationView(discord.ui.View):
634634
def __init__(self):
635-
super().__init__(timeout=20)
635+
# Match thread_creation_menu_timeout default (30s) for consistency in UX
636+
super().__init__(timeout=30)
636637
self.value = None
637638

638639

0 commit comments

Comments
 (0)