Skip to content

fix: voice chat tip frequency - #9742

Merged
lorux0 merged 5 commits into
devfrom
fix/voice-chat-tip-frequency
Aug 17, 2026
Merged

fix: voice chat tip frequency#9742
lorux0 merged 5 commits into
devfrom
fix/voice-chat-tip-frequency

Conversation

@lorux0

@lorux0 lorux0 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What does this PR change?

Changes when the Nearby Voice Chat intro tip shows up. Closes #9708 and #9696, supersedes #9698.

Today it hits brand new users on their very first session, and comes back every launch until they click one of its buttons. Now it waits a few sessions, shows at most twice, skips anyone who already uses Nearby Voice Chat, and hides when you open another panel instead of sitting on top of it.

It also gets its own feature flag, alfa-nearby-voice-chat-tip, so it can be turned off remotely. How often it shows and how many times are set in the flag's config variant:

{
    "showEverySessions": 5,
    "maxTimesShown": 2
}

With those values a new user sees it on session 5 and again on session 10.

Test Instructions

Test steps for existing account

  1. Launch, the tip should not appear if you already tried the voice chat before
  2. Launch few times until the tip appears
  3. Close it
  4. Launch few times again until the tip re-appears after 5 sessions
  5. Click Try it now — tip closes, Nearby Voice widget opens (unchanged).
  6. Launch few times again, the tip should not re-open again

Test Steps for new account

Steps (fresh account):

metaforge account create --clear
metaforge explorer run XXXX  # ← replace with this PR number
  1. Delete the file at ~/Library/Application Support/Decentraland/Explorer/userdata_{n}.json
  2. Repeat same steps

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (docs/feature-flags.md)

@lorux0
lorux0 requested review from a team as code owners August 13, 2026 19:42
@lorux0
lorux0 marked this pull request as draft August 13, 2026 19:42
@github-actions
github-actions Bot requested a review from DafGreco August 13, 2026 19:42
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below.

Name Link
Commit 8ddb464
Logs https://github.com/decentraland/unity-explorer/actions/runs/31812391840
Download Windows https://github.com/decentraland/unity-explorer/suites/86304922552/artifacts/9224678649
Download Windows S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/fix/voice-chat-tip-frequency/pr-25146-8ddb464/Decentraland_windows64.zip
Download Mac https://github.com/decentraland/unity-explorer/suites/86304922552/artifacts/9224647180
Download Mac S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/fix/voice-chat-tip-frequency/pr-25146-8ddb464/Decentraland_macos.zip
Built on 2026-08-14T15:35:45Z

Lint

Warnings count reduced: 13156 => 13154

Warnings/errors in files changed by this PR (11)
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs:186  CSharpWarnings::CS0618  CS0618: Constant 'DCL.FeatureFlags.FeatureFlagsStrings.GPUI_ENABLED' is obsolete: 'GPU Instancer Pro terrain is no longer optional so the flag is not needed'
Assets/DCL/Prefs/DCLPlayerPrefs.cs:47  CSharpWarnings::CS8618  Non-nullable field 'dclPrefs' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/PluginSystem/Global/VoiceChatPlugin.cs:88  CSharpWarnings::CS8618  Non-nullable field 'voiceChatConfiguration' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Prefs/DCLPlayerPrefs.cs:30  MemberHidesStaticFromOuterClass  Method 'bool GetBool(string, bool)' hides method from outer class
Assets/DCL/Prefs/DCLPlayerPrefs.cs:31  MemberHidesStaticFromOuterClass  Method 'bool HasKey(string)' hides method from outer class
Assets/DCL/Prefs/DCLPlayerPrefs.cs:29  MemberHidesStaticFromOuterClass  Method 'float GetFloat(string, float)' hides method from outer class
Assets/DCL/Prefs/DCLPlayerPrefs.cs:28  MemberHidesStaticFromOuterClass  Method 'int GetInt(string, int)' hides method from outer class
Assets/DCL/Prefs/DCLPlayerPrefs.cs:27  MemberHidesStaticFromOuterClass  Method 'string GetString(string, string)' hides method from outer class
Assets/DCL/Prefs/DCLPlayerPrefs.cs:33  MemberHidesStaticFromOuterClass  Method 'void DeleteAll()' hides method from outer class
Assets/DCL/Prefs/DCLPlayerPrefs.cs:34  MemberHidesStaticFromOuterClass  Method 'void Save()' hides method from outer class
Assets/DCL/Prefs/DCLPlayerPrefs.cs:35  MemberHidesStaticFromOuterClass  Method 'void SaveSync()' hides method from outer class

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 25008 0 13
PlayMode ✅ Passed 236 0 36

