mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Implements the two highest-impact gaps from the client UX spec. Optimistic send: - messages.store gains addOptimisticMessage / markSendFailed / removeOptimistic, and confirmSend now stamps the real id + "sent" on the ack. addMessage reconciles the broadcast by real id (idempotent, replay-safe) with a defensive author match, so an echo never duplicates. Message gains status/correlationId/errorCode. - ChannelController.performSend renders a pending row immediately and supports retry / delete-draft (retry preserves attachments). - MessageList renders pending (dimmed) and failed (reason + Retry / Delete) rows; the hover action bar is limited to confirmed rows. - Failures are precise: the server echoes the request id on error replies (buildErrorMsgWithID), so the dispatcher maps SLOW_MODE / FORBIDDEN / RATE_LIMITED / BAD_REQUEST to the exact row instead of dropping the code. An offline send is shown failed, not silently lost. Composer permission + connection gating: - The server computes an authoritative per-channel can_send in the ready payload (channelCanSend mirrors MessageService.checkSendPermission: READ|SEND, MANAGE_MESSAGES for announcement, admin bypass, channel overrides). channels.store carries it as Channel.canSend. - MessageInput gains a disabled-with-reason mode; ChannelController derives the reason from can_send + channel type + connection status and disables the composer reactively (announcement read-only, no-permission, reconnecting) rather than accepting a click and failing. Older servers that omit can_send default permissive. Docs: the corresponding "Current gap" callouts in docs/architecture/ux are updated to reflect the implementation. Verified: full server suite + client tsc + 3204 unit tests + lint + gofmt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
47 lines
1.3 KiB
Go
47 lines
1.3 KiB
Go
package ws_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// TestBuildReady_IncludesCanSend confirms every ready channel carries the
|
|
// can_send affordance flag the client composer keys off.
|
|
func TestBuildReady_IncludesCanSend(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "cansend-user")
|
|
role, err := database.GetRoleByID(1)
|
|
if err != nil || role == nil {
|
|
t.Fatalf("GetRoleByID: %v", err)
|
|
}
|
|
if _, err := database.CreateChannel("general", "text", "", "", 0); err != nil {
|
|
t.Fatalf("CreateChannel: %v", err)
|
|
}
|
|
msg, err := hub.BuildReadyWithRoleForTest(database, user.ID, role)
|
|
if err != nil {
|
|
t.Fatalf("BuildReadyWithRoleForTest: %v", err)
|
|
}
|
|
var env struct {
|
|
Payload struct {
|
|
Channels []map[string]any `json:"channels"`
|
|
} `json:"payload"`
|
|
}
|
|
if err := json.Unmarshal(msg, &env); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if len(env.Payload.Channels) == 0 {
|
|
t.Fatal("expected at least one channel")
|
|
}
|
|
for _, ch := range env.Payload.Channels {
|
|
canSend, ok := ch["can_send"]
|
|
if !ok {
|
|
t.Errorf("channel %v missing can_send", ch["name"])
|
|
continue
|
|
}
|
|
// Owner role → can_send true everywhere.
|
|
if canSend != true {
|
|
t.Errorf("channel %v can_send = %v, want true for owner", ch["name"], canSend)
|
|
}
|
|
}
|
|
}
|