Files
status-app/src/app/global/feature_flags.nim
T
Jonathan Rainville 4bfbcec0f2 feat(threads): implement threads feature POC
Fixes #21257

Implements threaded messages in community channels with UI integration, gated behind a feature flag

### Features
- **Create thread** — right-click any message → "Create Thread" opens a thread rooted at that message as a sub-chat
- **Open thread** — if a thread already exists the action shows "Open Thread" instead
- **Auto-display threads** — threads are automatically listed in chat when loaded, positioned directly below their parent chat
- **Parallel to replies** — threads are independent of the existing reply feature

### Architecture
- Reuses `sendMessage` with a `threadId` parameter; no duplicate send variants
- Thread messages are lazy-loaded per chat on first open and cached in memory
- `hasThread` model role derived from the cache drives the Open/Create label
- `threadId == parentMessageId` (validated against status-go behaviour), so no separate ID mapping is needed
- Unified `MessageByChatID` API across all layers (Go → Nim → RPC) with optional `threadId` parameter
- Thread message routing fixed: base chat only shows messages with `threadId IS NULL OR threadId = ''`
- `isThread` property on ChatItem enables proper list positioning and future UI styling

### Implementation Details
- Go layer: `MessageByChatID(chatID, threadID, ...)` handles both base and thread message fetching
- Nim layer: Single `fetchMessages` function with optional threadId parameter
- UI layer: Threads auto-populate in chat list via `SIGNAL_CHAT_THREADS_LOADED` signal
- Threading: No separate reactions fetch for thread messages (only base chat reactions)
2026-08-26 11:43:28 -04:00

221 lines
7.8 KiB
Nim