@decentraland-bot
decentraland-bot self-requested a review August 13, 2026 19:42

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review — fix: voice chat tip frequency

STEP 2 — Root-cause check: PASS ✅

The problem is that the Nearby Voice Chat intro tip shows on the very first session and reappears every launch until dismissed. The root cause was the original NearbyVoiceTipFlow class, which had no scheduling logic — it simply showed on every launch gated only by a single NEARBY_VOICE_TIP_DISMISSED pref.

The diff fixes the actual cause by replacing the inline flow with:

  1. A NearbyVoiceTipSchedule struct with configurable frequency and max-display cap
  2. A NearbyVoiceTipController that manages schedule checking, panel-covering awareness, voice-use detection, and retirement
  3. A feature flag kill-switch (alfa-nearby-voice-chat-tip)

STEP 3 — Design & integration: PASS ✅

New long-lived units introduced:

  • NearbyVoiceTipController — manages the tip view lifecycle (schedule, show/hide, retire)
  • NearbyVoiceTipSchedule — pure value-type (readonly struct) for schedule logic, no lifecycle

Owner search for NearbyVoiceTipController:

  • The tip view (NearbyVoiceTipView) is constructed in the sidebar and injected through VoiceChatPlugin.
  • The lifecycle owner is VoiceChatPlugin, which creates the controller and adds it to pluginScope for disposal — matching the pattern used by NearbyVoiceChatButtonController (line 253) and NearbyVoiceWidgetController (line 256).
  • The old tip logic was an inline static method RunNearbyVoiceTipAsync + nested NearbyVoiceTipFlow within VoiceChatPlugin.
  • Extraction is justified: the controller holds its own subscription state (event bus for MVC view open/close, voice state subscription, CTS management, prefs tracking). Inlining this into the plugin would bloat a class that already creates ~15 objects. The pattern is consistent with existing nearby-voice controllers in the same namespace.

Teardown / consumption trace — all subscriptions have matching teardowns:

  • stateSubscriptionDispose() line 63 ✅
  • scope (event bus subscriptions) → Dispose() line 64 ✅
  • cts (CancellationTokenSource) → SafeCancelAndDispose() in Dispose() (line 61) and Retire() (line 143) ✅
  • The controller itself → added to pluginScope in VoiceChatPlugin (line 263) ✅

STEP 4 — Member audit: PASS ✅

  • NearbyVoiceTipSchedule.ShowEverySessions (public readonly field) — consumed by ShouldShow internally and by the clamping assertion in ClampANonPositivePeriodSoTheTipIsNotDueEveryLaunch test. Legitimate public exposure for testability.
  • NearbyVoiceTipSchedule.MaxTimesShown (public readonly field) — symmetric with ShowEverySessions; struct fields are value-copied so exposure is harmless.
  • NearbyVoiceTipSchedule.ShouldShow(...) — core logic, 11 test cases covering thresholds, caps, returning users, voice-use suppression, custom frequencies, disabled schedule, and degenerate input.
  • NearbyVoiceTipSchedule.FromFeatureFlags(...) — factory method, single consumer (VoiceChatPlugin line 260). Standard factory pattern.
  • NearbyVoiceTipSchedule.Disabled — used in VoiceChatPlugin (line 261) and tested in NeverShowWhenDisabled. Two consumers.

