Skip to content

Commit d940339

Browse files
authored
Merge pull request #189 from Warp-net/claude/app-polish
Fix blank image URLs and enable StrictMode in debug builds
2 parents 01940eb + 87af275 commit d940339

12 files changed

Lines changed: 170 additions & 167 deletions

File tree

cmd/node/member/node/member-node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ func (m *MemberNode) replyHandlers(
543543
},
544544
{
545545
event.PUBLIC_GET_REPLIES,
546-
handler.StreamGetRepliesHandler(r.replyRepo, userRepo, m),
546+
handler.StreamGetRepliesHandler(r.replyRepo),
547547
},
548548
}
549549
}

core/handler/reply.go

Lines changed: 32 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ package handler
2929

3030
import (
3131
"errors"
32-
"fmt"
3332
"strings"
3433

3534
"github.com/Warp-net/warpnet/core/stream"
@@ -304,84 +303,53 @@ func StreamDeleteReplyHandler(
304303
}
305304
}
306305

307-
func StreamGetRepliesHandler(
308-
repo ReplyStorer,
309-
userRepo ReplyUserFetcher,
310-
streamer ReplyStreamer,
311-
) warpnet.WarpHandlerFunc {
306+
// StreamGetRepliesHandler answers /public/get/replies requests.
307+
//
308+
// ev.RootId is the root tweet of the thread; ev.ParentId is the parent
309+
// TWEET id selecting which subtree of replies to return (NOT a user id —
310+
// it gets compared against tweet/reply ids in the repo). Clients send
311+
// an empty ParentId for "give me the top-level replies of the thread",
312+
// which we normalise to RootId so the repo lookup matches the first
313+
// tier of replies. Replies are served straight from the local store:
314+
// any reply we know about (because the author's node pushed it to us
315+
// via gossip, or because we cached an earlier fetch) is returned;
316+
// otherwise the response is empty.
317+
//
318+
// Note on routing: this used to try to forward the request to the
319+
// "parent user" by treating ParentId as a user id and looking it up in
320+
// userRepo. That can't work — ParentId is a tweet id — so the lookup
321+
// always returned ErrUserNotFound and we silently fell back to local
322+
// storage anyway. The dead code is removed; proper remote-fetch
323+
// routing would need a RootUserId in GetAllRepliesEvent to identify the
324+
// author of the root tweet, which clients don't currently send.
325+
func StreamGetRepliesHandler(repo ReplyStorer) warpnet.WarpHandlerFunc {
312326
return func(buf []byte, s warpnet.WarpStream) (any, error) {
313327
var ev event.GetAllRepliesEvent
314328
err := json.Unmarshal(buf, &ev)
315329
if err != nil {
316330
return nil, err
317331
}
318-
if ev.ParentId == "" {
319-
return nil, warpnet.WarpError("empty parent id")
320-
}
321332
if ev.RootId == "" {
322333
return nil, warpnet.WarpError("empty root id")
323334
}
335+
// Top-level replies on a thread have no parent — clients send an
336+
// empty parent_id in that case. Treat it as the root itself so
337+
// the repo returns the first-tier replies hanging off RootId.
338+
if ev.ParentId == "" {
339+
ev.ParentId = ev.RootId
340+
}
324341

325342
rootId := strings.TrimPrefix(ev.RootId, domain.RetweetPrefix)
326343
parentId := strings.TrimPrefix(ev.ParentId, domain.RetweetPrefix)
327-
isOwnTweetReplies := parentId == streamer.NodeInfo().OwnerId
328-
329-
if isOwnTweetReplies {
330-
replies, cursor, err := repo.GetRepliesTree(rootId, parentId, ev.Limit, ev.Cursor)
331-
if err != nil {
332-
return nil, err
333-
}
334-
return event.RepliesResponse{
335-
Cursor: cursor,
336-
Replies: replies,
337-
UserId: &parentId,
338-
}, nil
339-
}
340-
341-
parentUser, err := userRepo.Get(parentId)
342-
if errors.Is(err, database.ErrUserNotFound) {
343-
replies, cursor, _ := repo.GetRepliesTree(rootId, parentId, ev.Limit, ev.Cursor)
344-
return event.RepliesResponse{
345-
Cursor: cursor,
346-
Replies: replies,
347-
UserId: &parentId,
348-
}, nil
349-
}
350-
if err != nil {
351-
return nil, err
352-
}
353344

354-
replyDataResp, err := streamer.GenericStream(
355-
parentUser.NodeId,
356-
event.PUBLIC_GET_REPLIES,
357-
ev,
358-
)
359-
if errors.Is(err, warpnet.ErrNodeIsOffline) {
360-
replies, cursor, _ := repo.GetRepliesTree(rootId, parentId, ev.Limit, ev.Cursor)
361-
return event.RepliesResponse{
362-
Cursor: cursor,
363-
Replies: replies,
364-
UserId: &parentId,
365-
}, nil
366-
}
345+
replies, cursor, err := repo.GetRepliesTree(rootId, parentId, ev.Limit, ev.Cursor)
367346
if err != nil {
368347
return nil, err
369348
}
370-
371-
var possibleError event.ResponseError
372-
if _ = json.Unmarshal(replyDataResp, &possibleError); possibleError.Message != "" {
373-
return nil, fmt.Errorf("unmarshal other delete reply error response: %w", possibleError)
374-
}
375-
376-
var repliesResp event.RepliesResponse
377-
if err := json.Unmarshal(replyDataResp, &repliesResp); err != nil {
378-
return nil, err
379-
}
380-
for _, reply := range repliesResp.Replies {
381-
if _, err := repo.AddReply(reply.Reply); err != nil {
382-
log.Errorf("failed to add reply to replies repo: %v", err)
383-
}
384-
}
385-
return repliesResp, nil
349+
return event.RepliesResponse{
350+
Cursor: cursor,
351+
Replies: replies,
352+
UserId: &parentId,
353+
}, nil
386354
}
387355
}

core/handler/reply_test.go

Lines changed: 30 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -267,39 +267,52 @@ func TestStreamNewReplyHandler(t *testing.T) {
267267
}
268268

269269
func TestStreamGetRepliesHandler(t *testing.T) {
270-
owner := "owner-1"
271270
rootId := "root-1"
272271
parentId := "parent-1"
273272

274273
t.Run("invalid payload", func(t *testing.T) {
275-
h := StreamGetRepliesHandler(stubReplyRepo{}, stubReplyUserRepo{}, stubStreamer{})
274+
h := StreamGetRepliesHandler(stubReplyRepo{})
276275
_, err := h([]byte("{"), nil)
277276
if err == nil {
278277
t.Fatal("expected error")
279278
}
280279
})
281280

282-
t.Run("empty parent id", func(t *testing.T) {
283-
h := StreamGetRepliesHandler(stubReplyRepo{}, stubReplyUserRepo{}, stubStreamer{})
281+
t.Run("empty parent id defaults to root", func(t *testing.T) {
282+
// Top-level replies on a thread carry no parent_id from the
283+
// client — the handler must fall back to root_id so the repo
284+
// lookup runs against the first tier of replies hanging off
285+
// the root tweet.
286+
var seenRoot, seenParent string
287+
h := StreamGetRepliesHandler(
288+
stubReplyRepo{getRepliesTreeFn: func(rootID, parentIdArg string, _ *uint64, _ *string) ([]domain.ReplyNode, string, error) {
289+
seenRoot = rootID
290+
seenParent = parentIdArg
291+
return nil, "", nil
292+
}},
293+
)
284294
_, err := h(marshal(t, event.GetAllRepliesEvent{RootId: rootId}), nil)
285-
if err == nil || err.Error() != "empty parent id" {
295+
if err != nil {
286296
t.Fatalf("unexpected err: %v", err)
287297
}
298+
if seenRoot != rootId || seenParent != rootId {
299+
t.Fatalf("expected rootId %q and parentId %q from root fallback, got root=%q parent=%q", rootId, rootId, seenRoot, seenParent)
300+
}
288301
})
289302

290303
t.Run("empty root id", func(t *testing.T) {
291-
h := StreamGetRepliesHandler(stubReplyRepo{}, stubReplyUserRepo{}, stubStreamer{})
304+
h := StreamGetRepliesHandler(stubReplyRepo{})
292305
_, err := h(marshal(t, event.GetAllRepliesEvent{ParentId: parentId}), nil)
293306
if err == nil || err.Error() != "empty root id" {
294307
t.Fatalf("unexpected err: %v", err)
295308
}
296309
})
297310

298-
t.Run("own tweet replies", func(t *testing.T) {
311+
t.Run("serves replies from local repo", func(t *testing.T) {
299312
replies := []domain.ReplyNode{{Reply: domain.Tweet{Id: "r1", Text: "reply"}}}
300313
h := StreamGetRepliesHandler(stubReplyRepo{getRepliesTreeFn: func(rootID, parentIdArg string, limit *uint64, cursor *string) ([]domain.ReplyNode, string, error) {
301314
return replies, "end", nil
302-
}}, stubReplyUserRepo{}, stubStreamer{nodeInfo: warpnet.NodeInfo{OwnerId: parentId}})
315+
}})
303316
resp, err := h(marshal(t, event.GetAllRepliesEvent{RootId: rootId, ParentId: parentId}), nil)
304317
if err != nil {
305318
t.Fatalf("unexpected err: %v", err)
@@ -308,65 +321,19 @@ func TestStreamGetRepliesHandler(t *testing.T) {
308321
if len(r.Replies) != 1 {
309322
t.Fatalf("expected 1 reply, got %d", len(r.Replies))
310323
}
311-
})
312-
313-
t.Run("parent user not found fallback", func(t *testing.T) {
314-
h := StreamGetRepliesHandler(stubReplyRepo{}, stubReplyUserRepo{getFn: func(userId string) (domain.User, error) {
315-
return domain.User{}, database.ErrUserNotFound
316-
}}, stubStreamer{nodeInfo: warpnet.NodeInfo{OwnerId: owner}})
317-
resp, err := h(marshal(t, event.GetAllRepliesEvent{RootId: rootId, ParentId: parentId}), nil)
318-
if err != nil {
319-
t.Fatalf("unexpected err: %v", err)
320-
}
321-
_ = resp.(event.RepliesResponse)
322-
})
323-
324-
t.Run("stream node offline fallback", func(t *testing.T) {
325-
h := StreamGetRepliesHandler(stubReplyRepo{}, stubReplyUserRepo{}, stubStreamer{
326-
nodeInfo: warpnet.NodeInfo{OwnerId: owner},
327-
genericStreamFn: func(nodeId string, path stream.WarpRoute, data any) ([]byte, error) {
328-
return nil, warpnet.ErrNodeIsOffline
329-
},
330-
})
331-
resp, err := h(marshal(t, event.GetAllRepliesEvent{RootId: rootId, ParentId: parentId}), nil)
332-
if err != nil {
333-
t.Fatalf("unexpected err: %v", err)
324+
if r.Cursor != "end" {
325+
t.Fatalf("expected cursor 'end', got %q", r.Cursor)
334326
}
335-
_ = resp.(event.RepliesResponse)
336327
})
337328

338-
t.Run("stream error", func(t *testing.T) {
339-
streamErr := errors.New("broken")
340-
h := StreamGetRepliesHandler(stubReplyRepo{}, stubReplyUserRepo{}, stubStreamer{
341-
nodeInfo: warpnet.NodeInfo{OwnerId: owner},
342-
genericStreamFn: func(nodeId string, path stream.WarpRoute, data any) ([]byte, error) {
343-
return nil, streamErr
344-
},
345-
})
329+
t.Run("propagates repo error", func(t *testing.T) {
330+
boom := errors.New("db down")
331+
h := StreamGetRepliesHandler(stubReplyRepo{getRepliesTreeFn: func(string, string, *uint64, *string) ([]domain.ReplyNode, string, error) {
332+
return nil, "", boom
333+
}})
346334
_, err := h(marshal(t, event.GetAllRepliesEvent{RootId: rootId, ParentId: parentId}), nil)
347-
if !errors.Is(err, streamErr) {
348-
t.Fatalf("expected stream error: %v", err)
349-
}
350-
})
351-
352-
t.Run("remote successful response", func(t *testing.T) {
353-
remoteResp, _ := json.Marshal(event.RepliesResponse{
354-
Cursor: "end",
355-
Replies: []domain.ReplyNode{{Reply: domain.Tweet{Id: "r1", Text: "remote reply", RootId: rootId, ParentId: &parentId}}},
356-
})
357-
h := StreamGetRepliesHandler(stubReplyRepo{}, stubReplyUserRepo{}, stubStreamer{
358-
nodeInfo: warpnet.NodeInfo{OwnerId: owner},
359-
genericStreamFn: func(nodeId string, path stream.WarpRoute, data any) ([]byte, error) {
360-
return remoteResp, nil
361-
},
362-
})
363-
resp, err := h(marshal(t, event.GetAllRepliesEvent{RootId: rootId, ParentId: parentId}), nil)
364-
if err != nil {
365-
t.Fatalf("unexpected err: %v", err)
366-
}
367-
r := resp.(event.RepliesResponse)
368-
if len(r.Replies) != 1 {
369-
t.Fatalf("expected 1 reply: %v", r)
335+
if !errors.Is(err, boom) {
336+
t.Fatalf("expected db error, got %v", err)
370337
}
371338
})
372339
}

core/handler/self_request_test.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,6 @@ func TestOwnerSelfRequest_NoOutboundStream(t *testing.T) {
6060
ownerTweetUserRepo := stubTweetUserRepo{getFn: func(userId string) (domain.User, error) {
6161
return domain.User{Id: userId, NodeId: ownerNodeID}, nil
6262
}}
63-
ownerReplyUserRepo := stubReplyUserRepo{getFn: func(userId string) (domain.User, error) {
64-
return domain.User{Id: userId, NodeId: ownerNodeID}, nil
65-
}}
6663
ownerLikeUserRepo := stubLikeUserRepo{getFn: func(userId string) (domain.User, error) {
6764
return domain.User{Id: userId, NodeId: ownerNodeID}, nil
6865
}}
@@ -255,11 +252,9 @@ func TestOwnerSelfRequest_NoOutboundStream(t *testing.T) {
255252
})
256253

257254
t.Run("StreamGetRepliesHandler - replies under own tweet", func(t *testing.T) {
258-
streamer := stubStreamer{
259-
nodeInfo: ownerInfo,
260-
genericStreamFn: failOnStream(t),
261-
}
262-
h := StreamGetRepliesHandler(stubReplyRepo{}, ownerReplyUserRepo, streamer)
255+
// Replies handler is fully local now; no streamer/user repo
256+
// involved (see reply.go for why).
257+
h := StreamGetRepliesHandler(stubReplyRepo{})
263258
if _, err := h(marshal(t, event.GetAllRepliesEvent{RootId: rootID, ParentId: owner}), nil); err != nil {
264259
t.Fatalf("unexpected err: %v", err)
265260
}

domain/warpnet.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ type ReplyNode struct {
144144
const RetweetPrefix = "RT:"
145145

146146
// Tweet defines model for Tweet.
147+
//
148+
// ParentId is the parent TWEET id (not a user id) for replies; nil for
149+
// top-level tweets and for replies that hang directly off RootId.
147150
type Tweet struct {
148151
CreatedAt time.Time `json:"created_at"`
149152
UpdatedAt *time.Time `json:"updated_at,omitempty"`

event/event.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ type GetAllMessagesEvent struct {
134134
}
135135

136136
// GetAllRepliesEvent defines model for GetAllRepliesEvent.
137+
//
138+
// ParentId is the parent TWEET id (not a user id) — it selects which
139+
// subtree of replies inside RootId to return. Empty means "top-level
140+
// replies of the thread"; the handler treats that as ParentId = RootId.
141+
// RootId is the root tweet of the thread.
137142
type GetAllRepliesEvent struct {
138143
Cursor *string `json:"cursor,omitempty"`
139144
Limit *uint64 `json:"limit,omitempty"`
@@ -284,6 +289,12 @@ type NewMessageEvent = domain.ChatMessage
284289
type NewMessageResponse = domain.ChatMessage
285290

286291
// NewReplyEvent defines model for NewReplyEvent.
292+
//
293+
// ParentId is the parent TWEET id this reply is attached to (nil/empty
294+
// means the reply hangs directly off RootId). ParentUserId is the user
295+
// id of the parent tweet's author — that's the routing key the server
296+
// uses to forward the request to the right node when the parent tweet
297+
// lives on a remote peer.
287298
type NewReplyEvent struct {
288299
CreatedAt time.Time `json:"created_at"`
289300
Id domain.ID `json:"id"`

version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.7.15
1+
0.7.18

warpdroid/app/src/main/java/site/warpnet/warpdroid/EditProfileActivity.kt

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -200,19 +200,27 @@ class EditProfileActivity : BaseActivity() {
200200
(me.source?.fields?.size ?: 0) < maxAccountFields
201201

202202
if (viewModel.avatarData.value == null) {
203-
Glide.with(this@EditProfileActivity)
204-
.load(me.avatar)
205-
.placeholder(R.drawable.avatar_default)
206-
.transform(
207-
FitCenter(),
208-
RoundedCorners(
209-
resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)
203+
if (me.avatar.isNotBlank()) {
204+
Glide.with(this@EditProfileActivity)
205+
.load(me.avatar)
206+
.placeholder(R.drawable.avatar_default)
207+
.transform(
208+
FitCenter(),
209+
RoundedCorners(
210+
resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)
211+
)
210212
)
211-
)
212-
.into(binding.avatarPreview)
213+
.into(binding.avatarPreview)
214+
} else {
215+
// Profile has no avatar — fall back to
216+
// the same default that Glide would have
217+
// used as a placeholder, otherwise the
218+
// preview shows an empty ImageView.
219+
binding.avatarPreview.setImageResource(R.drawable.avatar_default)
220+
}
213221
}
214222

215-
if (viewModel.headerData.value == null) {
223+
if (viewModel.headerData.value == null && me.header.isNotBlank()) {
216224
Glide.with(this@EditProfileActivity)
217225
.load(me.header)
218226
.into(binding.headerPreview)

0 commit comments

Comments
 (0)