8.6 KiB
Poke Without Message Design
Date: 2026-06-09 Status: Approved design for implementation Scope: Allow intentional TeamSpeak-compatible pokes without message text while preserving empty-message blocking for normal chat targets.
1. Goal
Chanora should let a user poke another connected client without typing a message. A poke is an attention event, not an empty chat message. The UI should make that distinction explicit so the empty state is intentional, understandable, and safe from accidental spam.
The implementation target is narrow:
- Sending a poke with an empty message is allowed.
- Sending an empty normal chat message remains blocked.
- Incoming and historical empty pokes continue to render as poke events, not blank chat bubbles.
- Existing poke notification behavior remains compatible with message and no-message pokes.
2. Research Summary
TeamSpeak-compatible poke behavior is command-like: the ServerQuery shape is clientpoke clid={clientID} msg={text}, backed by poke permissions such as i_client_poke_power and i_client_needed_poke_power. The product semantics are closer to an attention nudge than to a private text message.
Client behavior and community expectations point to two UX risks:
- The action can be useful without text because the sender often only wants attention.
- The action can be abused as interruption spam, so the UI must keep the action deliberate and preserve existing receiver-side suppression and notification preferences.
The approved product direction is therefore to model no-message poke as a first-class attention event with optional text, rather than as an exception in the normal chat composer.
3. Recommended UX
Poke uses a poke-specific sending surface. The surface may reuse the current chat detail implementation internally, but the user-facing copy and validation must make the target type clear.
Required poke-target behavior:
| Element | Behavior |
|---|---|
| Header | Shows that the current surface is for poking the selected user. |
| Text field | Optional message input. Placeholder should communicate that the message is optional. |
| Primary action | Label is Poke, not Send. Enabled even when the trimmed message is empty. |
| Empty send | Sends an intentional poke with message: ''. |
| Non-empty send | Sends a poke with the typed message. |
| History row | Empty poke renders as an attention event such as Alice poked you, never as a blank message. |
Required non-poke chat behavior:
| Target | Empty text behavior |
|---|---|
| Channel chat | Block send. |
| Server chat | Block send. |
| Private chat | Block send. |
| Any future text-chat target | Block send unless it is explicitly modeled as a poke-like attention event. |
4. Architecture Boundaries
The change should stay inside the existing UI and bridge boundaries:
- Flutter owns presentation, composer validation, button enablement, localization copy, and widget tests.
- Flutter Rust Bridge continues to pass typed
BridgeMessageTargetand message text across the bridge. - Rust Core and Protocol continue to route
MessageTarget::Poke(client_id)through the existing poke send path. - Protocol remains the only layer that knows how
tsclientlibsends a TeamSpeak-compatible poke.
No new protocol concept is required. The existing bridge/protocol model already has BridgeMessageTarget.poke / MessageTarget::Poke(u64) and client.poke(message). The key design change is target-aware composer validation in Flutter.
5. Implementation Design
The implementation should use a target-aware send policy.
For BridgeMessageTarget.poke:
- Do not reject an empty trimmed input.
- Send the original or trimmed message according to the existing chat composer convention. If the current send path trims normal messages before sending, apply the same text normalization before passing the poke message.
- Clear the composer after successful send, including empty-poke sends.
- Preserve existing error and snackbar behavior for failed sends.
For all other BridgeMessageTarget variants:
- Keep the existing empty-trimmed-text guard.
- Keep current button enablement and keyboard submit behavior unless those paths need target-aware adjustment to preserve the same empty-message block.
A simple policy helper is preferred over scattered conditionals. Example shape:
bool canSendMessage({
required BridgeMessageTarget target,
required String text,
}) {
if (target is BridgeMessageTarget_Poke) {
return true;
}
return text.trim().isNotEmpty;
}
The exact Dart type checks should follow the generated bridge type names used in the current codebase.
6. Notification And History Behavior
Existing no-message receiving behavior should remain the reference behavior:
- Incoming empty poke notification body falls back to text equivalent to
Alice pokes you. - Incoming poke with message includes the message in the notification body.
- Active-chat suppression and muted-sender preferences continue to apply.
- Poke history rows distinguish poke events from normal chat rows.
The send-side change must not introduce a new blank message row shape. If the sender's local history records sent pokes, empty poke history should render as a poke action line with no empty bubble.
7. Abuse And Safety Rules
This slice does not add new anti-spam controls. It relies on existing TeamSpeak-compatible permissions, inbound poke strength/rate suppression, notification preferences, active-chat suppression, and muted sender handling.
The implementation must not weaken any existing receiver-side controls. If testing reveals that empty sent pokes bypass suppression, notification preferences, or history classification, that is a bug to fix in the same implementation pass.
Future follow-ups, not part of this slice:
- Per-sender or per-server outbound poke cooldown UI.
- Receiver-side "never show poke dialog" equivalent beyond current notification preferences.
- Dedicated poke inbox or grouped poke history.
8. Files Expected To Change
Expected implementation targets:
| File | Expected change |
|---|---|
apps/chanora_flutter/lib/widgets/chat_views.dart |
Make composer validation and action enablement target-aware for poke. Update poke placeholder/action copy if needed. |
apps/chanora_flutter/test/widgets/chat_views_test.dart |
Add widget coverage for empty poke send and normal empty chat blocking. |
Optional targets if the implementation exposes missing copy or routing seams:
| File | Possible change |
|---|---|
apps/chanora_flutter/lib/main.dart |
Only if opening a poke target needs a clearer poke-specific title or route configuration. |
apps/chanora_flutter/lib/l10n/*.arb |
Only if current copy cannot express optional poke messages without hard-coded strings. |
apps/chanora_flutter/test/services/poke_notification_service_test.dart |
Only if send-side changes affect notification payload assumptions. |
The Rust protocol path should not need behavior changes unless tests prove that empty strings are blocked below Flutter.
9. Test Design
Required tests:
- Poke target shows an enabled primary
Pokeaction when the text field is empty. - Tapping
Pokeon an empty poke target calls the send callback withBridgeMessageTarget.pokeand an empty message. - Poke target still sends a typed message when text is present.
- Normal channel/server/private chat targets keep blocking empty sends.
- Empty poke history renders as a poke event line, not an empty text bubble.
Useful regression checks if already easy to target:
- Keyboard submit follows the same target-aware validation as the button.
- Failed empty-poke send keeps existing error presentation.
- Incoming empty poke notification tests still pass unchanged.
10. Validation
For the implementation branch, run focused Flutter verification first:
flutter test test/widgets/chat_views_test.dart
flutter test test/services/poke_notification_service_test.dart
flutter test test/services/poke_active_chat_test.dart
flutter analyze
If Rust or bridge files are touched, also run the matching Rust and bridge checks for the touched layer. Documentation-only changes require reading the affected spec and checking the diff; code tests are not required for this design commit.
11. Success Criteria
This design is implemented successfully when:
- A user can send a poke with no typed message.
- Normal chat targets still reject empty sends.
- The poke composer communicates that message text is optional.
- Empty pokes are represented as poke events in history and notifications.
- Existing poke notification preferences and suppression behavior remain intact.
- Focused widget/service tests and
flutter analyzepass, or any unrelated pre-existing failure is named with evidence.