No single-use accessor anti-patterns. No absent ≠ false/null conflation.

STEP 5 — Line-level review

See inline comments below.

STEP 6 — Complexity assessment

COMPLEX — introduces new async flow with CTS management, event bus subscriptions, feature flag integration, and plugin wiring changes. Touches 7+ files with ~450 lines of changes.

STEP 7 — QA assessment

QA_REQUIRED: YES — modifies runtime UI code that controls when and how often the voice chat tip is shown to users.

STEP 8 — Non-blocking warnings

None. Main scene not modified.

Security review

No security issues found. No hardcoded secrets, no auth/authz changes, no sensitive data exposure. Feature flag payload uses nullable types with defaults and input validation (Math.Max).

Consumer impact

No public API surface is modified. All new types are internal to the VoiceChat subsystem. New FeatureId.NearbyVoiceChatTip enum value is additive. No breaking changes.


REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Introduces new controller with async/CTS management, event bus subscriptions, feature flag integration, and plugin wiring changes across the VoiceChat subsystem.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Jarvis reviewed this PR and found no blocking issues, but assessed it as complex — human DEV review is still required before merging.

@decentraland-bot

This comment has been minimized.

@lorux0
lorux0 marked this pull request as ready for review August 13, 2026 22:23
@decentraland-bot
decentraland-bot self-requested a review August 13, 2026 22:23

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: fix: voice chat tip frequency

STEP 2 — Root-cause check: ✅ PASS

The original problem was that the Nearby Voice Chat intro tip appeared on every launch until explicitly dismissed — hitting brand-new users on their very first session. The diff addresses the root cause by introducing proper scheduling logic (frequency interval, display cap, user-discovery detection) rather than working around a symptom.

STEP 3 — Design & integration: ✅ PASS

Owner search for NearbyVoiceTipController:

Question Answer
What does it manage? NearbyVoiceTipView (intro tip UI), display scheduling, and nearby-voice-used tracking
Who already owns creation/disposal? VoiceChatPlugin — creates all nearby voice chat controllers in InitializeAsync() and disposes via pluginScope
Files checked VoiceChatPlugin.cs, NearbyVoiceChatButtonController, NearbyVoiceWidgetController, NearbyVoiceChatSuppressor, VoiceChatPanelPresenter
Could this live in the existing owner? It does — the controller is instantiated inside VoiceChatPlugin.InitializeAsync() and added to pluginScope (line 263), exactly like the other 17 pluginScope.Add() calls in the same file

The old code was a static nested class NearbyVoiceTipFlow with a detached async and a separate CancellationTokenSource field on the plugin. The new design moves this into a self-contained IDisposable controller — cleaner ownership, same lifecycle home.

Schedule/Controller separation is justified:

  • NearbyVoiceTipSchedule is a pure readonly struct with no side effects — independently testable (13 test cases)
  • NearbyVoiceTipController manages 3 disposal chains (CTS, event scope, state subscription) and interactive state
  • Neither is a bridge/wrapper; each has distinct responsibility

Teardown / consumption trace — all resources matched:

Resource Opened Closed Line
CancellationTokenSource Constructor (L56) Retire() (L158) + Dispose() (L62) via SafeCancelAndDispose()
EventSubscriptionScope (MVCViewOpen/Close) Constructor (L53–54) Dispose()scope.Dispose() (L65)
DisposableSubscription<NearbyVoiceChatState> Constructor (L47) Dispose()stateSubscription.Dispose() (L64)

The stateSubscription intentionally outlives the tip display — it tracks nearby-voice-used discovery even when the tip is not scheduled, so enabling the flag later doesn't resurface it for users who already found the feature on their own. This is correct.

STEP 4 — Member audit: ✅ PASS