import nimqml
import os, macros, strutils
proc boolToEnv*(defaultValue: bool): string =
return if defaultValue: "1" else: "0"
# The `featureFlag` macro defines a feature flag based on an environment variable.
# If the `buildFlag` variable is true, the feature flag is determined at compile time.
# Otherwise, it is determined at runtime.
macro featureFlag(name: string, defaultValue: bool, buildFlag: static bool = false): untyped =
let flagName = newIdentNode(name.strVal)
if `buildFlag`:
return quote do:
const `flagName`* = static(getEnv("FLAG_" & `name`.toUpper, boolToEnv(`defaultValue`)) != "0")
else:
return quote do:
let `flagName`* = getEnv("FLAG_" & `name`.toUpper, boolToEnv(`defaultValue`)) != "0"
const DEFAULT_FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED = true
const DEFAULT_FLAG_PAYMENT_REQUEST_ENABLED = true
const DEFAULT_FLAG_MARKET_ENABLED = true
const DEFAULT_FLAG_HOMEPAGE_ENABLED = true
const DEFAULT_FLAG_LOCAL_BACKUP_ENABLED = true
const DEFAULT_FLAG_PRIVACY_MODE_FEATURE_ENABLED = true
const DEFAULT_FLAG_MESSAGE_LINK_SHARING_ENABLED = true
const DEFAULT_FLAG_STATUS_SUPPORT_BOT_ENABLED = true
const DEFAULT_FLAG_THREADS_ENABLED = false
# Compile time feature flags
const DEFAULT_FLAG_DAPPS_ENABLED = true
const DEFAULT_FLAG_BROWSER_ENABLED = true
const DEFAULT_FLAG_CONNECTOR_ENABLED = true
const DEFAULT_FLAG_KEYCARD_ENABLED = true
const DEFAULT_FLAG_THREADPOOL_ENABLED = true
const DEFAULT_FLAG_SINGLE_STATUS_INSTANCE_ENABLED = true
const DEFAULT_FLAG_BUY_ENABLED = true
const DEFAULT_FLAG_SWAP_ENABLED = true
# Public feature flags
featureFlag("SEND_VIA_PERSONAL_CHAT_ENABLED", DEFAULT_FLAG_SEND_VIA_PERSONAL_CHAT_ENABLED)
featureFlag("PAYMENT_REQUEST_ENABLED", DEFAULT_FLAG_PAYMENT_REQUEST_ENABLED)
featureFlag("MARKET_ENABLED", DEFAULT_FLAG_MARKET_ENABLED)
featureFlag("HOMEPAGE_ENABLED", DEFAULT_FLAG_HOMEPAGE_ENABLED)
featureFlag("LOCAL_BACKUP_ENABLED", DEFAULT_FLAG_LOCAL_BACKUP_ENABLED)
featureFlag("PRIVACY_MODE_FEATURE_ENABLED", DEFAULT_FLAG_PRIVACY_MODE_FEATURE_ENABLED)
featureFlag("MESSAGE_LINK_SHARING_ENABLED", DEFAULT_FLAG_MESSAGE_LINK_SHARING_ENABLED)
featureFlag("STATUS_SUPPORT_BOT_ENABLED", DEFAULT_FLAG_STATUS_SUPPORT_BOT_ENABLED)
featureFlag("THREADS_ENABLED", DEFAULT_FLAG_THREADS_ENABLED)
featureFlag("DAPPS_ENABLED", DEFAULT_FLAG_DAPPS_ENABLED, true)
featureFlag("BROWSER_ENABLED", DEFAULT_FLAG_BROWSER_ENABLED, true)
featureFlag("CONNECTOR_ENABLED", DEFAULT_FLAG_CONNECTOR_ENABLED, true)
featureFlag("KEYCARD_ENABLED", DEFAULT_FLAG_KEYCARD_ENABLED, true)
featureFlag("THREADPOOL_ENABLED", DEFAULT_FLAG_THREADPOOL_ENABLED, true)
featureFlag("SINGLE_STATUS_INSTANCE_ENABLED", DEFAULT_FLAG_SINGLE_STATUS_INSTANCE_ENABLED, true)
featureFlag("BUY_ENABLED", DEFAULT_FLAG_BUY_ENABLED, true)
featureFlag("SWAP_ENABLED", DEFAULT_FLAG_SWAP_ENABLED, true)
# The `featureGuard` macro conditionally replaces the guarded code
# There are two main usages:
# 1. With a statement list:
# featureGuard(FEATURE_FLAG):
# echo "Feature is enabled"
# else:
# echo "Feature is disabled"
# 2. As a pragma:
# proc myProc(): void {.featureGuard(FEATURE_FLAG).} = echo "Feature is enabled"
macro featureGuard*(flag: static bool, n: varargs[untyped]): untyped =
if n.len == 2 and n[1].kind == nnkElse:
if flag:
result = n[0]
else:
result = n[1][0]
else:
if not flag:
if n[0].kind == nnkStmtList:
result = newStmtList()
elif n[0].kind == nnkProcDef:
result = n[0]
result[6] = newStmtList(
newCall(bindSym"echo", newLit("Warning! Calling a disabled feature")),
newTree(nnkDiscardStmt, newEmptyNode())
) # Replace body with `discard`
else:
result = newStmtList()
else:
result = n[0]
QtObject:
type FeatureFlags* = ref object of QObject
dappsEnabled: bool
browserEnabled: bool
swapEnabled: bool
connectorEnabled: bool
sendViaPersonalChatEnabled: bool
paymentRequestEnabled: bool
keycardEnabled: bool
marketEnabled: bool
homePageEnabled: bool
localBackupEnabled: bool
privacyModeFeatureEnabled: bool
messageLinkSharingEnabled: bool
statusSupportBotEnabled: bool
buyEnabled: bool
threadsEnabled: bool
proc setup(self: FeatureFlags) =
self.QObject.setup()
self.dappsEnabled = DAPPS_ENABLED
self.browserEnabled = BROWSER_ENABLED
self.swapEnabled = SWAP_ENABLED
self.connectorEnabled = CONNECTOR_ENABLED
self.sendViaPersonalChatEnabled = SEND_VIA_PERSONAL_CHAT_ENABLED
self.paymentRequestEnabled = PAYMENT_REQUEST_ENABLED
self.keycardEnabled = KEYCARD_ENABLED
self.marketEnabled = MARKET_ENABLED
self.homePageEnabled = HOMEPAGE_ENABLED
self.localBackupEnabled = LOCAL_BACKUP_ENABLED
self.privacyModeFeatureEnabled = PRIVACY_MODE_FEATURE_ENABLED
self.messageLinkSharingEnabled = MESSAGE_LINK_SHARING_ENABLED
self.statusSupportBotEnabled = STATUS_SUPPORT_BOT_ENABLED
self.buyEnabled = BUY_ENABLED
self.threadsEnabled = THREADS_ENABLED
proc newFeatureFlags*(): FeatureFlags =
new(result)
result.setup()
proc delete*(self: FeatureFlags) =
self.QObject.delete()
proc getDappsEnabled*(self: FeatureFlags): bool {.slot.} =
return self.dappsEnabled
QtProperty[bool] dappsEnabled:
read = getDappsEnabled
proc getBrowserEnabled*(self: FeatureFlags): bool {.slot.} =
return self.browserEnabled
QtProperty[bool] browserEnabled:
read = getBrowserEnabled
proc getSwapEnabled*(self: FeatureFlags): bool {.slot.} =
return self.swapEnabled
QtProperty[bool] swapEnabled:
read = getSwapEnabled
proc getConnectorEnabled*(self: FeatureFlags): bool {.slot.} =
return self.connectorEnabled
QtProperty[bool] connectorEnabled:
read = getConnectorEnabled
proc getSendViaPersonalChatEnabled*(self: FeatureFlags): bool {.slot.} =
return self.sendViaPersonalChatEnabled
QtProperty[bool] sendViaPersonalChatEnabled:
read = getSendViaPersonalChatEnabled
proc getPaymentRequestEnabled*(self: FeatureFlags): bool {.slot.} =
return self.paymentRequestEnabled
QtProperty[bool] paymentRequestEnabled:
read = getPaymentRequestEnabled
QtProperty[bool] keycardEnabled:
read = getKeycardEnabled
proc getKeycardEnabled*(self: FeatureFlags): bool {.slot.} =
return self.keycardEnabled
QtProperty[bool] marketEnabled:
read = getMarketEnabled
proc getMarketEnabled*(self: FeatureFlags): bool {.slot.} =
return self.marketEnabled
QtProperty[bool] homePageEnabled:
read = getHomePageEnabled
proc getHomePageEnabled*(self: FeatureFlags): bool {.slot.} =
return self.homePageEnabled
QtProperty[bool] localBackupEnabled:
read = getLocalBackupEnabled
proc getLocalBackupEnabled*(self: FeatureFlags): bool {.slot.} =
return self.localBackupEnabled
QtProperty[bool] privacyModeFeatureEnabled:
read = getPrivacyModeFeatureEnabled
proc getPrivacyModeFeatureEnabled*(self: FeatureFlags): bool {.slot.} =
return self.privacyModeFeatureEnabled
proc getMessageLinkSharingEnabled*(self: FeatureFlags): bool {.slot.} =
return self.messageLinkSharingEnabled
QtProperty[bool] messageLinkSharingEnabled:
read = getMessageLinkSharingEnabled
proc getStatusSupportBotEnabled*(self: FeatureFlags): bool {.slot.} =
return self.statusSupportBotEnabled
QtProperty[bool] statusSupportBotEnabled:
read = getStatusSupportBotEnabled
QtProperty[bool] buyEnabled:
read = getBuyEnabled
proc getBuyEnabled*(self: FeatureFlags): bool {.slot.} =
return self.buyEnabled
QtProperty[bool] threadsEnabled:
read = getThreadsEnabled
proc getThreadsEnabled*(self: FeatureFlags): bool {.slot.} =
return self.threadsEnabled