Member Consumers Verdict
ShowEverySessions ShouldShow(), test ClampANonPositivePeriodSoTheTipIsNotDueEveryLaunch Public readonly field on readonly struct — legitimate
MaxTimesShown ShouldShow() Same — read by core logic
ShouldShow() NearbyVoiceTipController.IsDue(), 13 test cases Pure function, well-tested
FromFeatureFlags() VoiceChatPlugin Factory appropriately located on the struct
Disabled VoiceChatPlugin, test Sentinel value (maxTimesShown=0) — correct

No single-use intermediates that should be merged. No absent-means-false predicates.

STEP 5 — Line-level review: ✅ No blocking issues

Checked and verified:

  • Naming: PascalCase for types/methods/properties, camelCase for locals/params — consistent ✅
  • Async (§9): ShowWhenReadyAsync is async UniTaskVoid with .Forget(), CTS-lifecycled, catches OperationCanceledException — follows existing pattern (NearbyMuteService.LoadAsync(ct).Forget() in same file) ✅
  • CTS management: SafeCancelAndDispose() extension handles null correctly (null-conditional ?.Cancel() / ?.Dispose()) ✅
  • Event pattern: MVCViewOpenEvent/MVCViewClosedEvent via ChatSharedAreaEventBus + EventSubscriptionScope — matches VoiceChatPanelPresenter
  • DTO (§11): ConfigDto struct with // ReSharper disable InconsistentNaming, nullable int? for optional fields — correct ✅
  • Input validation: Math.Max(1, showEverySessions) prevents zero/negative period; Math.Max(0, maxTimesShown) prevents negative cap ✅
  • Feature flag: Kill-switch pattern (off until explicitly enabled) with FeatureId.NearbyVoiceChatTip = 74 — correct next enum value after 73 ✅
  • Unused import cleanup: Removed LiveKit.Rooms.Streaming.Audio and Utility.Multithreading — good hygiene ✅
  • Nullability: CancellationTokenSource? properly annotated; no unjustified null-forgiving operators ✅
  • coveringViews counter: Math.Max(0, coveringViews - 1) prevents underflow from mismatched open/close events ✅
  • Test coverage: 13 test cases covering thresholds, caps, returning users, custom frequencies, disabled schedule, and degenerate input — thorough for the pure logic ✅
  • Documentation: docs/feature-flags.md update is clear and complete ✅
  • Unrelated .meta files: EventsStateServiceLookupPerformanceTest.cs.meta and PlacesStateServiceLookupPerformanceTest.cs.meta are missing-meta fixes — harmless ✅

STEP 6 — Complexity

COMPLEX — introduces new controller + schedule struct with async lifecycle, feature flag integration, event-driven UI state management, and unit tests across 14 files.

STEP 7 — QA assessment

QA_REQUIRED: YES — changes user-facing behavior (when and how often the voice chat intro tip appears), modifies runtime code.

STEP 8 — Non-blocking warnings

None. Main scene not modified.

Security review

No security issues found. The code reads server-side feature flag configuration via the existing TryGetJsonPayload() API with type-safe deserialization and default fallbacks. PlayerPrefs stores only non-sensitive display metrics (count, launch number, boolean flags). No secrets, injection vectors, or auth concerns.


REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: New controller + schedule struct with async lifecycle, feature flag integration, event-driven UI state, and tests across 14 files
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

@decentraland-bot

This comment has been minimized.

# Conflicts:
#	Explorer/Assets/DCL/Tests/PlayMode/PerformanceTests/EventsStateServiceLookupPerformanceTest.cs.meta
#	Explorer/Assets/DCL/Tests/PlayMode/PerformanceTests/PlacesStateServiceLookupPerformanceTest.cs.meta
@decentraland-bot

This comment has been minimized.

@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9742, run #31815319012

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions per run — the average number of exceptions in a run's log; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2287 (×3) 2373 (×3)
CPU average 39.2 ms (36.8–39.3) 37.7 ms (37.6–38.0) -1.5 ms ⚪ within noise
CPU 1% worst 378.9 ms (366.5–380.5) 290.5 ms (282.3–306.0) -88.3 ms 🟢 23% faster
CPU 0.1% worst 395.9 ms (389.0–396.8) 302.3 ms (300.2–320.3) -93.6 ms 🟢 24% faster
GPU average 9.5 ms (9.4–9.6) 8.3 ms (8.2–8.3) -1.2 ms 🟢 13% faster
GPU 1% worst 40.2 ms (39.7–40.3) 19.4 ms (19.3–19.5) -20.8 ms 🟢 52% faster
GPU 0.1% worst 48.7 ms (44.9–49.0) 20.7 ms (20.6–21.0) -28.0 ms 🟢 57% faster
Exceptions per run 66 66 0 ⚪ none new
Exception breakdown
Exception Baseline (3 runs) Change (3 runs)
[UI] DllNotFoundException 192 192
[ENGINE] NullReferenceException 3 3
[ENGINE] ObjectDisposedException 3 3

Apple M1

Metric Baseline Change Δ Result
Samples 4021 (×3) 3954 (×3)
CPU average 22.3 ms (21.7–23.2) 22.6 ms (22.6–23.6) 0.4 ms ⚪ within noise
CPU 1% worst 230.5 ms (224.7–232.1) 217.3 ms (217.2–222.0) -13.3 ms 🟢 6% faster
CPU 0.1% worst 238.5 ms (234.0–239.6) 221.2 ms (220.1–240.1) -17.3 ms ⚪ within noise
GPU average 6.9 ms (2.7–7.2) 5.9 ms (3.8–11.4) -1.0 ms ⚪ within noise
GPU 1% worst 35.3 ms (33.7–37.2) 36.3 ms (35.5–38.1) 1.0 ms ⚪ within noise
GPU 0.1% worst 36.3 ms (35.1–38.0) 38.0 ms (36.6–39.8) 1.7 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

@davidejensen davidejensen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice!

Comment thread Explorer/Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs
@Ludmilafantaniella

Copy link
Copy Markdown
Contributor

✅ Approve by QA

Tested on Windows and Mac.

Existing account

  • Tip does not appear for an account that already used Nearby Voice Chat before

New account

  • Deleted userdata_n.json once before creating the new account (per Nico's guidance) - tip correctly appeared after 5 sessions, not before
  • Closed the tip, launched a few more times - tip re-appeared after 5 more sessions (2nd of maxTimesShown: 2)
  • Clicked "Try it now" - tip closes, Nearby Voice widget opens correctly
  • Launched a few more times after that - tip does not reappear (max times shown respected)

No blockers on either platform.

pass-9742.mp4

@lorux0
lorux0 enabled auto-merge (squash) August 17, 2026 16:23
@anicalbano
anicalbano self-requested a review August 17, 2026 16:39
@lorux0
lorux0 merged commit 3342506 into dev Aug 17, 2026
36 of 43 checks passed
@lorux0
lorux0 deleted the fix/voice-chat-tip-frequency branch August 17, 2026 16:48
@Ludmilafantaniella

Copy link
Copy Markdown
Contributor

✅ Approve by QA

Tested on Windows and Mac.

Existing account

  • Tip does not appear for an account that already used Nearby Voice Chat before

New account

  • Deleted userdata_n.json once before creating the new account (per Nico's guidance) - tip correctly appeared after 5 sessions, not before
  • Closed the tip, launched a few more times - tip re-appeared after 5 more sessions (2nd of maxTimesShown: 2)
  • Clicked "Try it now" - tip closes, Nearby Voice widget opens correctly
  • Launched a few more times after that - tip does not reappear (max times shown respected)

No blockers on either platform.

9742-evi.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nearby Voice Chat tip: show based on session count instead of first login

6 participants